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