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