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 os.WEXITSTATUS(status) != 0:
292 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
296 raise DVDError(0, "I won't prepare a non-rewritable medium")
298 # Blank DVD+RW when there is no data on it
299 if self.is_plus_RW() and self.is_blank():
300 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
302 return # It has been completely blanked: Medium is ready to be used by Bacula
304 if self.is_minus_RW() and (not self.is_restricted_overwrite()):
305 print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
306 self.reformat_minus_RW()
307 return # Reformated: Medium is ready to be used by Bacula
309 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
310 if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
311 print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
315 cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
316 print "Running " + cmd
317 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
318 proc = popen2.Popen4(cmd)
322 line = proc.fromchild.readline()
325 line = proc.fromchild.readline()
330 signal.signal(signal.SIGTERM, oldsig)
331 if os.WEXITSTATUS(status) != 0:
332 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
335 cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
336 print "Running " + cmd
337 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
338 proc = popen2.Popen4(cmd)
342 line = proc.fromchild.readline()
345 line = proc.fromchild.readline()
350 signal.signal(signal.SIGTERM, oldsig)
351 if os.WEXITSTATUS(status) != 0:
352 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
354 def reformat_minus_RW(self):
355 cmd = self.dvdrwformat + " -force " + self.device
356 print "Running " + cmd
357 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
358 proc = popen2.Popen4(cmd)
362 line = proc.fromchild.readline()
365 line = proc.fromchild.readline()
370 signal.signal(signal.SIGTERM, oldsig)
371 if os.WEXITSTATUS(status) != 0:
372 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
374 # class disk ends here.
376 class DVDError(Exception):
377 def __init__(self, errno, value):
380 if self.value[-1] == '\n':
381 self.value = self.value[0:-1]
383 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
386 print "Wrong number of arguments."
390 dvd-handler DEVICE test
391 dvd-handler DEVICE free
392 dvd-handler DEVICE write APPEND FILE
393 dvd-handler DEVICE blank
395 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
398 test Scan the device and report the information found.
399 This operation needs no further arguments.
400 free Scan the device and report the available space.
401 write Write a part file to disk.
402 This operation needs two additional arguments.
403 The first indicates to append (0), restart the
404 disk (1) or restart existing disk (2). The second
405 is the file to write.
406 prepare Prepare a DVD+/-RW for being used by Bacula.
407 Note: This is only useful if you already have some
408 non-Bacula data on a medium, and you want to use
409 it with Bacula. Don't run this on blank media, it
414 if len(sys.argv) < 3:
417 dvd = disk(sys.argv[1])
419 if "free" == sys.argv[2]:
420 if len(sys.argv) == 3:
431 print "No Error reported."
433 print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
435 elif "prepare" == sys.argv[2]:
436 if len(sys.argv) == 3:
440 print "Error while preparing medium: ", str(e)
442 sys.exit(e.errno & 0x7F)
444 sys.exit(errno.EPIPE)
446 print "Medium prepared successfully."
448 print "Wrong number of arguments for prepare operation. Wanted 3 got", len(sys.argv)
450 elif "test" == sys.argv[2]:
453 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
454 print "Free space: " + str(dvd.free())
456 print "Error while getting informations: ", str(e)
457 elif "write" == sys.argv[2]:
458 if len(sys.argv) == 5:
460 dvd.write(long(sys.argv[3]), sys.argv[4])
462 print "Error while writing part file: ", str(e)
464 sys.exit(e.errno & 0x7F)
466 sys.exit(errno.EPIPE)
468 print "Part file " + sys.argv[4] + " successfully written to disk."
470 print "Wrong number of arguments for write operation. Wanted 5 got", len(sys.argv)
474 print "No operation - use test, free, prepare or write."
475 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"