]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-handler.in
53e42fe8860f95efcee209ae36fdbc639e8d8966
[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 #  where operation is one of
10 #    free
11 #    write
12 #
13 # in case of operation ``free'' returns:
14 # Prints on the first output line the free space available in bytes.
15 # If an error occurs, prints a negative number (-errno), followed,
16 # on the second line, by an error message.
17
18 # $Id$
19 #
20
21 import popen2
22 import os
23 import errno
24 import sys
25 import re
26 import signal
27 import time
28
29 # Configurable values:
30 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
31 growcmd = "@GROWISOFS@"
32 margin = 10485760 # 10 mb security margin
33
34 # Comment the following line if you want the tray to be reloaded
35 # when writing ends.
36 growcmd += " -use-the-force-luke=notray"
37
38 # end of configurable values
39
40 ## Check if we want to allow growisofs to cross the 4gb boundary
41 def is4gbsupported():
42    processi = popen2.Popen4("uname -s -r")
43    status = processi.wait()
44    if not os.WIFEXITED(status):
45 #      print "dvd-writepart: Cannot execute uname, allowing to cross the 4gb boundary."
46       return 1
47    if os.WEXITSTATUS(status) != 0:
48 #      print "dvd-writepart: Cannot execute uname, allowing to cross the 4gb boundary."
49       return 1
50    strres = processi.fromchild.readline()[0:-1]
51    res = strres.split(" ")
52    if len(res) != 2:
53 #      print "dvd-writepart: Unable to parse uname (" + strres + "), allowing to cross the 4gb boundary."
54       return 1
55    if res[0] != "Linux":
56 #      print "dvd-writepart: The current OS is no Linux, allowing to cross the 4gb boundary."
57       return 1
58    ver = res[1].split(".")
59    if len(ver) < 3:
60 #      print "dvd-writepart: Unable to parse version string (" + res[1] + "), allowing to cross the 4gb boundary."
61       return 1
62    subver = ver[2].split("-")
63    
64    if ((not ver[0].isdigit()) or (not ver[1].isdigit()) or (not subver[0].isdigit())):
65 #      print "dvd-writepart: Unable to parse version string (" + res[1] + "), allowing to cross the 4gb boundary."
66       return 1
67    
68    if (int(ver[0]) > 2) or (int(ver[1]) > 6) or ((int(ver[0]) == 2) and (int(ver[1]) == 6) and (int(subver[0]) >= 8)):
69 #      print "dvd-writepart: Kernel version >=2.6.8, allowing to cross the 4gb boundary."
70       return 1
71    else:
72 #      print "dvd-writepart: Kernel version <2.6.8, not allowing to cross the 4gb boundary."
73       return 0
74
75 class DVDError(Exception):
76    def __init__(self, errno, value):
77       self.errno = errno
78       self.value = value
79       if self.value[-1] == '\n':
80          self.value = self.value[0:-1]
81    def __str__(self):
82       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
83
84 class disk:
85 ###############################################################################
86 #
87 # This class represents DVD disk informations.
88 # When instantiated, it needs a device name.
89 # Status information about the device and the disk loaded is collected only when
90 # asked for (for example dvd-freespace doesn't need to know the media type, and
91 # dvd-writepart doesn't not always need to know the free space).
92 #
93 # The following methods are implemented:
94 # __init__       we need that...
95 # __repr__       this seems to be a good idea to have.
96 #                Quite minimalistic implementation, though.
97 # __str__        For casts to string. Return the current disk information
98 # is_empty       Returns TRUE if the disk is empty, blank... this needs more
99 #                work, especially concerning non-RW media and blank vs. no
100 #                filesystem considerations. Here, we should also look for
101 #                other filesystems - probably we don't want to silently
102 #                overwrite UDF or ext2 or anything not mentioned in fstab...
103 #                (NB: I don't think it is a problem)
104 # free           Returns the available free space.
105 # write          Writes one part file to disk, either starting a new file
106 #                system on disk, or appending to it.
107 #                This method should also prepare a blank disk so that a
108 #                certain part of the disk is used to allow detection of a
109 #                used disk by all / more disk drives.
110 # blank          Blank the device
111 #
112 ###############################################################################
113    def __init__(self, devicename):
114       self.device = devicename
115       self.disktype = "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       return
125
126    def __repr__(self):
127       return "disk(" + self.device + ") # This is an instance of class disk"
128
129    def __str__(self):
130       if not self.freespace_collected:
131          self.collect_freespace();
132       if not self.mediumtype_collected:
133          self.collect_mediumtype();
134       
135       self.me  = "Class disk, initialized with device " + self.device + "\n"
136       self.me += "type = " + self.disktype
137       self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
138       self.me += "Hardware device is " + self.hardwaredevice + "\n"
139       return self.me
140
141    def collect_freespace(self): # Collects current free space
142       self.cmd = growcmd + " -F " + self.device
143       processi = popen2.Popen4(self.cmd)
144       status = processi.wait()
145       if not os.WIFEXITED(status):
146          raise DVDError(0, "growisofs process did not exit correctly.")
147       result = processi.fromchild.read()
148       if os.WEXITSTATUS(status) != 0:
149          if (os.WEXITSTATUS(status) & 0x7F) == errno.ENOSPC:
150             # Kludge to force dvd-handler to return a free space of 0
151             self.next_session = 1
152             self.capacity = 1
153             self.freespace_collected = 1
154             return
155          else:
156             raise DVDError(os.WEXITSTATUS(status), "growisofs returned with an error " + result + ". Please check your are using a patched version of dvd+rw-tools.")
157       next_sess = re.search(r"\snext_session=(\d+)\s", result, re.MULTILINE)
158       capa = re.search(r"\capacity=(\d+)\s", result, re.MULTILINE)
159    
160       if next_sess and capa:
161          self.next_session = long(next_sess.group(1))
162          self.capacity = long(capa.group(1))
163          
164          # testing cheat (emulate 4GB boundary at 100MB)
165          #if self.next_session > 100000000:
166          #   self.capacity = self.next_session
167       else:
168          raise DVDError(0, "Cannot get next_session and capacity from growisofs.\nReturned: " + result)
169       
170       self.freespace_collected = 1
171       return
172    
173    def collect_mediumtype(self): # Collects current medium type
174       self.lasterror = ""
175       cmd = dvdrwmediainfo + " " + self.device
176       processi = popen2.Popen4(cmd)
177       status = processi.wait()
178       if not os.WIFEXITED(status):
179          raise DVDError(dvdrwmediainfo + " process did not exit correctly.")
180       if os.WEXITSTATUS(status) != 0:
181          raise DVDError("Cannot get media info from " + dvdrwmediainfo)
182          return
183       result = processi.fromchild.read()
184       
185       hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
186       mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
187       
188       if hardware:
189          self.hardwaredevice = hardware.group(1)
190       if mediatype:
191          self.disktype = mediatype.group(2)
192       else:
193          raise DVDError("Media type not found in " + dvdrwmediainfo + " output")
194       self.mediumtype_collected = 1
195       return
196
197    def is_empty(self):
198       if not self.freespace_collected:
199          self.collect_freespace();
200       
201       return 0 == self.next_session
202
203    def is_RW(self):
204       if not self.mediumtype_collected:
205          self.collect_mediumtype();
206       
207       return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
208
209    def free(self):
210       if not self.freespace_collected:
211          self.collect_freespace();
212       
213       return self.capacity-self.next_session-margin
214
215    def term_handler(self, signum, frame):
216       print 'dvd-handler: Signal term_handler called with signal', signum
217       if self.pid != 0:
218          print "dvd-handler: Sending SIGTERM to pid", self.pid
219          os.kill(self.pid, signal.SIGTERM)
220          time.sleep(10)
221          print "dvd-handler: Sending SIGKILL to pid", self.pid
222          os.kill(self.pid, signal.SIGKILL)
223          sys.exit(1)
224
225    def write(self, newvol, partfile):
226       # Blank DVD+/-RW/-RAM when there is no data on it
227       # Ideally, we should only have to do it once, but I don't know how to
228       # identify if a disk is blank of if it is brand-new.
229       if newvol and self.is_RW() and self.is_empty():
230          print "DVD looks brand-new, blank it to fix some DVD-writers bugs."
231          self.blank()
232          print "Done, now writing the real part file."
233       
234       cmd = growcmd + growparams
235       if newvol:
236          cmd += " -Z "
237       else:
238          cmd += " -M "
239       cmd += self.device + " " + str(partfile)
240       print "Running " + cmd
241       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
242       proc = popen2.Popen4(cmd)
243       self.pid = proc.pid
244       status = proc.poll()
245       while status == -1:
246          line = proc.fromchild.readline()
247          while len(line) > 0:
248             print line,
249             line = proc.fromchild.readline()
250          time.sleep(1)
251          status = proc.poll()
252       self.pid = 0
253       print
254       signal.signal(signal.SIGTERM, oldsig)
255       if os.WEXITSTATUS(status) != 0:
256          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
257
258    def blank(self):
259       cmd = growcmd + " -Z " + self.device + "=/dev/zero"
260       print "Running " + cmd
261       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
262       proc = popen2.Popen4(cmd)
263       self.pid = proc.pid
264       status = proc.poll()
265       while status == -1:
266          line = proc.fromchild.readline()
267          while len(line) > 0:
268             print line,
269             line = proc.fromchild.readline()
270          time.sleep(1)
271          status = proc.poll()
272       self.pid = 0
273       print
274       signal.signal(signal.SIGTERM, oldsig)
275       if os.WEXITSTATUS(status) != 0:
276          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
277
278 # class disk ends here.
279
280 def usage():
281    print "Wrong number of arguments."
282    print """
283 Usage:
284
285 dvd-handler DEVICE test
286 dvd-handler DEVICE free
287 dvd-handler DEVICE write APPEND FILE
288
289 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
290
291 Operations:
292 test       Scan the device and report the information found.
293            This operation needs no further arguments.
294 free       Scan the device and report the available space.
295 write      Write a part file to disk.
296            This operation needs two additional arguments.
297            The first indicates to append (0) or restart the
298            disk (1). The second is the file to write.
299 """
300    sys.exit(1)
301
302 if len(sys.argv) < 3:
303    usage()
304
305 growcmd += " -quiet"
306  
307 if is4gbsupported():
308    growcmd += " -use-the-force-luke=4gms"
309
310 growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
311               "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
312
313 dvd = disk(sys.argv[1])
314
315 if "free" == sys.argv[2]:
316    if len(sys.argv) == 3:
317       try:
318          free = dvd.free()
319       except DVDError, e:
320          if e.errno != 0:
321             print -e.errno
322          else:
323             print errno.EPIPE
324          print str(e)
325       else:
326          print free
327          print "No Error reported."
328    else:
329       print "Wrong number of arguments for free operation."
330       usage()
331 elif "test" == sys.argv[2]:
332    try:
333       print str(dvd)
334       print "Empty disk: " + str(dvd.is_empty()) + " ReWritable disk: " + str(dvd.is_RW())
335       print "Free space: " + str(dvd.free())
336    except DVDError, e:
337       print "Error while getting informations: ", str(e)
338 elif "write" == sys.argv[2]:
339    if len(sys.argv) == 5:
340       try:
341          dvd.write(long(sys.argv[3]), sys.argv[4])
342       except DVDError, e:
343          print "Error while writing part file: ", str(e)
344          if e.errno != 0:
345             sys.exit(e.errno & 0x7F)
346          else:
347             sys.exit(errno.EPIPE)
348       else:
349          print "Part file " + sys.argv[4] + " successfully written to disk."
350    else:
351       print "Wrong number of arguments for write operation."
352       usage()
353       sys.exit(1)
354 else:
355    print "No operation - use test, free or write."
356    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
357 sys.exit(0)