Statistics
| Branch: | Tag: | Revision:

root / snf-image-host / copy-monitor.py @ df1c62e2

History | View | Annotate | Download (5.5 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", dest="output", default=None,
56
                    metavar="FILE",
57
                    help="Write output notifications to this file")
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
    to.write("%s\n" % json.dumps(message))
99
    to.flush()
100

    
101

    
102
def main():
103
    (opts, args) = parse_arguments(sys.argv[1:])
104

    
105
    with open(opts.output, 'w') as out:
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'] = 'copy-progress'
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
137
                                                os.WEXITSTATUS(status) == 0):
138
                        return 1
139
                    else:
140
                        message['position'] = message['total']
141
                        message['progress'] = float(100)
142
                        send_message(out, message)
143
                        return 0
144

    
145
            iof.seek(0)
146
            for l in iof.readlines():
147
                if l.startswith("rchar:"):
148
                    message['position'] = int(l.split(': ')[1])
149
                    message['progress'] = float(100) if opts.read_bytes == 0 \
150
                        else float("%2.2f" % (
151
                            message['position'] * 100.0 / message['total']))
152
                    send_message(out, message)
153
                    break
154

    
155
            # Sleep for a while
156
            time.sleep(3)
157

    
158

    
159
if __name__ == "__main__":
160
    sys.exit(main())
161

    
162
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :