]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-simulator.in
Fix #1545 about fix in manual_prune.pl script with large number of volumes
[bacula/bacula] / bacula / scripts / dvd-simulator.in
1 #!@PYTHON@
2 #
3 #   Bacula(R) - The Network Backup Solution
4 #
5 #   Copyright (C) 2000-2015 Kern Sibbald
6 #   Copyright (C) 2000-2014 Free Software Foundation Europe e.V.
7 #
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.
10 #
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.
15 #
16 #   This notice must be preserved when any source code is 
17 #   conveyed and/or propagated.
18 #
19 #   Bacula(R) is a registered trademark of Kern Sibbald.
20 #
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
24 #    along.
25 #
26 #  called:  dvd-simulator <dvd-device-name> operation args
27 #
28 #  operations used by Bacula:
29 #
30 #   free  (no arguments)
31 #             Scan the device and report the available space.
32 #
33 #   write  op filename
34 #              Write a part file to disk.
35 #              This operation needs two additional arguments.
36 #              The first (op) indicates to
37 #                  0 -- append
38 #                  1 -- first write to a blank disk
39 #                  2 -- blank or truncate a disk
40 #
41 #               The second is the filename to write
42 #
43 #   operations available but not used by Bacula:
44 #
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
51 #              is useless.
52 #
53 #
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.
58
59
60 import popen2
61 import os
62 import os.path
63 import shutil
64 import errno
65 import sys
66 import re
67 import signal
68 import time
69 import array
70
71 class disk:
72 # Configurable values:
73    
74    dvdrwmediainfo = "@DVDRWMEDIAINFO@"
75    growcmd = "@GROWISOFS@"
76    dvdrwformat = "@DVDRWFORMAT@"
77    dd = "@DD@"
78    margin = 10485760 # 10 mb security margin
79
80    # Comment the following line if you want the tray to be reloaded
81    # when writing ends.
82    growcmd += " -use-the-force-luke=notray"
83
84 # end of configurable values
85
86 ###############################################################################
87 #
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).
93 #
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
112 #
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"
120       self.pid = 0
121       self.next_session = -1
122       self.capacity = -1
123       self.maxcapacity = 4000000000
124
125       self.freespace_collected = 0
126       self.mediumtype_collected = 0
127
128       self.growcmd += " -quiet"
129
130       if self.is4gbsupported():
131          self.growcmd += " -use-the-force-luke=4gms"
132
133       self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
134                         "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
135
136       return
137
138    def __repr__(self):
139       return "disk(" + self.device + ") # This is an instance of class disk"
140
141    def __str__(self):
142       if not self.freespace_collected:
143          self.collect_freespace();
144       if not self.mediumtype_collected:
145          self.collect_mediumtype();
146       
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"
152       return self.me
153
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):
159          return 1
160       if os.WEXITSTATUS(status) != 0:
161          return 1
162       strres = processi.fromchild.readline()[0:-1]
163       version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
164       if not version: # Non-Linux: allow
165          return 1
166       
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)):
168          return 1
169       else:
170          return 0
171
172    def collect_freespace(self): # Collects current free space
173       self.next_session = 0
174       self.capacity = 4000000000
175       self.freespace_collected = 1
176
177       cmd = "du -sb " + self.device
178       processi = popen2.Popen4(cmd)        
179       status = processi.wait()
180       if not os.WIFEXITED(status):
181          return 1;
182       if os.WEXITSTATUS(status) != 0:
183          return 1;
184       result = processi.fromchild.read()
185       
186       used = re.search(r"(\d+)\s", result, re.MULTILINE)
187
188       self.capacity = self.maxcapacity - long(used.group(1))
189       if self.capacity < 0:
190          self.capacity = 0
191
192       return 0
193    
194    def collect_mediumtype(self): # Collects current medium type
195       self.lasterror = ""
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)
203          return
204       result = processi.fromchild.read()
205       
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)
210       
211       if hardware:
212          self.hardwaredevice = hardware.group(1)
213       
214       if mediatype:
215          self.disktype = mediatype.group(2)
216       else:
217          raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
218       
219       if self.disktype == "DVD-RW":
220          if mediamode:
221             self.diskmode = mediamode.group(1)
222          else:
223             raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
224       
225       if status:
226          self.diskstatus = status.group(1)
227       else:
228          raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
229
230       
231       self.mediumtype_collected = 1
232       return
233
234    def is_empty(self):
235       if not self.freespace_collected:
236          self.collect_freespace();
237       
238       return 0 == self.next_session
239
240    def is_RW(self):
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
244
245    def is_plus_RW(self):
246       if not self.mediumtype_collected:
247          self.collect_mediumtype();
248       return "DVD+RW" == self.disktype
249
250    def is_minus_RW(self):
251       if not self.mediumtype_collected:
252          self.collect_mediumtype();
253       return "DVD-RW" == self.disktype
254       
255    def is_restricted_overwrite(self):
256       if not self.mediumtype_collected:
257          self.collect_mediumtype();
258       return self.diskmode == "Restricted Overwrite"
259
260    def is_blank(self):
261       if not self.mediumtype_collected:
262          self.collect_mediumtype();
263       
264       return self.diskstatus == "blank"
265
266    def free(self):
267       if not self.freespace_collected:
268          self.collect_freespace();
269       
270       fr = self.capacity-self.next_session-self.margin
271       if fr < 0:
272          return 0
273       else:
274          return fr
275
276    def term_handler(self, signum, frame):
277       print 'dvd-handler: Signal term_handler called with signal', signum
278       if self.pid != 0:
279          print "dvd-handler: Sending SIGTERM to pid", self.pid
280          os.kill(self.pid, signal.SIGTERM)
281          time.sleep(10)
282          print "dvd-handler: Sending SIGKILL to pid", self.pid
283          os.kill(self.pid, signal.SIGKILL)
284          sys.exit(1)
285
286    def write(self, newvol, partfile):
287       if newvol:
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)
293
294    def prepare(self):
295       if not self.is_RW():
296          raise DVDError(0, "I won't prepare a non-rewritable medium")
297       
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."
301          self.blank()
302          return # It has been completely blanked: Medium is ready to be used by Bacula
303       
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
308       
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."
312          self.blank()
313          return
314       
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)
319       self.pid = proc.pid
320       status = proc.poll() 
321       while status == -1:
322          line = proc.fromchild.readline()
323          while len(line) > 0:
324             print line,
325             line = proc.fromchild.readline()
326          time.sleep(1)
327          status = proc.poll()
328       self.pid = 0
329       print
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))
333
334    def blank(self):
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)
339       self.pid = proc.pid
340       status = proc.poll()
341       while status == -1:
342          line = proc.fromchild.readline()
343          while len(line) > 0:
344             print line,
345             line = proc.fromchild.readline()
346          time.sleep(1)
347          status = proc.poll()
348       self.pid = 0
349       print
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))
353
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)
359       self.pid = proc.pid
360       status = proc.poll()
361       while status == -1:
362          line = proc.fromchild.readline()
363          while len(line) > 0:
364             print line,
365             line = proc.fromchild.readline()
366          time.sleep(1)
367          status = proc.poll()
368       self.pid = 0
369       print
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))
373
374 # class disk ends here.
375
376 class DVDError(Exception):
377    def __init__(self, errno, value):
378       self.errno = errno
379       self.value = value
380       if self.value[-1] == '\n':
381          self.value = self.value[0:-1]
382    def __str__(self):
383       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
384
385 def usage():
386    print "Wrong number of arguments."
387    print """
388 Usage:
389
390 dvd-handler DEVICE test
391 dvd-handler DEVICE free
392 dvd-handler DEVICE write APPEND FILE
393 dvd-handler DEVICE blank
394
395 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
396
397 Operations:
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
410            is useless.
411 """
412    sys.exit(1)
413
414 if len(sys.argv) < 3:
415    usage()
416
417 dvd = disk(sys.argv[1])
418
419 if "free" == sys.argv[2]:
420    if len(sys.argv) == 3:
421       try:
422          free = dvd.free()
423       except DVDError, e:
424          if e.errno != 0:
425             print -e.errno
426          else:
427             print errno.EPIPE
428          print str(e)
429       else:
430          print free
431          print "No Error reported."
432    else:
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]
435       usage()
436 elif "prepare" == sys.argv[2]:
437    if len(sys.argv) == 3:
438       try:
439          dvd.prepare()
440       except DVDError, e:
441          print "Error while preparing medium: ", str(e)
442          if e.errno != 0:
443             sys.exit(e.errno & 0x7F)
444          else:
445             sys.exit(errno.EPIPE)
446       else:
447          print "Medium prepared successfully."
448    else:
449       print "Wrong number of arguments for prepare operation."
450       usage()
451 elif "test" == sys.argv[2]:
452    try:
453       print str(dvd)
454       print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
455       print "Free space: " + str(dvd.free())
456    except DVDError, e:
457       print "Error while getting informations: ", str(e)
458 elif "write" == sys.argv[2]:
459    if len(sys.argv) == 5:
460       try:
461          dvd.write(long(sys.argv[3]), sys.argv[4])
462       except DVDError, e:
463          print "Error while writing part file: ", str(e)
464          if e.errno != 0:
465             sys.exit(e.errno & 0x7F)
466          else:
467             sys.exit(errno.EPIPE)
468       else:
469          print "Part file " + sys.argv[4] + " successfully written to disk."
470    else:
471       print "Wrong number of arguments for write operation."
472       usage()
473       sys.exit(1)
474 else:
475    print "No operation - use test, free, prepare or write."
476    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
477 sys.exit(0)