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...)
7 # called: dvd-handler <dvd-device-name> operation args
9 # where operation is one of
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.
31 # Configurable values:
33 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
34 growcmd = "@GROWISOFS@"
35 dvdrwformat = "@DVDRWFORMAT@"
36 margin = 10485760 # 10 mb security margin
38 # Comment the following line if you want the tray to be reloaded
40 growcmd += " -use-the-force-luke=notray"
42 # end of configurable values
44 ###############################################################################
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).
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
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"
79 self.next_session = -1
82 self.freespace_collected = 0
83 self.mediumtype_collected = 0
85 self.growcmd += " -quiet"
87 if self.is4gbsupported():
88 self.growcmd += " -use-the-force-luke=4gms"
90 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
91 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
96 return "disk(" + self.device + ") # This is an instance of class disk"
99 if not self.freespace_collected:
100 self.collect_freespace();
101 if not self.mediumtype_collected:
102 self.collect_mediumtype();
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"
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):
117 if os.WEXITSTATUS(status) != 0:
119 strres = processi.fromchild.readline()[0:-1]
120 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
121 if not version: # Non-Linux: allow
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)):
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
141 self.freespace_collected = 1
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)
148 if next_sess and capa:
149 self.next_session = long(next_sess.group(1))
150 self.capacity = long(capa.group(1))
152 # testing cheat (emulate 4GB boundary at 100MB)
153 #if self.next_session > 100000000:
154 # self.capacity = self.next_session
156 raise DVDError(0, "Cannot get next_session and capacity from growisofs.\nReturned: " + result)
158 self.freespace_collected = 1
161 def collect_mediumtype(self): # Collects current medium type
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)
171 result = processi.fromchild.read()
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)
179 self.hardwaredevice = hardware.group(1)
182 self.disktype = mediatype.group(2)
184 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
186 if self.disktype == "DVD-RW":
188 self.diskmode = mediamode.group(1)
190 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
193 self.diskstatus = status.group(1)
195 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
198 self.mediumtype_collected = 1
202 if not self.freespace_collected:
203 self.collect_freespace();
205 return 0 == self.next_session
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
212 def is_plus_RW(self):
213 if not self.mediumtype_collected:
214 self.collect_mediumtype();
215 return "DVD+RW" == self.disktype
217 def is_minus_RW(self):
218 if not self.mediumtype_collected:
219 self.collect_mediumtype();
220 return "DVD-RW" == self.disktype
222 def is_restricted_overwrite(self):
223 if not self.mediumtype_collected:
224 self.collect_mediumtype();
225 return self.diskmode == "Restricted Overwrite"
228 if not self.mediumtype_collected:
229 self.collect_mediumtype();
231 return self.diskstatus == "blank"
234 if not self.freespace_collected:
235 self.collect_freespace();
237 fr = self.capacity-self.next_session-self.margin
243 def term_handler(self, signum, frame):
244 print 'dvd-handler: Signal term_handler called with signal', signum
246 print "dvd-handler: Sending SIGTERM to pid", self.pid
247 os.kill(self.pid, signal.SIGTERM)
249 print "dvd-handler: Sending SIGKILL to pid", self.pid
250 os.kill(self.pid, signal.SIGKILL)
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."
258 print "Done, now writing the part file."
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."
265 cmd = self.growcmd + self.growparams
270 cmd += self.device + " " + str(partfile)
271 print "Running " + cmd
272 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
273 proc = popen2.Popen4(cmd)
277 line = proc.fromchild.readline()
280 line = proc.fromchild.readline()
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))
291 raise DVDError(0, "I won't prepare a non-rewritable medium")
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."
297 return # It has been completely blanked: Medium is ready to be used by Bacula
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
304 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
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)
313 line = proc.fromchild.readline()
316 line = proc.fromchild.readline()
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))
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)
333 line = proc.fromchild.readline()
336 line = proc.fromchild.readline()
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))
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)
353 line = proc.fromchild.readline()
356 line = proc.fromchild.readline()
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))
365 # class disk ends here.
367 class DVDError(Exception):
368 def __init__(self, errno, value):
371 if self.value[-1] == '\n':
372 self.value = self.value[0:-1]
374 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
377 print "Wrong number of arguments."
381 dvd-handler DEVICE test
382 dvd-handler DEVICE free
383 dvd-handler DEVICE write APPEND FILE
384 dvd-handler DEVICE blank
386 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
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
404 if len(sys.argv) < 3:
407 dvd = disk(sys.argv[1])
409 if "free" == sys.argv[2]:
410 if len(sys.argv) == 3:
421 print "No Error reported."
423 print "Wrong number of arguments for free operation."
425 elif "prepare" == sys.argv[2]:
426 if len(sys.argv) == 3:
430 print "Error while preparing medium: ", str(e)
432 sys.exit(e.errno & 0x7F)
434 sys.exit(errno.EPIPE)
436 print "Medium prepared successfully."
438 print "Wrong number of arguments for prepare operation."
440 elif "test" == sys.argv[2]:
443 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
444 print "Free space: " + str(dvd.free())
446 print "Error while getting informations: ", str(e)
447 elif "write" == sys.argv[2]:
448 if len(sys.argv) == 5:
450 dvd.write(long(sys.argv[3]), sys.argv[4])
452 print "Error while writing part file: ", str(e)
454 sys.exit(e.errno & 0x7F)
456 sys.exit(errno.EPIPE)
458 print "Part file " + sys.argv[4] + " successfully written to disk."
460 print "Wrong number of arguments for write operation."
464 print "No operation - use test, free, prepare or write."
465 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"