Check in MountImage task if /etc/fstab is present
[snf-image] / snf-image-host / copy-monitor.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2011, 2012 GRNET S.A.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19
20 """Utility to monitor the progress of image deployment
21
22 A small utility to monitor the progress of image deployment
23 by watching the contents of /proc/<pid>/io and producing
24 notifications.
25 """
26
27 import os
28 import sys
29 import time
30 import json
31 import prctl
32 import signal
33 import socket
34
35 MSG_TYPE = "image-copy-progress"
36
37
38 def parse_arguments(args):
39     from optparse import OptionParser
40
41     kw = {}
42     kw['usage'] = "%prog [options] command [args...]"
43     kw['description'] = \
44         "%prog runs 'command' with the specified arguments, monitoring the " \
45         "number of bytes read by it. 'command' is assumed to be " \
46         "A program used to install the OS for a Ganeti instance. %prog " \
47         "periodically issues notifications of type 'copy-progress'."
48
49     parser = OptionParser(**kw)
50     parser.disable_interspersed_args()
51     parser.add_option("-r", "--read-bytes",
52                       action="store", type="int", dest="read_bytes",
53                       metavar="BYTES_TO_READ",
54                       help="The expected number of bytes to be read, "
55                            "used to compute input progress",
56                       default=None)
57     parser.add_option(
58         "-o", "--output_fd", dest="output", default=None, metavar="FILE",
59         type="int", help="Write output notifications to this file descriptor")
60
61     (opts, args) = parser.parse_args(args)
62
63     if opts.read_bytes is None:
64         sys.stderr.write("Fatal: Option '-r' is mandatory.\n")
65         parser.print_help()
66         sys.exit(1)
67
68     if opts.output is None:
69         sys.stderr.write("Fatal: Option '-o' is mandatory.\n")
70         parser.print_help()
71         sys.exit(1)
72
73     if len(args) == 0:
74         sys.stderr.write("Fatal: You need to specify the command to run.\n")
75         parser.print_help()
76         sys.exit(1)
77
78     return (opts, args)
79
80
81 def report_wait_status(pid, status):
82     if os.WIFEXITED(status):
83         sys.stderr.write("Child PID = %d exited, status = %d\n" %
84                          (pid, os.WEXITSTATUS(status)))
85     elif os.WIFSIGNALED(status):
86         sys.stderr.write("Child PID = %d died by signal, signal = %d\n" %
87                          (pid, os.WTERMSIG(status)))
88     elif os.WIFSTOPPED(status):
89         sys.stderr.write("Child PID = %d stopped by signal, signal = %d\n" %
90                          (pid, os.WSTOPSIG(status)))
91     else:
92         sys.stderr.write("Internal error: Unhandled case, "
93                          "PID = %d, status = %d\n" % (pid, status))
94         sys.exit(1)
95     sys.stderr.flush()
96
97
98 def send_message(to, message):
99     message['timestamp'] = time.time()
100     os.write(to, "%s\n" % json.dumps(message))
101
102
103 def main():
104     (opts, args) = parse_arguments(sys.argv[1:])
105     out = opts.output
106     pid = os.fork()
107     if pid == 0:
108         # In child process:
109
110         # Make sure we die with the parent and are not left behind
111         # WARNING: This uses the prctl(2) call and is Linux-specific.
112         prctl.set_pdeathsig(signal.SIGHUP)
113
114         # exec command specified in arguments,
115         # searching the $PATH, keeping all environment
116         os.execvpe(args[0], args, os.environ)
117         sys.stderr.write("execvpe failed, exiting with non-zero status")
118         os.exit(1)
119
120     # In parent process:
121     iofname = "/proc/%d/io" % pid
122     iof = open(iofname, "r", 0)   # 0: unbuffered open
123     sys.stderr.write("%s: created child PID = %d, monitoring file %s\n" %
124                      (sys.argv[0], pid, iofname))
125
126     message = {}
127     message['type'] = MSG_TYPE
128     message['total'] = opts.read_bytes
129
130     while True:
131         # check if the child process is still alive
132         (wpid, status) = os.waitpid(pid, os.WNOHANG)
133         if wpid == pid:
134             report_wait_status(pid, status)
135             if (os.WIFEXITED(status) or os.WIFSIGNALED(status)):
136                 if not (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0):
137                     return 1
138                 else:
139                     message['position'] = message['total']
140                     message['progress'] = float(100)
141                     send_message(out, message)
142                     return 0
143
144         iof.seek(0)
145         for l in iof.readlines():
146             if l.startswith("rchar:"):
147                 message['position'] = int(l.split(': ')[1])
148                 message['progress'] = float(0) if opts.read_bytes == 0 \
149                     else float("%2.2f" % (
150                         message['position'] * 100.0 / message['total']))
151                 send_message(out, message)
152                 break
153
154         # Sleep for a while
155         time.sleep(3)
156
157 if __name__ == "__main__":
158     sys.exit(main())
159
160 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :