]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-simulator.in
ea0e5c8db9abde7e8cea9e51cf7fa3464d5f0101
[bacula/bacula] / bacula / scripts / dvd-simulator.in
1 #!@PYTHON@
2 #
3 #  Modified version of dvd-handler used to simulate reading/writing
4 #    to a DVD but using disk storage. This is a pretty crude implementation
5 #    and a lot of the old code is still here and just sort of blunders
6 #    along.
7 #
8 #  called:  dvd-simulator <dvd-device-name> operation args
9 #
10 #  operations used by Bacula:
11 #
12 #   free  (no arguments)
13 #             Scan the device and report the available space.
14 #
15 #   write  op filename
16 #              Write a part file to disk.
17 #              This operation needs two additional arguments.
18 #              The first (op) indicates to
19 #                  0 -- append
20 #                  1 -- first write to a blank disk
21 #                  2 -- blank or truncate a disk
22 #
23 #               The second is the filename to write
24 #
25 #   operations available but not used by Bacula:
26 #
27 #   test      Scan the device and report the information found.
28 #              This operation needs no further arguments.
29 #   prepare   Prepare a DVD+/-RW for being used by Bacula.
30 #              Note: This is only useful if you already have some
31 #              non-Bacula data on a medium, and you want to use
32 #              it with Bacula. Don't run this on blank media, it
33 #              is useless.
34 #
35 #
36 # in case of operation ``free'' returns:
37 # Prints on the first output line the free space available in bytes.
38 # If an error occurs, prints a negative number (-errno), followed,
39 # on the second line, by an error message.
40
41 # $Id$
42 #
43
44 import popen2
45 import os
46 import os.path
47 import shutil
48 import errno
49 import sys
50 import re
51 import signal
52 import time
53 import array
54
55 class disk:
56 # Configurable values:
57    
58    dvdrwmediainfo = "@DVDRWMEDIAINFO@"
59    growcmd = "@GROWISOFS@"
60    dvdrwformat = "@DVDRWFORMAT@"
61    dd = "@DD@"
62    margin = 10485760 # 10 mb security margin
63
64    # Comment the following line if you want the tray to be reloaded
65    # when writing ends.
66    growcmd += " -use-the-force-luke=notray"
67
68 # end of configurable values
69
70 ###############################################################################
71 #
72 # This class represents DVD disk informations.
73 # When instantiated, it needs a device name.
74 # Status information about the device and the disk loaded is collected only when
75 # asked for (for example dvd-freespace doesn't need to know the media type, and
76 # dvd-writepart doesn't not always need to know the free space).
77 #
78 # The following methods are implemented:
79 # __init__       we need that...
80 # __repr__       this seems to be a good idea to have.
81 #                Quite minimalistic implementation, though.
82 # __str__        For casts to string. Return the current disk information
83 # is_empty       Returns TRUE if the disk is empty, blank... this needs more
84 #                work, especially concerning non-RW media and blank vs. no
85 #                filesystem considerations. Here, we should also look for
86 #                other filesystems - probably we don't want to silently
87 #                overwrite UDF or ext2 or anything not mentioned in fstab...
88 #                (NB: I don't think it is a problem)
89 # free           Returns the available free space.
90 # write          Writes one part file to disk, either starting a new file
91 #                system on disk, or appending to it.
92 #                This method should also prepare a blank disk so that a
93 #                certain part of the disk is used to allow detection of a
94 #                used disk by all / more disk drives.
95 # blank          Blank the device
96 #
97 ###############################################################################
98    def __init__(self, devicename):
99       self.device = devicename
100       self.disktype = "none"
101       self.diskmode = "none"
102       self.diskstatus = "none"
103       self.hardwaredevice = "none"
104       self.pid = 0
105       self.next_session = -1
106       self.capacity = -1
107       self.maxcapacity = 4000000000
108
109       self.freespace_collected = 0
110       self.mediumtype_collected = 0
111
112       self.growcmd += " -quiet"
113
114       if self.is4gbsupported():
115          self.growcmd += " -use-the-force-luke=4gms"
116
117       self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
118                         "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
119
120       return
121
122    def __repr__(self):
123       return "disk(" + self.device + ") # This is an instance of class disk"
124
125    def __str__(self):
126       if not self.freespace_collected:
127          self.collect_freespace();
128       if not self.mediumtype_collected:
129          self.collect_mediumtype();
130       
131       self.me  = "Class disk, initialized with device '" + self.device + "'\n"
132       self.me += "type = '" + self.disktype + "' mode='" + self.diskmode + "' status = '" + self.diskstatus + "'\n"
133       self.me += " next_session = " + str(self.next_session) + " capacity = " + str(self.capacity) + "\n"
134       self.me += "Hardware device is '" + self.hardwaredevice + "'\n"
135       self.me += "growcmd = '" + self.growcmd + "'\ngrowparams = '" + self.growparams + "'\n"
136       return self.me
137
138    ## Check if we want to allow growisofs to cross the 4gb boundary
139    def is4gbsupported(self):
140       processi = popen2.Popen4("uname -s -r")
141       status = processi.wait()
142       if not os.WIFEXITED(status):
143          return 1
144       if os.WEXITSTATUS(status) != 0:
145          return 1
146       strres = processi.fromchild.readline()[0:-1]
147       version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
148       if not version: # Non-Linux: allow
149          return 1
150       
151       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)):
152          return 1
153       else:
154          return 0
155
156    def collect_freespace(self): # Collects current free space
157       self.next_session = 0
158       self.capacity = 4000000000
159       self.freespace_collected = 1
160
161       cmd = "du -sb " + self.device
162       processi = popen2.Popen4(cmd)        
163       status = processi.wait()
164       if not os.WIFEXITED(status):
165          return 1;
166       if os.WEXITSTATUS(status) != 0:
167          return 1;
168       result = processi.fromchild.read()
169       
170       used = re.search(r"(\d+)\s", result, re.MULTILINE)
171
172       self.capacity = self.maxcapacity - long(used.group(1))
173       if self.capacity < 0:
174          self.capacity = 0
175
176       return 0
177    
178    def collect_mediumtype(self): # Collects current medium type
179       self.lasterror = ""
180       cmd = self.dvdrwmediainfo + " " + self.device
181       processi = popen2.Popen4(cmd)
182       status = processi.wait()
183       if not os.WIFEXITED(status):
184          raise DVDError(0, self.dvdrwmediainfo + " process did not exit correctly.")
185       if os.WEXITSTATUS(status) != 0:
186          raise DVDError(0, "Cannot get media info from " + self.dvdrwmediainfo)
187          return
188       result = processi.fromchild.read()
189       
190       hardware = re.search(r"INQUIRY:\s+(.*)\n", result, re.MULTILINE)
191       mediatype = re.search(r"\sMounted Media:\s+([0-9A-F]{2})h, (\S*)\s", result, re.MULTILINE)
192       mediamode = re.search(r"\sMounted Media:\s+[0-9A-F]{2}h, \S* (.*)\n", result, re.MULTILINE)
193       status = re.search(r"\sDisc status:\s+(.*)\n", result, re.MULTILINE)
194       
195       if hardware:
196          self.hardwaredevice = hardware.group(1)
197       
198       if mediatype:
199          self.disktype = mediatype.group(2)
200       else:
201          raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
202       
203       if self.disktype == "DVD-RW":
204          if mediamode:
205             self.diskmode = mediamode.group(1)
206          else:
207             raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
208       
209       if status:
210          self.diskstatus = status.group(1)
211       else:
212          raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
213
214       
215       self.mediumtype_collected = 1
216       return
217
218    def is_empty(self):
219       if not self.freespace_collected:
220          self.collect_freespace();
221       
222       return 0 == self.next_session
223
224    def is_RW(self):
225       if not self.mediumtype_collected:
226          self.collect_mediumtype();
227       return "DVD-RW" == self.disktype or "DVD+RW" == self.disktype or "DVD-RAM" == self.disktype
228
229    def is_plus_RW(self):
230       if not self.mediumtype_collected:
231          self.collect_mediumtype();
232       return "DVD+RW" == self.disktype
233
234    def is_minus_RW(self):
235       if not self.mediumtype_collected:
236          self.collect_mediumtype();
237       return "DVD-RW" == self.disktype
238       
239    def is_restricted_overwrite(self):
240       if not self.mediumtype_collected:
241          self.collect_mediumtype();
242       return self.diskmode == "Restricted Overwrite"
243
244    def is_blank(self):
245       if not self.mediumtype_collected:
246          self.collect_mediumtype();
247       
248       return self.diskstatus == "blank"
249
250    def free(self):
251       if not self.freespace_collected:
252          self.collect_freespace();
253       
254       fr = self.capacity-self.next_session-self.margin
255       if fr < 0:
256          return 0
257       else:
258          return fr
259
260    def term_handler(self, signum, frame):
261       print 'dvd-handler: Signal term_handler called with signal', signum
262       if self.pid != 0:
263          print "dvd-handler: Sending SIGTERM to pid", self.pid
264          os.kill(self.pid, signal.SIGTERM)
265          time.sleep(10)
266          print "dvd-handler: Sending SIGKILL to pid", self.pid
267          os.kill(self.pid, signal.SIGKILL)
268          sys.exit(1)
269
270    def write(self, newvol, partfile):
271       if newvol:
272          print "Newvol", newvol
273          print "Zap everything ..."
274          os.system("rm -f "+self.device+"/*")
275       print "cp", partfile, self.device
276       shutil.copy(partfile,self.device)
277
278    def prepare(self):
279       if not self.is_RW():
280          raise DVDError(0, "I won't prepare a non-rewritable medium")
281       
282       # Blank DVD+RW when there is no data on it
283       if self.is_plus_RW() and self.is_blank():
284          print "DVD+RW looks brand-new, blank it to fix some DVD-writers bugs."
285          self.blank()
286          return # It has been completely blanked: Medium is ready to be used by Bacula
287       
288       if self.is_minus_RW() and (not self.is_restricted_overwrite()):
289          print "DVD-RW is in " + self.diskmode + " mode, reformating it to Restricted Overwrite"
290          self.reformat_minus_RW()
291          return # Reformated: Medium is ready to be used by Bacula
292       
293       # TODO: Check if /dev/fd/0 and /dev/zero exists, otherwise, run self.blank()
294       if not os.path.exists("/dev/fd/0") or not os.path.exists("/dev/zero"):
295          print "/dev/fd/0 or /dev/zero doesn't exist, blank the medium completely."
296          self.blank()
297          return
298       
299       cmd = self.dd + " if=/dev/zero bs=1024 count=512 | " + self.growcmd + " -Z " + self.device + "=/dev/fd/0"
300       print "Running " + cmd
301       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
302       proc = popen2.Popen4(cmd)
303       self.pid = proc.pid
304       status = proc.poll() 
305       while status == -1:
306          line = proc.fromchild.readline()
307          while len(line) > 0:
308             print line,
309             line = proc.fromchild.readline()
310          time.sleep(1)
311          status = proc.poll()
312       self.pid = 0
313       print
314       signal.signal(signal.SIGTERM, oldsig)
315       if os.WEXITSTATUS(status) != 0:
316          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
317
318    def blank(self):
319       cmd = self.growcmd + " -Z " + self.device + "=/dev/zero"
320       print "Running " + cmd
321       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
322       proc = popen2.Popen4(cmd)
323       self.pid = proc.pid
324       status = proc.poll()
325       while status == -1:
326          line = proc.fromchild.readline()
327          while len(line) > 0:
328             print line,
329             line = proc.fromchild.readline()
330          time.sleep(1)
331          status = proc.poll()
332       self.pid = 0
333       print
334       signal.signal(signal.SIGTERM, oldsig)
335       if os.WEXITSTATUS(status) != 0:
336          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
337
338    def reformat_minus_RW(self):
339       cmd = self.dvdrwformat + " -force " + self.device
340       print "Running " + cmd
341       oldsig = signal.signal(signal.SIGTERM, self.term_handler)
342       proc = popen2.Popen4(cmd)
343       self.pid = proc.pid
344       status = proc.poll()
345       while status == -1:
346          line = proc.fromchild.readline()
347          while len(line) > 0:
348             print line,
349             line = proc.fromchild.readline()
350          time.sleep(1)
351          status = proc.poll()
352       self.pid = 0
353       print
354       signal.signal(signal.SIGTERM, oldsig)
355       if os.WEXITSTATUS(status) != 0:
356          raise DVDError(os.WEXITSTATUS(status), cmd + " exited with status " + str(os.WEXITSTATUS(status)) + ", signal/status " + str(status))
357
358 # class disk ends here.
359
360 class DVDError(Exception):
361    def __init__(self, errno, value):
362       self.errno = errno
363       self.value = value
364       if self.value[-1] == '\n':
365          self.value = self.value[0:-1]
366    def __str__(self):
367       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
368
369 def usage():
370    print "Wrong number of arguments."
371    print """
372 Usage:
373
374 dvd-handler DEVICE test
375 dvd-handler DEVICE free
376 dvd-handler DEVICE write APPEND FILE
377 dvd-handler DEVICE blank
378
379 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
380
381 Operations:
382 test      Scan the device and report the information found.
383            This operation needs no further arguments.
384 free      Scan the device and report the available space.
385 write     Write a part file to disk.
386            This operation needs two additional arguments.
387            The first indicates to append (0), restart the
388            disk (1) or restart existing disk (2). The second
389            is the file to write.
390 prepare   Prepare a DVD+/-RW for being used by Bacula.
391            Note: This is only useful if you already have some
392            non-Bacula data on a medium, and you want to use
393            it with Bacula. Don't run this on blank media, it
394            is useless.
395 """
396    sys.exit(1)
397
398 if len(sys.argv) < 3:
399    usage()
400
401 dvd = disk(sys.argv[1])
402
403 if "free" == sys.argv[2]:
404    if len(sys.argv) == 3:
405       try:
406          free = dvd.free()
407       except DVDError, e:
408          if e.errno != 0:
409             print -e.errno
410          else:
411             print errno.EPIPE
412          print str(e)
413       else:
414          print free
415          print "No Error reported."
416    else:
417       print "Wrong number of arguments for free operation. Wanted 3 got", len(sys.argv)
418       print sys.argv[1], sys.argv[2], sys.argv[3]
419       usage()
420 elif "prepare" == sys.argv[2]:
421    if len(sys.argv) == 3:
422       try:
423          dvd.prepare()
424       except DVDError, e:
425          print "Error while preparing medium: ", str(e)
426          if e.errno != 0:
427             sys.exit(e.errno & 0x7F)
428          else:
429             sys.exit(errno.EPIPE)
430       else:
431          print "Medium prepared successfully."
432    else:
433       print "Wrong number of arguments for prepare operation."
434       usage()
435 elif "test" == sys.argv[2]:
436    try:
437       print str(dvd)
438       print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
439       print "Free space: " + str(dvd.free())
440    except DVDError, e:
441       print "Error while getting informations: ", str(e)
442 elif "write" == sys.argv[2]:
443    if len(sys.argv) == 5:
444       try:
445          dvd.write(long(sys.argv[3]), sys.argv[4])
446       except DVDError, e:
447          print "Error while writing part file: ", str(e)
448          if e.errno != 0:
449             sys.exit(e.errno & 0x7F)
450          else:
451             sys.exit(errno.EPIPE)
452       else:
453          print "Part file " + sys.argv[4] + " successfully written to disk."
454    else:
455       print "Wrong number of arguments for write operation."
456       usage()
457       sys.exit(1)
458 else:
459    print "No operation - use test, free, prepare or write."
460    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
461 sys.exit(0)