root / snf-image-host / copy-monitor.py @ 87d1bf2e
History | View | Annotate | Download (5.4 kB)
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 |
|
36 |
def parse_arguments(args): |
37 |
from optparse import OptionParser |
38 |
|
39 |
kw = {} |
40 |
kw['usage'] = "%prog [options] command [args...]" |
41 |
kw['description'] = \
|
42 |
"%prog runs 'command' with the specified arguments, monitoring the " \
|
43 |
"number of bytes read by it. 'command' is assumed to be " \
|
44 |
"A program used to install the OS for a Ganeti instance. %prog " \
|
45 |
"periodically issues notifications of type 'copy-progress'."
|
46 |
|
47 |
parser = OptionParser(**kw) |
48 |
parser.disable_interspersed_args() |
49 |
parser.add_option("-r", "--read-bytes", |
50 |
action="store", type="int", dest="read_bytes", |
51 |
metavar="BYTES_TO_READ",
|
52 |
help="The expected number of bytes to be read, " \
|
53 |
"used to compute input progress",
|
54 |
default=None)
|
55 |
parser.add_option("-o", "--output_fd", dest="output", default=None, |
56 |
metavar="FILE", type="int", |
57 |
help="Write output notifications to this file descriptor")
|
58 |
|
59 |
(opts, args) = parser.parse_args(args) |
60 |
|
61 |
if opts.read_bytes is None: |
62 |
sys.stderr.write("Fatal: Option '-r' is mandatory.\n")
|
63 |
parser.print_help() |
64 |
sys.exit(1)
|
65 |
|
66 |
if opts.output is None: |
67 |
sys.stderr.write("Fatal: Option '-o' is mandatory.\n")
|
68 |
parser.print_help() |
69 |
sys.exit(1)
|
70 |
|
71 |
if len(args) == 0: |
72 |
sys.stderr.write("Fatal: You need to specify the command to run.\n")
|
73 |
parser.print_help() |
74 |
sys.exit(1)
|
75 |
|
76 |
return (opts, args)
|
77 |
|
78 |
|
79 |
def report_wait_status(pid, status): |
80 |
if os.WIFEXITED(status):
|
81 |
sys.stderr.write("Child PID = %d exited, status = %d\n" %
|
82 |
(pid, os.WEXITSTATUS(status))) |
83 |
elif os.WIFSIGNALED(status):
|
84 |
sys.stderr.write("Child PID = %d died by signal, signal = %d\n" %
|
85 |
(pid, os.WTERMSIG(status))) |
86 |
elif os.WIFSTOPPED(status):
|
87 |
sys.stderr.write("Child PID = %d stopped by signal, signal = %d\n" %
|
88 |
(pid, os.WSTOPSIG(status))) |
89 |
else:
|
90 |
sys.stderr.write("Internal error: Unhandled case, " \
|
91 |
"PID = %d, status = %d\n" % (pid, status))
|
92 |
sys.exit(1)
|
93 |
sys.stderr.flush() |
94 |
|
95 |
|
96 |
def send_message(to, message): |
97 |
message['timestamp'] = time.time()
|
98 |
os.write(to, "%s\n" % json.dumps(message))
|
99 |
|
100 |
|
101 |
def main(): |
102 |
(opts, args) = parse_arguments(sys.argv[1:])
|
103 |
out = opts.output |
104 |
pid = os.fork() |
105 |
if pid == 0: |
106 |
# In child process:
|
107 |
|
108 |
# Make sure we die with the parent and are not left behind
|
109 |
# WARNING: This uses the prctl(2) call and is Linux-specific.
|
110 |
prctl.set_pdeathsig(signal.SIGHUP) |
111 |
|
112 |
# exec command specified in arguments,
|
113 |
# searching the $PATH, keeping all environment
|
114 |
os.execvpe(args[0], args, os.environ)
|
115 |
sys.stderr.write("execvpe failed, exiting with non-zero status")
|
116 |
os.exit(1)
|
117 |
|
118 |
# In parent process:
|
119 |
iofname = "/proc/%d/io" % pid
|
120 |
iof = open(iofname, "r", 0) # 0: unbuffered open |
121 |
sys.stderr.write("%s: created child PID = %d, monitoring file %s\n" %
|
122 |
(sys.argv[0], pid, iofname))
|
123 |
|
124 |
message = {} |
125 |
message['type'] = 'copy-progress' |
126 |
message['total'] = opts.read_bytes
|
127 |
|
128 |
while True: |
129 |
# check if the child process is still alive
|
130 |
(wpid, status) = os.waitpid(pid, os.WNOHANG) |
131 |
if wpid == pid:
|
132 |
report_wait_status(pid, status) |
133 |
if (os.WIFEXITED(status) or os.WIFSIGNALED(status)): |
134 |
if not (os.WIFEXITED(status) and |
135 |
os.WEXITSTATUS(status) == 0):
|
136 |
return 1 |
137 |
else:
|
138 |
message['position'] = message['total'] |
139 |
message['progress'] = float(100) |
140 |
send_message(out, message) |
141 |
return 0 |
142 |
|
143 |
iof.seek(0)
|
144 |
for l in iof.readlines(): |
145 |
if l.startswith("rchar:"): |
146 |
message['position'] = int(l.split(': ')[1]) |
147 |
message['progress'] = float(100) if opts.read_bytes == 0 \ |
148 |
else float("%2.2f" % ( |
149 |
message['position'] * 100.0 / message['total'])) |
150 |
send_message(out, message) |
151 |
break
|
152 |
|
153 |
# Sleep for a while
|
154 |
time.sleep(3)
|
155 |
|
156 |
if __name__ == "__main__": |
157 |
sys.exit(main()) |
158 |
|
159 |
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
|