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
270 # Ignore any existing iso9660 filesystem - used for truncate
272 cmd += "-use-the-force-luke=tty "
275 cmd += self.device + " " + str(partfile)
276 print "Running " + cmd
277 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
278 proc = popen2.Popen4(cmd)
282 line = proc.fromchild.readline()
285 line = proc.fromchild.readline()
290 signal.signal(signal.SIGTERM, oldsig)
291 if not os.WIFEXITED(status):
292 raise DVDError(0, cmd + " process did not exit correctly, signal/status " + str(status))
293 if os.WEXITSTATUS(status) != 0:
294 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
298 raise DVDError(0, "I won't prepare a non-rewritable medium")
300 # Blank DVD+RW when there is no data on it
301 if self.is_plus_RW() and self.is_blank():
302 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
304 return # It has been completely blanked: Medium is ready to be used by Bacula
306 if self.is_minus_RW() and (not self.is_restricted_overwrite()):
307 print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
308 self.reformat_minus_RW()
309 return # Reformated: Medium is ready to be used by Bacula
311 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
312 if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
313 print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
317 cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
318 print "Running " + cmd
319 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
320 proc = popen2.Popen4(cmd)
324 line = proc.fromchild.readline()
327 line = proc.fromchild.readline()
332 signal.signal(signal.SIGTERM, oldsig)
333 if os.WEXITSTATUS(status) != 0:
334 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
337 cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
338 print "Running " + cmd
339 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
340 proc = popen2.Popen4(cmd)
344 line = proc.fromchild.readline()
347 line = proc.fromchild.readline()
352 signal.signal(signal.SIGTERM, oldsig)
353 if os.WEXITSTATUS(status) != 0:
354 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
356 def reformat_minus_RW(self):
357 cmd = self.dvdrwformat + " -force " + self.device
358 print "Running " + cmd
359 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
360 proc = popen2.Popen4(cmd)
364 line = proc.fromchild.readline()
367 line = proc.fromchild.readline()
372 signal.signal(signal.SIGTERM, oldsig)
373 if os.WEXITSTATUS(status) != 0:
374 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
376 # class disk ends here.
378 class DVDError(Exception):
379 def __init__(self, errno, value):
382 if self.value[-1] == '\n':
383 self.value = self.value[0:-1]
385 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
388 print "Wrong number of arguments."
392 dvd-handler DEVICE test
393 dvd-handler DEVICE free
394 dvd-handler DEVICE write APPEND FILE
395 dvd-handler DEVICE blank
397 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
400 test Scan the device and report the information found.
401 This operation needs no further arguments.
402 free Scan the device and report the available space.
403 write Write a part file to disk.
404 This operation needs two additional arguments.
405 The first indicates to append (0), restart the
406 disk (1) or restart existing disk (2). The second
407 is the file to write.
408 prepare Prepare a DVD+/-RW for being used by Bacula.
409 Note: This is only useful if you already have some
410 non-Bacula data on a medium, and you want to use
411 it with Bacula. Don't run this on blank media, it
416 if len(sys.argv) < 3:
419 dvd = disk(sys.argv[1])
421 if "free" == sys.argv[2]:
422 if len(sys.argv) == 3:
433 print "No Error reported."
435 print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
437 elif "prepare" == sys.argv[2]:
438 if len(sys.argv) == 3:
442 print "Error while preparing medium: ", str(e)
444 sys.exit(e.errno & 0x7F)
446 sys.exit(errno.EPIPE)
448 print "Medium prepared successfully."
450 print "Wrong number of arguments for prepare operation. Wanted 3 got", len(sys.argv)
452 elif "test" == sys.argv[2]:
455 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
456 print "Free space: " + str(dvd.free())
458 print "Error while getting informations: ", str(e)
459 elif "write" == sys.argv[2]:
460 if len(sys.argv) == 5:
462 dvd.write(long(sys.argv[3]), sys.argv[4])
464 print "Error while writing part file: ", str(e)
466 sys.exit(e.errno & 0x7F)
468 sys.exit(errno.EPIPE)
470 print "Part file " + sys.argv[4] + " successfully written to disk."
472 print "Wrong number of arguments for write operation. Wanted 5 got", len(sys.argv)
476 print "No operation - use test, free, prepare or write."
477 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"