]> git.sur5r.net Git - bacula/bacula/blob - bacula/scripts/dvd-simulator.in
License cleanups
[bacula/bacula] / bacula / scripts / dvd-simulator.in
1 #!@PYTHON@
2 #
3 # Copyright (C) 2000-2015, Kern Sibbald
4 # License: BSD 2-Clause
5 #
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
9 #    along.
10 #
11 #  called:  dvd-simulator <dvd-device-name> operation args
12 #
13 #  operations used by Bacula:
14 #
15 #   free  (no arguments)
16 #             Scan the device and report the available space.
17 #
18 #   write  op filename
19 #              Write a part file to disk.
20 #              This operation needs two additional arguments.
21 #              The first (op) indicates to
22 #                  0 -- append
23 #                  1 -- first write to a blank disk
24 #                  2 -- blank or truncate a disk
25 #
26 #               The second is the filename to write
27 #
28 #   operations available but not used by Bacula:
29 #
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
36 #              is useless.
37 #
38 #
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.
43
44
45 import popen2
46 import os
47 import os.path
48 import shutil
49 import errno
50 import sys
51 import re
52 import signal
53 import time
54 import array
55
56 class disk:
57 # Configurable values:
58    
59    dvdrwmediainfo = "@DVDRWMEDIAINFO@"
60    growcmd = "@GROWISOFS@"
61    dvdrwformat = "@DVDRWFORMAT@"
62    dd = "@DD@"
63    margin = 10485760 # 10 mb security margin
64
65    # Comment the following line if you want the tray to be reloaded
66    # when writing ends.
67    growcmd += " -use-the-force-luke=notray"
68
69 # end of configurable values
70
71 ###############################################################################
72 #
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).
78 #
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
97 #
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"
105       self.pid = 0
106       self.next_session = -1
107       self.capacity = -1
108       self.maxcapacity = 4000000000
109
110       self.freespace_collected = 0
111       self.mediumtype_collected = 0
112
113       self.growcmd += " -quiet"
114
115       if self.is4gbsupported():
116          self.growcmd += " -use-the-force-luke=4gms"
117
118       self.growparams = " -A 'Bacula Data' -input-charset=default -iso-level 3 -pad " + \
119                         "-p 'dvd-handler / growisofs' -sysid 'BACULADATA' -R"
120
121       return
122
123    def __repr__(self):
124       return "disk(" + self.device + ") # This is an instance of class disk"
125
126    def __str__(self):
127       if not self.freespace_collected:
128          self.collect_freespace();
129       if not self.mediumtype_collected:
130          self.collect_mediumtype();
131       
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"
137       return self.me
138
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):
144          return 1
145       if os.WEXITSTATUS(status) != 0:
146          return 1
147       strres = processi.fromchild.readline()[0:-1]
148       version = re.search(r"Linux (\d+)\.(\d+)\.(\d+)", strres)
149       if not version: # Non-Linux: allow
150          return 1
151       
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)):
153          return 1
154       else:
155          return 0
156
157    def collect_freespace(self): # Collects current free space
158       self.next_session = 0
159       self.capacity = 4000000000
160       self.freespace_collected = 1
161
162       cmd = "du -sb " + self.device
163       processi = popen2.Popen4(cmd)        
164       status = processi.wait()
165       if not os.WIFEXITED(status):
166          return 1;
167       if os.WEXITSTATUS(status) != 0:
168          return 1;
169       result = processi.fromchild.read()
170       
171       used = re.search(r"(\d+)\s", result, re.MULTILINE)
172
173       self.capacity = self.maxcapacity - long(used.group(1))
174       if self.capacity < 0:
175          self.capacity = 0
176
177       return 0
178    
179    def collect_mediumtype(self): # Collects current medium type
180       self.lasterror = ""
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)
188          return
189       result = processi.fromchild.read()
190       
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)
195       
196       if hardware:
197          self.hardwaredevice = hardware.group(1)
198       
199       if mediatype:
200          self.disktype = mediatype.group(2)
201       else:
202          raise DVDError(0, "Media type not found in " + self.dvdrwmediainfo + " output")
203       
204       if self.disktype == "DVD-RW":
205          if mediamode:
206             self.diskmode = mediamode.group(1)
207          else:
208             raise DVDError(0, "Media mode not found for DVD-RW in " + self.dvdrwmediainfo + " output")
209       
210       if status:
211          self.diskstatus = status.group(1)
212       else:
213          raise DVDError(0, "Disc status not found in " + self.dvdrwmediainfo + " output")
214
215       
216       self.mediumtype_collected = 1
217       return
218
219    def is_empty(self):
220       if not self.freespace_collected:
221          self.collect_freespace();
222       
223       return 0 == self.next_session
224
225    def is_RW(self):
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
229
230    def is_plus_RW(self):
231       if not self.mediumtype_collected:
232          self.collect_mediumtype();
233       return "DVD+RW" == self.disktype
234
235    def is_minus_RW(self):
236       if not self.mediumtype_collected:
237          self.collect_mediumtype();
238       return "DVD-RW" == self.disktype
239       
240    def is_restricted_overwrite(self):
241       if not self.mediumtype_collected:
242          self.collect_mediumtype();
243       return self.diskmode == "Restricted Overwrite"
244
245    def is_blank(self):
246       if not self.mediumtype_collected:
247          self.collect_mediumtype();
248       
249       return self.diskstatus == "blank"
250
251    def free(self):
252       if not self.freespace_collected:
253          self.collect_freespace();
254       
255       fr = self.capacity-self.next_session-self.margin
256       if fr < 0:
257          return 0
258       else:
259          return fr
260
261    def term_handler(self, signum, frame):
262       print 'dvd-handler: Signal term_handler called with signal', signum
263       if self.pid != 0:
264          print "dvd-handler: Sending SIGTERM to pid", self.pid
265          os.kill(self.pid, signal.SIGTERM)
266          time.sleep(10)
267          print "dvd-handler: Sending SIGKILL to pid", self.pid
268          os.kill(self.pid, signal.SIGKILL)
269          sys.exit(1)
270
271    def write(self, newvol, partfile):
272       if newvol:
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)
278
279    def prepare(self):
280       if not self.is_RW():
281          raise DVDError(0, "I won't prepare a non-rewritable medium")
282       
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."
286          self.blank()
287          return # It has been completely blanked: Medium is ready to be used by Bacula
288       
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
293       
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."
297          self.blank()
298          return
299       
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)
304       self.pid = proc.pid
305       status = proc.poll() 
306       while status == -1:
307          line = proc.fromchild.readline()
308          while len(line) > 0:
309             print line,
310             line = proc.fromchild.readline()
311          time.sleep(1)
312          status = proc.poll()
313       self.pid = 0
314       print
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))
318
319    def blank(self):
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)
324       self.pid = proc.pid
325       status = proc.poll()
326       while status == -1:
327          line = proc.fromchild.readline()
328          while len(line) > 0:
329             print line,
330             line = proc.fromchild.readline()
331          time.sleep(1)
332          status = proc.poll()
333       self.pid = 0
334       print
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))
338
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)
344       self.pid = proc.pid
345       status = proc.poll()
346       while status == -1:
347          line = proc.fromchild.readline()
348          while len(line) > 0:
349             print line,
350             line = proc.fromchild.readline()
351          time.sleep(1)
352          status = proc.poll()
353       self.pid = 0
354       print
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))
358
359 # class disk ends here.
360
361 class DVDError(Exception):
362    def __init__(self, errno, value):
363       self.errno = errno
364       self.value = value
365       if self.value[-1] == '\n':
366          self.value = self.value[0:-1]
367    def __str__(self):
368       return str(self.value) + " || errno = " + str(self.errno) + " (" + os.strerror(self.errno & 0x7F) + ")"
369
370 def usage():
371    print "Wrong number of arguments."
372    print """
373 Usage:
374
375 dvd-handler DEVICE test
376 dvd-handler DEVICE free
377 dvd-handler DEVICE write APPEND FILE
378 dvd-handler DEVICE blank
379
380 where DEVICE is a device name like /dev/sr0 or /dev/dvd.
381
382 Operations:
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
395            is useless.
396 """
397    sys.exit(1)
398
399 if len(sys.argv) < 3:
400    usage()
401
402 dvd = disk(sys.argv[1])
403
404 if "free" == sys.argv[2]:
405    if len(sys.argv) == 3:
406       try:
407          free = dvd.free()
408       except DVDError, e:
409          if e.errno != 0:
410             print -e.errno
411          else:
412             print errno.EPIPE
413          print str(e)
414       else:
415          print free
416          print "No Error reported."
417    else:
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]
420       usage()
421 elif "prepare" == sys.argv[2]:
422    if len(sys.argv) == 3:
423       try:
424          dvd.prepare()
425       except DVDError, e:
426          print "Error while preparing medium: ", str(e)
427          if e.errno != 0:
428             sys.exit(e.errno & 0x7F)
429          else:
430             sys.exit(errno.EPIPE)
431       else:
432          print "Medium prepared successfully."
433    else:
434       print "Wrong number of arguments for prepare operation."
435       usage()
436 elif "test" == sys.argv[2]:
437    try:
438       print str(dvd)
439       print "Blank disk: " + str(dvd.is_blank()) + " ReWritable disk: " + str(dvd.is_RW())
440       print "Free space: " + str(dvd.free())
441    except DVDError, e:
442       print "Error while getting informations: ", str(e)
443 elif "write" == sys.argv[2]:
444    if len(sys.argv) == 5:
445       try:
446          dvd.write(long(sys.argv[3]), sys.argv[4])
447       except DVDError, e:
448          print "Error while writing part file: ", str(e)
449          if e.errno != 0:
450             sys.exit(e.errno & 0x7F)
451          else:
452             sys.exit(errno.EPIPE)
453       else:
454          print "Part file " + sys.argv[4] + " successfully written to disk."
455    else:
456       print "Wrong number of arguments for write operation."
457       usage()
458       sys.exit(1)
459 else:
460    print "No operation - use test, free, prepare or write."
461    print "THIS MIGHT BE A CASE OF DEBUGGING BACULA OR AN ERROR!"
462 sys.exit(0)