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