]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-handler.in
f51e9473d0d4331ad0945ba82acd3c65bfa89237
[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 os.WEXITSTATUS(status) != 0:
292          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
293
294    def prepare(self):
295       if not self.is_RW():
296          raise DVDError(0, "I won't prepare a non-rewritable medium")
297       
298       # Blank DVD+RW when there is no data on it
299       if self.is_plus_RW() and self.is_blank():
300          print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
301          self.blank()
302          return # It has been completely blanked: Medium is ready to be used by Bacula
303       
304       if self.is_minus_RW() and (not self.is_restricted_overwrite()):
305          print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
306          self.reformat_minus_RW()
307          return # Reformated: Medium is ready to be used by Bacula
308       
309       # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
310       if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
311          print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
312          self.blank()
313          return
314       
315       cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
316       print "Running " + cmd
317       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
318       proc = popen2.Popen4(cmd)
319       self.pid = proc.pid
320       status = proc.poll() 
321       while status == -1:
322          line = proc.fromchild.readline()
323          while len(line) > 0:
324             print line,
325             line = proc.fromchild.readline()
326          time.sleep(1)
327          status = proc.poll()
328       self.pid = 0
329       print
330       signal.signal(signal.SIGTERM, oldsig)
331       if os.WEXITSTATUS(status) != 0:
332          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
333
334    def blank(self):
335       cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
336       print "Running " + cmd
337       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
338       proc = popen2.Popen4(cmd)
339       self.pid = proc.pid
340       status = proc.poll()
341       while status == -1:
342          line = proc.fromchild.readline()
343          while len(line) > 0:
344             print line,
345             line = proc.fromchild.readline()
346          time.sleep(1)
347          status = proc.poll()
348       self.pid = 0
349       print
350       signal.signal(signal.SIGTERM, oldsig)
351       if os.WEXITSTATUS(status) != 0:
352          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
353
354    def reformat_minus_RW(self):
355       cmd = self.dvdrwformat + " -force " + self.device
356       print "Running " + cmd
357       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
358       proc = popen2.Popen4(cmd)
359       self.pid = proc.pid
360       status = proc.poll()
361       while status == -1:
362          line = proc.fromchild.readline()
363          while len(line) > 0:
364             print line,
365             line = proc.fromchild.readline()
366          time.sleep(1)
367          status = proc.poll()
368       self.pid = 0
369       print
370       signal.signal(signal.SIGTERM, oldsig)
371       if os.WEXITSTATUS(status) != 0:
372          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
373
374 # class disk ends here.
375
376 class DVDError(Exception):
377    def __init__(self, errno, value):
378       self.errno = errno
379       self.value = value
380       if self.value[-1] == '\n':
381          self.value = self.value[0:-1]
382    def __str__(self):
383       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
384
385 def usage():
386    print "Wrong number of arguments."
387    print """
388 Usage:
389
390 dvd-handler DEVICE test
391 dvd-handler DEVICE free
392 dvd-handler DEVICE write APPEND FILE
393 dvd-handler DEVICE blank
394
395 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
396
397 Operations:
398 test      Scan the device and report the information found.
399            This operation needs no further arguments.
400 free      Scan the device and report the available space.
401 write     Write a part file to disk.
402            This operation needs two additional arguments.
403            The first indicates to append (0), restart the
404            disk (1) or restart existing disk (2). The second
405            is the file to write.
406 prepare   Prepare a DVD+/-RW for being used by Bacula.
407            Note: This is only useful if you already have some
408            non-Bacula data on a medium, and you want to use
409            it with Bacula. Don't run this on blank media, it
410            is useless.
411 """
412    sys.exit(1)
413
414 if len(sys.argv) < 3:
415    usage()
416
417 dvd = disk(sys.argv[1])
418
419 if "free" == sys.argv[2]:
420    if len(sys.argv) == 3:
421       try:
422          free = dvd.free()
423       except DVDError, e:
424          if e.errno != 0:
425             print -e.errno
426          else:
427             print errno.EPIPE
428          print str(e)
429       else:
430          print free
431          print "No Error reported."
432    else:
433       print "Wrong number of arguments for free operation."
434       usage()
435 elif "prepare" == sys.argv[2]:
436    if len(sys.argv) == 3:
437       try:
438          dvd.prepare()
439       except DVDError, e:
440          print "Error while preparing medium: ", str(e)
441          if e.errno != 0:
442             sys.exit(e.errno & 0x7F)
443          else:
444             sys.exit(errno.EPIPE)
445       else:
446          print "Medium prepared successfully."
447    else:
448       print "Wrong number of arguments for prepare operation."
449       usage()
450 elif "test" == sys.argv[2]:
451    try:
452       print str(dvd)
453       print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
454       print "Free space: " + str(dvd.free())
455    except DVDError, e:
456       print "Error while getting informations: ", str(e)
457 elif "write" == sys.argv[2]:
458    if len(sys.argv) == 5:
459       try:
460          dvd.write(long(sys.argv[3]), sys.argv[4])
461       except DVDError, e:
462          print "Error while writing part file: ", str(e)
463          if e.errno != 0:
464             sys.exit(e.errno & 0x7F)
465          else:
466             sys.exit(errno.EPIPE)
467       else:
468          print "Part file " + sys.argv[4] + " successfully written to disk."
469    else:
470       print "Wrong number of arguments for write operation."
471       usage()
472       sys.exit(1)
473 else:
474    print "No operation - use test, free, prepare or write."
475    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
476 sys.exit(0)