3 # Bacula(R) - The Network Backup Solution
5 # Copyright (C) 2000-2015 Kern Sibbald
6 # Copyright (C) 2000-2014 Free Software Foundation Europe e.V.
8 # The original author of Bacula is Kern Sibbald, with contributions
9 # from many others, a complete list can be found in the file AUTHORS.
11 # You may use this file and others of this release according to the
12 # license defined in the LICENSE file, which includes the Affero General
13 # Public License, v3.0 ("AGPLv3") and some additional permissions and
14 # terms pursuant to its AGPLv3 Section 7.
16 # This notice must be preserved when any source code is
17 # conveyed and/or propagated.
19 # Bacula(R) is a registered trademark of Kern Sibbald.
21 # Modified version of dvd-handler used to simulate reading/writing
22 # to a DVD but using disk storage. This is a pretty crude implementation
23 # and a lot of the old code is still here and just sort of blunders
26 # called: dvd-simulator <dvd-device-name> operation args
28 # operations used by Bacula:
31 # Scan the device and report the available space.
34 # Write a part file to disk.
35 # This operation needs two additional arguments.
36 # The first (op) indicates to
38 # 1 -- first write to a blank disk
39 # 2 -- blank or truncate a disk
41 # The second is the filename to write
43 # operations available but not used by Bacula:
45 # test Scan the device and report the information found.
46 # This operation needs no further arguments.
47 # prepare Prepare a DVD+/-RW for being used by Bacula.
48 # Note: This is only useful if you already have some
49 # non-Bacula data on a medium, and you want to use
50 # it with Bacula. Don't run this on blank media, it
54 # in case of operation ``free'' returns:
55 # Prints on the first output line the free space available in bytes.
56 # If an error occurs, prints a negative number (-errno), followed,
57 # on the second line, by an error message.
72 # Configurable values:
74 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
75 growcmd = "@GROWISOFS@"
76 dvdrwformat = "@DVDRWFORMAT@"
78 margin = 10485760 # 10 mb security margin
80 # Comment the following line if you want the tray to be reloaded
82 growcmd += " -use-the-force-luke=notray"
84 # end of configurable values
86 ###############################################################################
88 # This class represents DVD disk informations.
89 # When instantiated, it needs a device name.
90 # Status information about the device and the disk loaded is collected only when
91 # asked for (for example dvd-freespace doesn't need to know the media type, and
92 # dvd-writepart doesn't not always need to know the free space).
94 # The following methods are implemented:
95 # __init__ we need that...
96 # __repr__ this seems to be a good idea to have.
97 # Quite minimalistic implementation, though.
98 # __str__ For casts to string. Return the current disk information
99 # is_empty Returns TRUE if the disk is empty, blank... this needs more
100 # work, especially concerning non-RW media and blank vs. no
101 # filesystem considerations. Here, we should also look for
102 # other filesystems - probably we don't want to silently
103 # overwrite UDF or ext2 or anything not mentioned in fstab...
104 # (NB: I don't think it is a problem)
105 # free Returns the available free space.
106 # write Writes one part file to disk, either starting a new file
107 # system on disk, or appending to it.
108 # This method should also prepare a blank disk so that a
109 # certain part of the disk is used to allow detection of a
110 # used disk by all / more disk drives.
111 # blank Blank the device
113 ###############################################################################
114 def __init__(self, devicename):
115 self.device = devicename
116 self.disktype = "none"
117 self.diskmode = "none"
118 self.diskstatus = "none"
119 self.hardwaredevice = "none"
121 self.next_session = -1
123 self.maxcapacity = 4000000000
125 self.freespace_collected = 0
126 self.mediumtype_collected = 0
128 self.growcmd += " -quiet"
130 if self.is4gbsupported():
131 self.growcmd += " -use-the-force-luke=4gms"
133 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
134 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
139 return "disk(" + self.device + ") # This is an instance of class disk"
142 if not self.freespace_collected:
143 self.collect_freespace();
144 if not self.mediumtype_collected:
145 self.collect_mediumtype();
147 self.me = "Class disk, initialized with device '" + self.device + "'\n"
148 self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
149 self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
150 self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
151 self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
154 ## Check if we want to allow growisofs to cross the 4gb boundary
155 def is4gbsupported(self):
156 processi = popen2.Popen4("uname -s -r")
157 status = processi.wait()
158 if not os.WIFEXITED(status):
160 if os.WEXITSTATUS(status) != 0:
162 strres = processi.fromchild.readline()[0:-1]
163 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
164 if not version: # Non-Linux: allow
167 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)):
172 def collect_freespace(self): # Collects current free space
173 self.next_session = 0
174 self.capacity = 4000000000
175 self.freespace_collected = 1
177 cmd = "du -sb " + self.device
178 processi = popen2.Popen4(cmd)
179 status = processi.wait()
180 if not os.WIFEXITED(status):
182 if os.WEXITSTATUS(status) != 0:
184 result = processi.fromchild.read()
186 used = re.search(r"(\d+)\s", result, re.MULTILINE)
188 self.capacity = self.maxcapacity - long(used.group(1))
189 if self.capacity < 0:
194 def collect_mediumtype(self): # Collects current medium type
196 cmd = self.dvdrwmediainfo + " " + self.device
197 processi = popen2.Popen4(cmd)
198 status = processi.wait()
199 if not os.WIFEXITED(status):
200 raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
201 if os.WEXITSTATUS(status) != 0:
202 raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
204 result = processi.fromchild.read()
206 hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
207 mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
208 mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
209 status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
212 self.hardwaredevice = hardware.group(1)
215 self.disktype = mediatype.group(2)
217 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
219 if self.disktype == "DVD-RW":
221 self.diskmode = mediamode.group(1)
223 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
226 self.diskstatus = status.group(1)
228 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
231 self.mediumtype_collected = 1
235 if not self.freespace_collected:
236 self.collect_freespace();
238 return 0 == self.next_session
241 if not self.mediumtype_collected:
242 self.collect_mediumtype();
243 return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
245 def is_plus_RW(self):
246 if not self.mediumtype_collected:
247 self.collect_mediumtype();
248 return "DVD+RW" == self.disktype
250 def is_minus_RW(self):
251 if not self.mediumtype_collected:
252 self.collect_mediumtype();
253 return "DVD-RW" == self.disktype
255 def is_restricted_overwrite(self):
256 if not self.mediumtype_collected:
257 self.collect_mediumtype();
258 return self.diskmode == "Restricted Overwrite"
261 if not self.mediumtype_collected:
262 self.collect_mediumtype();
264 return self.diskstatus == "blank"
267 if not self.freespace_collected:
268 self.collect_freespace();
270 fr = self.capacity-self.next_session-self.margin
276 def term_handler(self, signum, frame):
277 print 'dvd-handler: Signal term_handler called with signal', signum
279 print "dvd-handler: Sending SIGTERM to pid", self.pid
280 os.kill(self.pid, signal.SIGTERM)
282 print "dvd-handler: Sending SIGKILL to pid", self.pid
283 os.kill(self.pid, signal.SIGKILL)
286 def write(self, newvol, partfile):
288 print "Newvol", newvol
289 print "Zap everything ..."
290 os.system("rm -f "+self.device+"/*")
291 print "cp", partfile, self.device
292 shutil.copy(partfile,self.device)
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)
434 print sys.argv[1], sys.argv[2], sys.argv[3]
436 elif "prepare" == sys.argv[2]:
437 if len(sys.argv) == 3:
441 print "Error while preparing medium: ", str(e)
443 sys.exit(e.errno & 0x7F)
445 sys.exit(errno.EPIPE)
447 print "Medium prepared successfully."
449 print "Wrong number of arguments for prepare operation."
451 elif "test" == sys.argv[2]:
454 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
455 print "Free space: " + str(dvd.free())
457 print "Error while getting informations: ", str(e)
458 elif "write" == sys.argv[2]:
459 if len(sys.argv) == 5:
461 dvd.write(long(sys.argv[3]), sys.argv[4])
463 print "Error while writing part file: ", str(e)
465 sys.exit(e.errno & 0x7F)
467 sys.exit(errno.EPIPE)
469 print "Part file " + sys.argv[4] + " successfully written to disk."
471 print "Wrong number of arguments for write operation."
475 print "No operation - use test, free, prepare or write."
476 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"