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.
32 # Configurable values:
34 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
35 growcmd = "@GROWISOFS@"
36 dvdrwformat = "@DVDRWFORMAT@"
38 margin = 10485760 # 10 mb security margin
40 # Comment the following line if you want the tray to be reloaded
42 growcmd += " -use-the-force-luke=notray"
44 # end of configurable values
46 ###############################################################################
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).
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
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"
81 self.next_session = -1
84 self.freespace_collected = 0
85 self.mediumtype_collected = 0
87 self.growcmd += " -quiet"
89 if self.is4gbsupported():
90 self.growcmd += " -use-the-force-luke=4gms"
92 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
93 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
98 return "disk(" + self.device + ") # This is an instance of class disk"
101 if not self.freespace_collected:
102 self.collect_freespace();
103 if not self.mediumtype_collected:
104 self.collect_mediumtype();
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"
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):
119 if os.WEXITSTATUS(status) != 0:
121 strres = processi.fromchild.readline()[0:-1]
122 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
123 if not version: # Non-Linux: allow
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)):
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
143 self.freespace_collected = 1
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)
150 if next_sess and capa:
151 self.next_session = long(next_sess.group(1))
152 self.capacity = long(capa.group(1))
154 # testing cheat (emulate 4GB boundary at 100MB)
155 #if self.next_session > 100000000:
156 # self.capacity = self.next_session
158 raise DVDError(0, "Cannot get next_session and capacity from growisofs.\nReturned: " + result)
160 self.freespace_collected = 1
163 def collect_mediumtype(self): # Collects current medium type
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)
173 result = processi.fromchild.read()
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)
181 self.hardwaredevice = hardware.group(1)
184 self.disktype = mediatype.group(2)
186 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
188 if self.disktype == "DVD-RW":
190 self.diskmode = mediamode.group(1)
192 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
195 self.diskstatus = status.group(1)
197 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
200 self.mediumtype_collected = 1
204 if not self.freespace_collected:
205 self.collect_freespace();
207 return 0 == self.next_session
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
214 def is_plus_RW(self):
215 if not self.mediumtype_collected:
216 self.collect_mediumtype();
217 return "DVD+RW" == self.disktype
219 def is_minus_RW(self):
220 if not self.mediumtype_collected:
221 self.collect_mediumtype();
222 return "DVD-RW" == self.disktype
224 def is_restricted_overwrite(self):
225 if not self.mediumtype_collected:
226 self.collect_mediumtype();
227 return self.diskmode == "Restricted Overwrite"
230 if not self.mediumtype_collected:
231 self.collect_mediumtype();
233 return self.diskstatus == "blank"
236 if not self.freespace_collected:
237 self.collect_freespace();
239 fr = self.capacity-self.next_session-self.margin
245 def term_handler(self, signum, frame):
246 print 'dvd-handler: Signal term_handler called with signal', signum
248 print "dvd-handler: Sending SIGTERM to pid", self.pid
249 os.kill(self.pid, signal.SIGTERM)
251 print "dvd-handler: Sending SIGKILL to pid", self.pid
252 os.kill(self.pid, signal.SIGKILL)
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."
260 print "Done, now writing the part file."
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."
267 cmd = self.growcmd + self.growparams
272 cmd += self.device + " " + str(partfile)
273 print "Running " + cmd
274 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
275 proc = popen2.Popen4(cmd)
279 line = proc.fromchild.readline()
282 line = proc.fromchild.readline()
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))
293 raise DVDError(0, "I won't prepare a non-rewritable medium")
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."
299 return # It has been completely blanked: Medium is ready to be used by Bacula
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
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."
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)
319 line = proc.fromchild.readline()
322 line = proc.fromchild.readline()
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))
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)
339 line = proc.fromchild.readline()
342 line = proc.fromchild.readline()
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))
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)
359 line = proc.fromchild.readline()
362 line = proc.fromchild.readline()
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))
371 # class disk ends here.
373 class DVDError(Exception):
374 def __init__(self, errno, value):
377 if self.value[-1] == '\n':
378 self.value = self.value[0:-1]
380 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
383 print "Wrong number of arguments."
387 dvd-handler DEVICE test
388 dvd-handler DEVICE free
389 dvd-handler DEVICE write APPEND FILE
390 dvd-handler DEVICE blank
392 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
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
410 if len(sys.argv) < 3:
413 dvd = disk(sys.argv[1])
415 if "free" == sys.argv[2]:
416 if len(sys.argv) == 3:
427 print "No Error reported."
429 print "Wrong number of arguments for free operation."
431 elif "prepare" == sys.argv[2]:
432 if len(sys.argv) == 3:
436 print "Error while preparing medium: ", str(e)
438 sys.exit(e.errno & 0x7F)
440 sys.exit(errno.EPIPE)
442 print "Medium prepared successfully."
444 print "Wrong number of arguments for prepare operation."
446 elif "test" == sys.argv[2]:
449 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
450 print "Free space: " + str(dvd.free())
452 print "Error while getting informations: ", str(e)
453 elif "write" == sys.argv[2]:
454 if len(sys.argv) == 5:
456 dvd.write(long(sys.argv[3]), sys.argv[4])
458 print "Error while writing part file: ", str(e)
460 sys.exit(e.errno & 0x7F)
462 sys.exit(errno.EPIPE)
464 print "Part file " + sys.argv[4] + " successfully written to disk."
466 print "Wrong number of arguments for write operation."
470 print "No operation - use test, free, prepare or write."
471 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"