]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-handler.in
Add Phil Stracchino's fix for Qt5
[bacula/bacula] / bacula / scripts / dvd-handler.in
1 #!@PYTHON@
2 #
3 #   Bacula(R) - The Network Backup Solution
4 #
5 #   Copyright (C) 2000-2016 Kern Sibbald
6 #
7 #   The original author of Bacula is Kern Sibbald, with contributions
8 #   from many others, a complete list can be found in the file AUTHORS.
9 #
10 #   You may use this file and others of this release according to the
11 #   license defined in the LICENSE file, which includes the Affero General
12 #   Public License, v3.0 ("AGPLv3") and some additional permissions and
13 #   terms pursuant to its AGPLv3 Section 7.
14 #
15 #   This notice must be preserved when any source code is 
16 #   conveyed and/or propagated.
17 #
18 #   Bacula(R) is a registered trademark of Kern Sibbald.
19 #
20 # Check the free space available on a writable DVD
21 # Should always exit with 0 status, otherwise it indicates a serious error.
22 # (wrong number of arguments, Python exception...)
23 #
24 #  called:  dvd-handler <dvd-device-name> operation args
25 #
26 #  operations used by Bacula:
27 #
28 #   free  (no arguments)
29 #             Scan the device and report the available space. It returns:
30 #             Prints on the first output line the free space available in bytes.
31 #             If an error occurs, prints a negative number (-errno), followed,
32 #             on the second line, by an error message.
33 #
34 #   write  op filename
35 #              Write a part file to disk.
36 #              This operation needs two additional arguments.
37 #              The first (op) indicates to
38 #                  0 -- append
39 #                  1 -- first write to a blank disk
40 #                  2 -- blank or truncate a disk
41 #
42 #               The second is the filename to write
43 #
44 #   operations available but not used by Bacula:
45 #
46 #   test      Scan the device and report the information found.
47 #              This operation needs no further arguments.
48 #   prepare   Prepare a DVD+/-RW for being used by Bacula.
49 #              Note: This is only useful if you already have some
50 #              non-Bacula data on a medium, and you want to use
51 #              it with Bacula. Don't run this on blank media, it
52 #              is useless.
53 #
54
55 # $Id$
56 #
57
58 import popen2
59 import os
60 import os.path
61 import errno
62 import sys
63 import re
64 import signal
65 import time
66 import array
67
68 class disk:
69 # Configurable values:
70    
71    dvdrwmediainfo = "@DVDRWMEDIAINFO@"
72    growcmd = "@GROWISOFS@"
73    dvdrwformat = "@DVDRWFORMAT@"
74    dd = "@DD@"
75    margin = 10485760 # 10 mb security margin
76
77    # Comment the following line if you want the tray to be reloaded
78    # when writing ends.
79    growcmd += " -use-the-force-luke=notray"
80
81 # end of configurable values
82
83 ###############################################################################
84 #
85 # This class represents DVD disk informations.
86 # When instantiated, it needs a device name.
87 # Status information about the device and the disk loaded is collected only when
88 # asked for (for example dvd-freespace doesn't need to know the media type, and
89 # dvd-writepart doesn't not always need to know the free space).
90 #
91 # The following methods are implemented:
92 # __init__       we need that...
93 # __repr__       this seems to be a good idea to have.
94 #                Quite minimalistic implementation, though.
95 # __str__        For casts to string. Return the current disk information
96 # is_empty       Returns TRUE if the disk is empty, blank... this needs more
97 #                work, especially concerning non-RW media and blank vs. no
98 #                filesystem considerations. Here, we should also look for
99 #                other filesystems - probably we don't want to silently
100 #                overwrite UDF or ext2 or anything not mentioned in fstab...
101 #                (NB: I don't think it is a problem)
102 # free           Returns the available free space.
103 # write          Writes one part file to disk, either starting a new file
104 #                system on disk, or appending to it.
105 #                This method should also prepare a blank disk so that a
106 #                certain part of the disk is used to allow detection of a
107 #                used disk by all / more disk drives.
108 # prepare        Blank the device
109 #
110 ###############################################################################
111    def __init__(self, devicename):
112       self.device = devicename
113       self.disktype = "none"
114       self.diskmode = "none"
115       self.diskstatus = "none"
116       self.hardwaredevice = "none"
117       self.pid = 0
118       self.next_session = -1
119       self.capacity = -1
120
121       self.freespace_collected = 0
122       self.mediumtype_collected = 0
123
124       self.growcmd += " -quiet"
125
126       if self.is4gbsupported():
127          self.growcmd += " -use-the-force-luke=4gms"
128
129       self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
130                         "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
131
132       return
133
134    def __repr__(self):
135       return "disk(" + self.device + ") # This is an instance of class disk"
136
137    def __str__(self):
138       if not self.freespace_collected:
139          self.collect_freespace();
140       if not self.mediumtype_collected:
141          self.collect_mediumtype();
142       
143       self.me  = "Class disk, initialized with device '" + self.device + "'\n"
144       self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
145       self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
146       self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
147       self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
148       return self.me
149
150    ## Check if we want to allow growisofs to cross the 4gb boundary
151    def is4gbsupported(self):
152       processi = popen2.Popen4("uname -s -r")
153       status = processi.wait()
154       if not os.WIFEXITED(status):
155          return 1
156       if os.WEXITSTATUS(status) != 0:
157          return 1
158       strres = processi.fromchild.readline()[0:-1]
159       version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
160       if not version: # Non-Linux: allow
161          return 1
162       
163       if (int(version.group(1)) > 2) or (int(version.group(2)) > 6) or ((int(version.group(1)) == 2) and (int(version.group(2)) == 6) and (int(version.group(3)) >= 8)):
164          return 1
165       else:
166          return 0
167
168    def collect_freespace(self): # Collects current free space
169       self.cmd = self.growcmd + " -F " + self.device
170       processi = popen2.Popen4(self.cmd)
171       status = processi.wait()
172       if not os.WIFEXITED(status):
173          raise DVDError(0, "growisofs process did not exit correctly.")
174       result = processi.fromchild.read()
175       if os.WEXITSTATUS(status) != 0:
176          if (os.WEXITSTATUS(status) & 0x7F) == errno.ENOSPC:
177             # Kludge to force dvd-handler to return a free space of 0
178             self.next_session = 1
179             self.capacity = 1
180             self.freespace_collected = 1
181             return
182          else:
183             raise DVDError(os.WEXITSTATUS(status), "growisofs returned with an error " + result + ". Please check your are using a patched version of dvd+rw-tools.")
184       next_sess = re.search(r"\snext_session=(\d+)\s", result, re.MULTILINE)
185       capa = re.search(r"\scapacity=(\d+)\s", result, re.MULTILINE)
186    
187       if next_sess and capa:
188          self.next_session = long(next_sess.group(1))
189          self.capacity = long(capa.group(1))
190          
191          # testing cheat (emulate 4GB boundary at 100MB)
192          #if self.next_session > 100000000:
193          #   self.capacity = self.next_session
194       else:
195          raise DVDError(0, "Cannot get next_session and capacity from growisofs.\nReturned: " + result)
196       
197       self.freespace_collected = 1
198       return
199    
200    def collect_mediumtype(self): # Collects current medium type
201       self.lasterror = ""
202       cmd = self.dvdrwmediainfo + " " + self.device
203       processi = popen2.Popen4(cmd)
204       status = processi.wait()
205       if not os.WIFEXITED(status):
206          raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
207       if os.WEXITSTATUS(status) != 0:
208          raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
209          return
210       result = processi.fromchild.read()
211       
212       hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
213       mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
214       mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
215       status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
216       
217       if hardware:
218          self.hardwaredevice = hardware.group(1)
219       
220       if mediatype:
221          self.disktype = mediatype.group(2)
222       else:
223          raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
224       
225       if self.disktype == "DVD-RW":
226          if mediamode:
227             self.diskmode = mediamode.group(1)
228          else:
229             raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
230       
231       if status:
232          self.diskstatus = status.group(1)
233       else:
234          raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
235
236       
237       self.mediumtype_collected = 1
238       return
239
240    def is_empty(self):
241       if not self.freespace_collected:
242          self.collect_freespace();
243       
244       return 0 == self.next_session
245
246    def is_RW(self):
247       if not self.mediumtype_collected:
248          self.collect_mediumtype();
249       return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
250
251    def is_plus_RW(self):
252       if not self.mediumtype_collected:
253          self.collect_mediumtype();
254       return "DVD+RW" == self.disktype
255
256    def is_minus_RW(self):
257       if not self.mediumtype_collected:
258          self.collect_mediumtype();
259       return "DVD-RW" == self.disktype
260       
261    def is_restricted_overwrite(self):
262       if not self.mediumtype_collected:
263          self.collect_mediumtype();
264       return self.diskmode == "Restricted Overwrite"
265
266    def is_blank(self):
267       if not self.mediumtype_collected:
268          self.collect_mediumtype();
269       
270       return self.diskstatus == "blank"
271
272    def free(self):
273       if not self.freespace_collected:
274          self.collect_freespace();
275       
276       fr = self.capacity-self.next_session-self.margin
277       if fr < 0:
278          return 0
279       else:
280          return fr
281
282    def term_handler(self, signum, frame):
283       print 'dvd-handler: Signal term_handler called with signal', signum
284       if self.pid != 0:
285          print "dvd-handler: Sending SIGTERM to pid", self.pid
286          os.kill(self.pid, signal.SIGTERM)
287          time.sleep(10)
288          print "dvd-handler: Sending SIGKILL to pid", self.pid
289          os.kill(self.pid, signal.SIGKILL)
290          sys.exit(1)
291
292    def write(self, newvol, partfile):
293       # Blank DVD+RW when there is no data on it
294       if newvol and self.is_plus_RW() and self.is_blank():
295          print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
296          self.blank()
297          print "Done, now writing the part file."
298       
299       if newvol and self.is_minus_RW() and (not self.is_restricted_overwrite()):
300          print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
301          self.reformat_minus_RW()
302          print "Done, now writing the part file."
303       
304       cmd = self.growcmd + self.growparams
305       if newvol:
306          # Ignore any existing iso9660 filesystem - used for truncate
307          if newvol == 2:
308              cmd += " -use-the-force-luke=tty"
309          cmd += " -Z "
310       else:
311          cmd += " -M "
312       cmd += self.device + " " + str(partfile)
313       print "Running " + cmd
314       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
315       proc = popen2.Popen4(cmd)
316       self.pid = proc.pid
317       status = proc.poll()
318       while status == -1:
319          line = proc.fromchild.readline()
320          while len(line) > 0:
321             print line,
322             line = proc.fromchild.readline()
323          time.sleep(1)
324          status = proc.poll()
325       self.pid = 0
326       print
327       signal.signal(signal.SIGTERM, oldsig)
328       if not os.WIFEXITED(status):
329          raise DVDError(0, cmd + " process did not exit correctly, signal/status " + str(status))
330       if os.WEXITSTATUS(status) != 0:
331          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
332
333    def prepare(self):
334       if not self.is_RW():
335          raise DVDError(0, "I won't prepare a non-rewritable medium")
336       
337       # Blank DVD+RW when there is no data on it
338       if self.is_plus_RW() and self.is_blank():
339          print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
340          self.blank()
341          return # It has been completely blanked: Medium is ready to be used by Bacula
342       
343       if self.is_minus_RW() and (not self.is_restricted_overwrite()):
344          print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
345          self.reformat_minus_RW()
346          return # Reformated: Medium is ready to be used by Bacula
347       
348       # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
349       if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
350          print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
351          self.blank()
352          return
353       
354       cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
355       print "Running " + cmd
356       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
357       proc = popen2.Popen4(cmd)
358       self.pid = proc.pid
359       status = proc.poll() 
360       while status == -1:
361          line = proc.fromchild.readline()
362          while len(line) > 0:
363             print line,
364             line = proc.fromchild.readline()
365          time.sleep(1)
366          status = proc.poll()
367       self.pid = 0
368       print
369       signal.signal(signal.SIGTERM, oldsig)
370       if os.WEXITSTATUS(status) != 0:
371          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
372
373    def blank(self):
374       cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
375       print "Running " + cmd
376       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
377       proc = popen2.Popen4(cmd)
378       self.pid = proc.pid
379       status = proc.poll()
380       while status == -1:
381          line = proc.fromchild.readline()
382          while len(line) > 0:
383             print line,
384             line = proc.fromchild.readline()
385          time.sleep(1)
386          status = proc.poll()
387       self.pid = 0
388       print
389       signal.signal(signal.SIGTERM, oldsig)
390       if os.WEXITSTATUS(status) != 0:
391          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
392
393    def reformat_minus_RW(self):
394       cmd = self.dvdrwformat + " -force " + self.device
395       print "Running " + cmd
396       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
397       proc = popen2.Popen4(cmd)
398       self.pid = proc.pid
399       status = proc.poll()
400       while status == -1:
401          line = proc.fromchild.readline()
402          while len(line) > 0:
403             print line,
404             line = proc.fromchild.readline()
405          time.sleep(1)
406          status = proc.poll()
407       self.pid = 0
408       print
409       signal.signal(signal.SIGTERM, oldsig)
410       if os.WEXITSTATUS(status) != 0:
411          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
412
413 # class disk ends here.
414
415 class DVDError(Exception):
416    def __init__(self, errno, value):
417       self.errno = errno
418       self.value = value
419       if self.value[-1] == '\n':
420          self.value = self.value[0:-1]
421    def __str__(self):
422       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
423
424 def usage():
425    print "Wrong number of arguments."
426    print """
427 Usage:
428
429 dvd-handler DEVICE test
430 dvd-handler DEVICE free
431 dvd-handler DEVICE write APPEND FILE
432 dvd-handler DEVICE prepare
433
434 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
435
436 Operations:
437 test      Scan the device and report the information found.
438            This operation needs no further arguments.
439 free      Scan the device and report the available space.
440 write     Write a part file to disk.
441            This operation needs two additional arguments.
442            The first indicates to append (0), restart the
443            disk (1) or restart existing disk (2). The second
444            is the file to write.
445 prepare   Prepare a DVD+/-RW for being used by Bacula.
446            Note: This is only useful if you already have some
447            non-Bacula data on a medium, and you want to use
448            it with Bacula. Don't run this on blank media, it
449            is useless.
450 """
451    sys.exit(1)
452
453 if len(sys.argv) < 3:
454    usage()
455
456 dvd = disk(sys.argv[1])
457
458 if "free" == sys.argv[2]:
459    if len(sys.argv) == 3:
460       try:
461          free = dvd.free()
462       except DVDError, e:
463          if e.errno != 0:
464             print -e.errno
465          else:
466             print errno.EPIPE
467          print str(e)
468       else:
469          print free
470          print "No Error reported."
471    else:
472       print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
473       usage()
474 elif "prepare" == sys.argv[2]:
475    if len(sys.argv) == 3:
476       try:
477          dvd.prepare()
478       except DVDError, e:
479          print "Error while preparing medium: ", str(e)
480          if e.errno != 0:
481             sys.exit(e.errno & 0x7F)
482          else:
483             sys.exit(errno.EPIPE)
484       else:
485          print "Medium prepared successfully."
486    else:
487       print "Wrong number of arguments for prepare operation. Wanted 3 got", len(sys.argv)
488       usage()
489 elif "test" == sys.argv[2]:
490    try:
491       print str(dvd)
492       print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
493       print "Free space: " + str(dvd.free())
494    except DVDError, e:
495       print "Error while getting informations: ", str(e)
496 elif "write" == sys.argv[2]:
497    if len(sys.argv) == 5:
498       try:
499          dvd.write(long(sys.argv[3]), sys.argv[4])
500       except DVDError, e:
501          print "Error while writing part file: ", str(e)
502          if e.errno != 0:
503             sys.exit(e.errno & 0x7F)
504          else:
505             sys.exit(errno.EPIPE)
506       else:
507          print "Part file " + sys.argv[4] + " successfully written to disk."
508    else:
509       print "Wrong number of arguments for write operation. Wanted 5 got", len(sys.argv)
510       usage()
511       sys.exit(1)
512 else:
513    print "No operation - use test, free, prepare or write."
514    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
515 sys.exit(0)