3 # Copyright (C) 2000-2015, Kern Sibbald
4 # License: BSD 2-Clause
6 # Modified version of dvd-handler used to simulate reading/writing
7 # to a DVD but using disk storage. This is a pretty crude implementation
8 # and a lot of the old code is still here and just sort of blunders
11 # called: dvd-simulator <dvd-device-name> operation args
13 # operations used by Bacula:
16 # Scan the device and report the available space.
19 # Write a part file to disk.
20 # This operation needs two additional arguments.
21 # The first (op) indicates to
23 # 1 -- first write to a blank disk
24 # 2 -- blank or truncate a disk
26 # The second is the filename to write
28 # operations available but not used by Bacula:
30 # test Scan the device and report the information found.
31 # This operation needs no further arguments.
32 # prepare Prepare a DVD+/-RW for being used by Bacula.
33 # Note: This is only useful if you already have some
34 # non-Bacula data on a medium, and you want to use
35 # it with Bacula. Don't run this on blank media, it
39 # in case of operation ``free'' returns:
40 # Prints on the first output line the free space available in bytes.
41 # If an error occurs, prints a negative number (-errno), followed,
42 # on the second line, by an error message.
57 # Configurable values:
59 dvdrwmediainfo = "@DVDRWMEDIAINFO@"
60 growcmd = "@GROWISOFS@"
61 dvdrwformat = "@DVDRWFORMAT@"
63 margin = 10485760 # 10 mb security margin
65 # Comment the following line if you want the tray to be reloaded
67 growcmd += " -use-the-force-luke=notray"
69 # end of configurable values
71 ###############################################################################
73 # This class represents DVD disk informations.
74 # When instantiated, it needs a device name.
75 # Status information about the device and the disk loaded is collected only when
76 # asked for (for example dvd-freespace doesn't need to know the media type, and
77 # dvd-writepart doesn't not always need to know the free space).
79 # The following methods are implemented:
80 # __init__ we need that...
81 # __repr__ this seems to be a good idea to have.
82 # Quite minimalistic implementation, though.
83 # __str__ For casts to string. Return the current disk information
84 # is_empty Returns TRUE if the disk is empty, blank... this needs more
85 # work, especially concerning non-RW media and blank vs. no
86 # filesystem considerations. Here, we should also look for
87 # other filesystems - probably we don't want to silently
88 # overwrite UDF or ext2 or anything not mentioned in fstab...
89 # (NB: I don't think it is a problem)
90 # free Returns the available free space.
91 # write Writes one part file to disk, either starting a new file
92 # system on disk, or appending to it.
93 # This method should also prepare a blank disk so that a
94 # certain part of the disk is used to allow detection of a
95 # used disk by all / more disk drives.
96 # blank Blank the device
98 ###############################################################################
99 def __init__(self, devicename):
100 self.device = devicename
101 self.disktype = "none"
102 self.diskmode = "none"
103 self.diskstatus = "none"
104 self.hardwaredevice = "none"
106 self.next_session = -1
108 self.maxcapacity = 4000000000
110 self.freespace_collected = 0
111 self.mediumtype_collected = 0
113 self.growcmd += " -quiet"
115 if self.is4gbsupported():
116 self.growcmd += " -use-the-force-luke=4gms"
118 self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
119 "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
124 return "disk(" + self.device + ") # This is an instance of class disk"
127 if not self.freespace_collected:
128 self.collect_freespace();
129 if not self.mediumtype_collected:
130 self.collect_mediumtype();
132 self.me = "Class disk, initialized with device '" + self.device + "'\n"
133 self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
134 self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
135 self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
136 self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
139 ## Check if we want to allow growisofs to cross the 4gb boundary
140 def is4gbsupported(self):
141 processi = popen2.Popen4("uname -s -r")
142 status = processi.wait()
143 if not os.WIFEXITED(status):
145 if os.WEXITSTATUS(status) != 0:
147 strres = processi.fromchild.readline()[0:-1]
148 version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
149 if not version: # Non-Linux: allow
152 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)):
157 def collect_freespace(self): # Collects current free space
158 self.next_session = 0
159 self.capacity = 4000000000
160 self.freespace_collected = 1
162 cmd = "du -sb " + self.device
163 processi = popen2.Popen4(cmd)
164 status = processi.wait()
165 if not os.WIFEXITED(status):
167 if os.WEXITSTATUS(status) != 0:
169 result = processi.fromchild.read()
171 used = re.search(r"(\d+)\s", result, re.MULTILINE)
173 self.capacity = self.maxcapacity - long(used.group(1))
174 if self.capacity < 0:
179 def collect_mediumtype(self): # Collects current medium type
181 cmd = self.dvdrwmediainfo + " " + self.device
182 processi = popen2.Popen4(cmd)
183 status = processi.wait()
184 if not os.WIFEXITED(status):
185 raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
186 if os.WEXITSTATUS(status) != 0:
187 raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
189 result = processi.fromchild.read()
191 hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
192 mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
193 mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
194 status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
197 self.hardwaredevice = hardware.group(1)
200 self.disktype = mediatype.group(2)
202 raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
204 if self.disktype == "DVD-RW":
206 self.diskmode = mediamode.group(1)
208 raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
211 self.diskstatus = status.group(1)
213 raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
216 self.mediumtype_collected = 1
220 if not self.freespace_collected:
221 self.collect_freespace();
223 return 0 == self.next_session
226 if not self.mediumtype_collected:
227 self.collect_mediumtype();
228 return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
230 def is_plus_RW(self):
231 if not self.mediumtype_collected:
232 self.collect_mediumtype();
233 return "DVD+RW" == self.disktype
235 def is_minus_RW(self):
236 if not self.mediumtype_collected:
237 self.collect_mediumtype();
238 return "DVD-RW" == self.disktype
240 def is_restricted_overwrite(self):
241 if not self.mediumtype_collected:
242 self.collect_mediumtype();
243 return self.diskmode == "Restricted Overwrite"
246 if not self.mediumtype_collected:
247 self.collect_mediumtype();
249 return self.diskstatus == "blank"
252 if not self.freespace_collected:
253 self.collect_freespace();
255 fr = self.capacity-self.next_session-self.margin
261 def term_handler(self, signum, frame):
262 print 'dvd-handler: Signal term_handler called with signal', signum
264 print "dvd-handler: Sending SIGTERM to pid", self.pid
265 os.kill(self.pid, signal.SIGTERM)
267 print "dvd-handler: Sending SIGKILL to pid", self.pid
268 os.kill(self.pid, signal.SIGKILL)
271 def write(self, newvol, partfile):
273 print "Newvol", newvol
274 print "Zap everything ..."
275 os.system("rm -f "+self.device+"/*")
276 print "cp", partfile, self.device
277 shutil.copy(partfile,self.device)
281 raise DVDError(0, "I won't prepare a non-rewritable medium")
283 # Blank DVD+RW when there is no data on it
284 if self.is_plus_RW() and self.is_blank():
285 print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
287 return # It has been completely blanked: Medium is ready to be used by Bacula
289 if self.is_minus_RW() and (not self.is_restricted_overwrite()):
290 print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
291 self.reformat_minus_RW()
292 return # Reformated: Medium is ready to be used by Bacula
294 # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
295 if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
296 print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
300 cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
301 print "Running " + cmd
302 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
303 proc = popen2.Popen4(cmd)
307 line = proc.fromchild.readline()
310 line = proc.fromchild.readline()
315 signal.signal(signal.SIGTERM, oldsig)
316 if os.WEXITSTATUS(status) != 0:
317 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
320 cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
321 print "Running " + cmd
322 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
323 proc = popen2.Popen4(cmd)
327 line = proc.fromchild.readline()
330 line = proc.fromchild.readline()
335 signal.signal(signal.SIGTERM, oldsig)
336 if os.WEXITSTATUS(status) != 0:
337 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
339 def reformat_minus_RW(self):
340 cmd = self.dvdrwformat + " -force " + self.device
341 print "Running " + cmd
342 oldsig = signal.signal(signal.SIGTERM, self.term_handler)
343 proc = popen2.Popen4(cmd)
347 line = proc.fromchild.readline()
350 line = proc.fromchild.readline()
355 signal.signal(signal.SIGTERM, oldsig)
356 if os.WEXITSTATUS(status) != 0:
357 raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
359 # class disk ends here.
361 class DVDError(Exception):
362 def __init__(self, errno, value):
365 if self.value[-1] == '\n':
366 self.value = self.value[0:-1]
368 return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
371 print "Wrong number of arguments."
375 dvd-handler DEVICE test
376 dvd-handler DEVICE free
377 dvd-handler DEVICE write APPEND FILE
378 dvd-handler DEVICE blank
380 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
383 test Scan the device and report the information found.
384 This operation needs no further arguments.
385 free Scan the device and report the available space.
386 write Write a part file to disk.
387 This operation needs two additional arguments.
388 The first indicates to append (0), restart the
389 disk (1) or restart existing disk (2). The second
390 is the file to write.
391 prepare Prepare a DVD+/-RW for being used by Bacula.
392 Note: This is only useful if you already have some
393 non-Bacula data on a medium, and you want to use
394 it with Bacula. Don't run this on blank media, it
399 if len(sys.argv) < 3:
402 dvd = disk(sys.argv[1])
404 if "free" == sys.argv[2]:
405 if len(sys.argv) == 3:
416 print "No Error reported."
418 print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
419 print sys.argv[1], sys.argv[2], sys.argv[3]
421 elif "prepare" == sys.argv[2]:
422 if len(sys.argv) == 3:
426 print "Error while preparing medium: ", str(e)
428 sys.exit(e.errno & 0x7F)
430 sys.exit(errno.EPIPE)
432 print "Medium prepared successfully."
434 print "Wrong number of arguments for prepare operation."
436 elif "test" == sys.argv[2]:
439 print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
440 print "Free space: " + str(dvd.free())
442 print "Error while getting informations: ", str(e)
443 elif "write" == sys.argv[2]:
444 if len(sys.argv) == 5:
446 dvd.write(long(sys.argv[3]), sys.argv[4])
448 print "Error while writing part file: ", str(e)
450 sys.exit(e.errno & 0x7F)
452 sys.exit(errno.EPIPE)
454 print "Part file " + sys.argv[4] + " successfully written to disk."
456 print "Wrong number of arguments for write operation."
460 print "No operation - use test, free, prepare or write."
461 print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"