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 # Modified version of dvd-handler used to simulate reading/writing
21 # to a DVD but using disk storage. This is a pretty crude implementation
22 # and a lot of the old code is still here and just sort of blunders
25 # called: dvd-simulator <dvd-device-name> operation args
27 # operations used by Bacula:
30 # Scan the device and report the available space.
33 # Write a part file to disk.
34 # This operation needs two additional arguments.
35 # The first (op) indicates to
37 # 1 -- first write to a blank disk
38 # 2 -- blank or truncate a disk
40 # The second is the filename to write
42 # operations available but not used by Bacula:
44 # test Scan the device and report the information found.
45 # This operation needs no further arguments.
46 # prepare Prepare a DVD+/-RW for being used by Bacula.
47 # Note: This is only useful if you already have some
48 # non-Bacula data on a medium, and you want to use
49 # it with Bacula. Don't run this on blank media, it
53 # in case of operation ``free'' returns:
54 # Prints on the first output line the free space available in bytes.
55 # If an error occurs, prints a negative number (-errno), followed,
56 # on the second line, by an error message.
71 # Configurable values:
73 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
74 growcmd = "@GROWISOFS@"
75 dvdrwformat = "@DVDRWFORMAT@"
77 margin = 10485760 # 10 mb security margin
79 # Comment the following line if you want the tray to be reloaded
81 growcmd += " -use-the-force-luke=notray"
83 # end of configurable values
85 ###############################################################################
87 # This class represents DVD disk informations.
88 # When instantiated, it needs a device name.
89 # Status information about the device and the disk loaded is collected only when
90 # asked for (for example dvd-freespace doesn't need to know the media type, and
91 # dvd-writepart doesn't not always need to know the free space).
93 # The following methods are implemented:
94 # __init__ we need that...
95 # __repr__ this seems to be a good idea to have.
96 # Quite minimalistic implementation, though.
97 # __str__ For casts to string. Return the current disk information
98 # is_empty Returns TRUE if the disk is empty, blank... this needs more
99 # work, especially concerning non-RW media and blank vs. no
100 # filesystem considerations. Here, we should also look for
101 # other filesystems - probably we don't want to silently
102 # overwrite UDF or ext2 or anything not mentioned in fstab...
103 # (NB: I don't think it is a problem)
104 # free Returns the available free space.
105 # write Writes one part file to disk, either starting a new file
106 # system on disk, or appending to it.
107 # This method should also prepare a blank disk so that a
108 # certain part of the disk is used to allow detection of a
109 # used disk by all / more disk drives.
110 # blank Blank the device
112 ###############################################################################
113 def __init__(self, devicename):
114 self.device = devicename
115 self.disktype = "none"
116 self.diskmode = "none"
117 self.diskstatus = "none"
118 self.hardwaredevice = "none"
120 self.next_session = -1
122 self.maxcapacity = 4000000000
124 self.freespace_collected = 0
125 self.mediumtype_collected = 0
127 self.growcmd += " -quiet"
129 if self.is4gbsupported():
130 self.growcmd += " -use-the-force-luke=4gms"
132 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
133 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
138 return "disk(" + self.device + ") # This is an instance of class disk"
141 if not self.freespace_collected:
142 self.collect_freespace();
143 if not self.mediumtype_collected:
144 self.collect_mediumtype();
146 self.me = "Class disk, initialized with device '" + self.device + "'\n"
147 self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
148 self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
149 self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
150 self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
153 ## Check if we want to allow growisofs to cross the 4gb boundary
154 def is4gbsupported(self):
155 processi = popen2.Popen4("uname -s -r")
156 status = processi.wait()
157 if not os.WIFEXITED(status):
159 if os.WEXITSTATUS(status) != 0:
161 strres = processi.fromchild.readline()[0:-1]
162 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
163 if not version: # Non-Linux: allow
166 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)):
171 def collect_freespace(self): # Collects current free space
172 self.next_session = 0
173 self.capacity = 4000000000
174 self.freespace_collected = 1
176 cmd = "du -sb " + self.device
177 processi = popen2.Popen4(cmd)
178 status = processi.wait()
179 if not os.WIFEXITED(status):
181 if os.WEXITSTATUS(status) != 0:
183 result = processi.fromchild.read()
185 used = re.search(r"(\d+)\s", result, re.MULTILINE)
187 self.capacity = self.maxcapacity - long(used.group(1))
188 if self.capacity < 0:
193 def collect_mediumtype(self): # Collects current medium type
195 cmd = self.dvdrwmediainfo + " " + self.device
196 processi = popen2.Popen4(cmd)
197 status = processi.wait()
198 if not os.WIFEXITED(status):
199 raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
200 if os.WEXITSTATUS(status) != 0:
201 raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
203 result = processi.fromchild.read()
205 hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
206 mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
207 mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
208 status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
211 self.hardwaredevice = hardware.group(1)
214 self.disktype = mediatype.group(2)
216 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
218 if self.disktype == "DVD-RW":
220 self.diskmode = mediamode.group(1)
222 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
225 self.diskstatus = status.group(1)
227 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
230 self.mediumtype_collected = 1
234 if not self.freespace_collected:
235 self.collect_freespace();
237 return 0 == self.next_session
240 if not self.mediumtype_collected:
241 self.collect_mediumtype();
242 return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
244 def is_plus_RW(self):
245 if not self.mediumtype_collected:
246 self.collect_mediumtype();
247 return "DVD+RW" == self.disktype
249 def is_minus_RW(self):
250 if not self.mediumtype_collected:
251 self.collect_mediumtype();
252 return "DVD-RW" == self.disktype
254 def is_restricted_overwrite(self):
255 if not self.mediumtype_collected:
256 self.collect_mediumtype();
257 return self.diskmode == "Restricted Overwrite"
260 if not self.mediumtype_collected:
261 self.collect_mediumtype();
263 return self.diskstatus == "blank"
266 if not self.freespace_collected:
267 self.collect_freespace();
269 fr = self.capacity-self.next_session-self.margin
275 def term_handler(self, signum, frame):
276 print 'dvd-handler: Signal term_handler called with signal', signum
278 print "dvd-handler: Sending SIGTERM to pid", self.pid
279 os.kill(self.pid, signal.SIGTERM)
281 print "dvd-handler: Sending SIGKILL to pid", self.pid
282 os.kill(self.pid, signal.SIGKILL)
285 def write(self, newvol, partfile):
287 print "Newvol", newvol
288 print "Zap everything ..."
289 os.system("rm -f "+self.device+"/*")
290 print "cp", partfile, self.device
291 shutil.copy(partfile,self.device)
295 raise DVDError(0, "I won't prepare a non-rewritable medium")
297 # Blank DVD+RW when there is no data on it
298 if self.is_plus_RW() and self.is_blank():
299 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
301 return # It has been completely blanked: Medium is ready to be used by Bacula
303 if self.is_minus_RW() and (not self.is_restricted_overwrite()):
304 print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
305 self.reformat_minus_RW()
306 return # Reformated: Medium is ready to be used by Bacula
308 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
309 if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
310 print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
314 cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
315 print "Running " + cmd
316 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
317 proc = popen2.Popen4(cmd)
321 line = proc.fromchild.readline()
324 line = proc.fromchild.readline()
329 signal.signal(signal.SIGTERM, oldsig)
330 if os.WEXITSTATUS(status) != 0:
331 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
334 cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
335 print "Running " + cmd
336 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
337 proc = popen2.Popen4(cmd)
341 line = proc.fromchild.readline()
344 line = proc.fromchild.readline()
349 signal.signal(signal.SIGTERM, oldsig)
350 if os.WEXITSTATUS(status) != 0:
351 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
353 def reformat_minus_RW(self):
354 cmd = self.dvdrwformat + " -force " + self.device
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))
373 # class disk ends here.
375 class DVDError(Exception):
376 def __init__(self, errno, value):
379 if self.value[-1] == '\n':
380 self.value = self.value[0:-1]
382 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
385 print "Wrong number of arguments."
389 dvd-handler DEVICE test
390 dvd-handler DEVICE free
391 dvd-handler DEVICE write APPEND FILE
392 dvd-handler DEVICE blank
394 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
397 test Scan the device and report the information found.
398 This operation needs no further arguments.
399 free Scan the device and report the available space.
400 write Write a part file to disk.
401 This operation needs two additional arguments.
402 The first indicates to append (0), restart the
403 disk (1) or restart existing disk (2). The second
404 is the file to write.
405 prepare Prepare a DVD+/-RW for being used by Bacula.
406 Note: This is only useful if you already have some
407 non-Bacula data on a medium, and you want to use
408 it with Bacula. Don't run this on blank media, it
413 if len(sys.argv) < 3:
416 dvd = disk(sys.argv[1])
418 if "free" == sys.argv[2]:
419 if len(sys.argv) == 3:
430 print "No Error reported."
432 print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
433 print sys.argv[1], sys.argv[2], sys.argv[3]
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."
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."
474 print "No operation - use test, free, prepare or write."
475 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"