3 # Bacula(R) - The Network Backup Solution
5 # Copyright (C) 2000-2016 Kern Sibbald
7 # The original author of Bacula is Kern Sibbald, with contributions
8 # from many others, a complete list can be found in the file AUTHORS.
10 # You may use this file and others of this release according to the
11 # license defined in the LICENSE file, which includes the Affero General
12 # Public License, v3.0 ("AGPLv3") and some additional permissions and
13 # terms pursuant to its AGPLv3 Section 7.
15 # This notice must be preserved when any source code is
16 # conveyed and/or propagated.
18 # Bacula(R) is a registered trademark of Kern Sibbald.
20 # Check the free space available on a writable DVD
21 # Should always exit with 0 status, otherwise it indicates a serious error.
22 # (wrong number of arguments, Python exception...)
24 # called: dvd-handler <dvd-device-name> operation args
26 # operations used by Bacula:
29 # Scan the device and report the available space. It returns:
30 # Prints on the first output line the free space available in bytes.
31 # If an error occurs, prints a negative number (-errno), followed,
32 # on the second line, by an error message.
35 # Write a part file to disk.
36 # This operation needs two additional arguments.
37 # The first (op) indicates to
39 # 1 -- first write to a blank disk
40 # 2 -- blank or truncate a disk
42 # The second is the filename to write
44 # operations available but not used by Bacula:
46 # test Scan the device and report the information found.
47 # This operation needs no further arguments.
48 # prepare Prepare a DVD+/-RW for being used by Bacula.
49 # Note: This is only useful if you already have some
50 # non-Bacula data on a medium, and you want to use
51 # it with Bacula. Don't run this on blank media, it
69 # Configurable values:
71 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
72 growcmd = "@GROWISOFS@"
73 dvdrwformat = "@DVDRWFORMAT@"
75 margin = 10485760 # 10 mb security margin
77 # Comment the following line if you want the tray to be reloaded
79 growcmd += " -use-the-force-luke=notray"
81 # end of configurable values
83 ###############################################################################
85 # This class represents DVD disk informations.
86 # When instantiated, it needs a device name.
87 # Status information about the device and the disk loaded is collected only when
88 # asked for (for example dvd-freespace doesn't need to know the media type, and
89 # dvd-writepart doesn't not always need to know the free space).
91 # The following methods are implemented:
92 # __init__ we need that...
93 # __repr__ this seems to be a good idea to have.
94 # Quite minimalistic implementation, though.
95 # __str__ For casts to string. Return the current disk information
96 # is_empty Returns TRUE if the disk is empty, blank... this needs more
97 # work, especially concerning non-RW media and blank vs. no
98 # filesystem considerations. Here, we should also look for
99 # other filesystems - probably we don't want to silently
100 # overwrite UDF or ext2 or anything not mentioned in fstab...
101 # (NB: I don't think it is a problem)
102 # free Returns the available free space.
103 # write Writes one part file to disk, either starting a new file
104 # system on disk, or appending to it.
105 # This method should also prepare a blank disk so that a
106 # certain part of the disk is used to allow detection of a
107 # used disk by all / more disk drives.
108 # prepare Blank the device
110 ###############################################################################
111 def __init__(self, devicename):
112 self.device = devicename
113 self.disktype = "none"
114 self.diskmode = "none"
115 self.diskstatus = "none"
116 self.hardwaredevice = "none"
118 self.next_session = -1
121 self.freespace_collected = 0
122 self.mediumtype_collected = 0
124 self.growcmd += " -quiet"
126 if self.is4gbsupported():
127 self.growcmd += " -use-the-force-luke=4gms"
129 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
130 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
135 return "disk(" + self.device + ") # This is an instance of class disk"
138 if not self.freespace_collected:
139 self.collect_freespace();
140 if not self.mediumtype_collected:
141 self.collect_mediumtype();
143 self.me = "Class disk, initialized with device '" + self.device + "'\n"
144 self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
145 self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
146 self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
147 self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
150 ## Check if we want to allow growisofs to cross the 4gb boundary
151 def is4gbsupported(self):
152 processi = popen2.Popen4("uname -s -r")
153 status = processi.wait()
154 if not os.WIFEXITED(status):
156 if os.WEXITSTATUS(status) != 0:
158 strres = processi.fromchild.readline()[0:-1]
159 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
160 if not version: # Non-Linux: allow
163 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)):
168 def collect_freespace(self): # Collects current free space
169 self.cmd = self.growcmd + " -F " + self.device
170 processi = popen2.Popen4(self.cmd)
171 status = processi.wait()
172 if not os.WIFEXITED(status):
173 raise DVDError(0, "growisofs process did not exit correctly.")
174 result = processi.fromchild.read()
175 if os.WEXITSTATUS(status) != 0:
176 if (os.WEXITSTATUS(status) & 0x7F) == errno.ENOSPC:
177 # Kludge to force dvd-handler to return a free space of 0
178 self.next_session = 1
180 self.freespace_collected = 1
183 raise DVDError(os.WEXITSTATUS(status), "growisofs returned with an error " + result + ". Please check your are using a patched version of dvd+rw-tools.")
184 next_sess = re.search(r"\snext_session=(\d+)\s", result, re.MULTILINE)
185 capa = re.search(r"\scapacity=(\d+)\s", result, re.MULTILINE)
187 if next_sess and capa:
188 self.next_session = long(next_sess.group(1))
189 self.capacity = long(capa.group(1))
191 # testing cheat (emulate 4GB boundary at 100MB)
192 #if self.next_session > 100000000:
193 # self.capacity = self.next_session
195 raise DVDError(0, "Cannot get next_session and capacity from growisofs.\nReturned: " + result)
197 self.freespace_collected = 1
200 def collect_mediumtype(self): # Collects current medium type
202 cmd = self.dvdrwmediainfo + " " + self.device
203 processi = popen2.Popen4(cmd)
204 status = processi.wait()
205 if not os.WIFEXITED(status):
206 raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
207 if os.WEXITSTATUS(status) != 0:
208 raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
210 result = processi.fromchild.read()
212 hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
213 mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
214 mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
215 status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
218 self.hardwaredevice = hardware.group(1)
221 self.disktype = mediatype.group(2)
223 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
225 if self.disktype == "DVD-RW":
227 self.diskmode = mediamode.group(1)
229 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
232 self.diskstatus = status.group(1)
234 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
237 self.mediumtype_collected = 1
241 if not self.freespace_collected:
242 self.collect_freespace();
244 return 0 == self.next_session
247 if not self.mediumtype_collected:
248 self.collect_mediumtype();
249 return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
251 def is_plus_RW(self):
252 if not self.mediumtype_collected:
253 self.collect_mediumtype();
254 return "DVD+RW" == self.disktype
256 def is_minus_RW(self):
257 if not self.mediumtype_collected:
258 self.collect_mediumtype();
259 return "DVD-RW" == self.disktype
261 def is_restricted_overwrite(self):
262 if not self.mediumtype_collected:
263 self.collect_mediumtype();
264 return self.diskmode == "Restricted Overwrite"
267 if not self.mediumtype_collected:
268 self.collect_mediumtype();
270 return self.diskstatus == "blank"
273 if not self.freespace_collected:
274 self.collect_freespace();
276 fr = self.capacity-self.next_session-self.margin
282 def term_handler(self, signum, frame):
283 print 'dvd-handler: Signal term_handler called with signal', signum
285 print "dvd-handler: Sending SIGTERM to pid", self.pid
286 os.kill(self.pid, signal.SIGTERM)
288 print "dvd-handler: Sending SIGKILL to pid", self.pid
289 os.kill(self.pid, signal.SIGKILL)
292 def write(self, newvol, partfile):
293 # Blank DVD+RW when there is no data on it
294 if newvol and self.is_plus_RW() and self.is_blank():
295 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
297 print "Done, now writing the part file."
299 if newvol and 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 print "Done, now writing the part file."
304 cmd = self.growcmd + self.growparams
306 # Ignore any existing iso9660 filesystem - used for truncate
308 cmd += " -use-the-force-luke=tty"
312 cmd += self.device + " " + str(partfile)
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 not os.WIFEXITED(status):
329 raise DVDError(0, cmd + " process did not exit correctly, signal/status " + str(status))
330 if os.WEXITSTATUS(status) != 0:
331 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
335 raise DVDError(0, "I won't prepare a non-rewritable medium")
337 # Blank DVD+RW when there is no data on it
338 if self.is_plus_RW() and self.is_blank():
339 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
341 return # It has been completely blanked: Medium is ready to be used by Bacula
343 if self.is_minus_RW() and (not self.is_restricted_overwrite()):
344 print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
345 self.reformat_minus_RW()
346 return # Reformated: Medium is ready to be used by Bacula
348 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
349 if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
350 print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
354 cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
355 print "Running " + cmd
356 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
357 proc = popen2.Popen4(cmd)
361 line = proc.fromchild.readline()
364 line = proc.fromchild.readline()
369 signal.signal(signal.SIGTERM, oldsig)
370 if os.WEXITSTATUS(status) != 0:
371 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
374 cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
375 print "Running " + cmd
376 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
377 proc = popen2.Popen4(cmd)
381 line = proc.fromchild.readline()
384 line = proc.fromchild.readline()
389 signal.signal(signal.SIGTERM, oldsig)
390 if os.WEXITSTATUS(status) != 0:
391 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
393 def reformat_minus_RW(self):
394 cmd = self.dvdrwformat + " -force " + self.device
395 print "Running " + cmd
396 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
397 proc = popen2.Popen4(cmd)
401 line = proc.fromchild.readline()
404 line = proc.fromchild.readline()
409 signal.signal(signal.SIGTERM, oldsig)
410 if os.WEXITSTATUS(status) != 0:
411 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
413 # class disk ends here.
415 class DVDError(Exception):
416 def __init__(self, errno, value):
419 if self.value[-1] == '\n':
420 self.value = self.value[0:-1]
422 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
425 print "Wrong number of arguments."
429 dvd-handler DEVICE test
430 dvd-handler DEVICE free
431 dvd-handler DEVICE write APPEND FILE
432 dvd-handler DEVICE prepare
434 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
437 test Scan the device and report the information found.
438 This operation needs no further arguments.
439 free Scan the device and report the available space.
440 write Write a part file to disk.
441 This operation needs two additional arguments.
442 The first indicates to append (0), restart the
443 disk (1) or restart existing disk (2). The second
444 is the file to write.
445 prepare Prepare a DVD+/-RW for being used by Bacula.
446 Note: This is only useful if you already have some
447 non-Bacula data on a medium, and you want to use
448 it with Bacula. Don't run this on blank media, it
453 if len(sys.argv) < 3:
456 dvd = disk(sys.argv[1])
458 if "free" == sys.argv[2]:
459 if len(sys.argv) == 3:
470 print "No Error reported."
472 print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
474 elif "prepare" == sys.argv[2]:
475 if len(sys.argv) == 3:
479 print "Error while preparing medium: ", str(e)
481 sys.exit(e.errno & 0x7F)
483 sys.exit(errno.EPIPE)
485 print "Medium prepared successfully."
487 print "Wrong number of arguments for prepare operation. Wanted 3 got", len(sys.argv)
489 elif "test" == sys.argv[2]:
492 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
493 print "Free space: " + str(dvd.free())
495 print "Error while getting informations: ", str(e)
496 elif "write" == sys.argv[2]:
497 if len(sys.argv) == 5:
499 dvd.write(long(sys.argv[3]), sys.argv[4])
501 print "Error while writing part file: ", str(e)
503 sys.exit(e.errno & 0x7F)
505 sys.exit(errno.EPIPE)
507 print "Part file " + sys.argv[4] + " successfully written to disk."
509 print "Wrong number of arguments for write operation. Wanted 5 got", len(sys.argv)
513 print "No operation - use test, free, prepare or write."
514 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"