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