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