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