1from __future__ import print_function
2#
3# Migration test command line shell integration
4#
5# Copyright (c) 2016 Red Hat, Inc.
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, see <http://www.gnu.org/licenses/>.
19#
20
21
22import argparse
23import fnmatch
24import os
25import os.path
26import platform
27import sys
28import logging
29
30from guestperf.hardware import Hardware
31from guestperf.engine import Engine
32from guestperf.scenario import Scenario
33from guestperf.comparison import COMPARISONS
34from guestperf.plot import Plot
35from guestperf.report import Report
36
37
38class BaseShell(object):
39
40    def __init__(self):
41        parser = argparse.ArgumentParser(description="Migration Test Tool")
42
43        # Test args
44        parser.add_argument("--debug", dest="debug", default=False, action="store_true")
45        parser.add_argument("--verbose", dest="verbose", default=False, action="store_true")
46        parser.add_argument("--sleep", dest="sleep", default=15, type=int)
47        parser.add_argument("--binary", dest="binary", default="/usr/bin/qemu-system-x86_64")
48        parser.add_argument("--dst-host", dest="dst_host", default="localhost")
49        parser.add_argument("--kernel", dest="kernel", default="/boot/vmlinuz-%s" % platform.release())
50        parser.add_argument("--initrd", dest="initrd", default="tests/migration/initrd-stress.img")
51        parser.add_argument("--transport", dest="transport", default="unix")
52
53
54        # Hardware args
55        parser.add_argument("--cpus", dest="cpus", default=1, type=int)
56        parser.add_argument("--mem", dest="mem", default=1, type=int)
57        parser.add_argument("--src-cpu-bind", dest="src_cpu_bind", default="")
58        parser.add_argument("--src-mem-bind", dest="src_mem_bind", default="")
59        parser.add_argument("--dst-cpu-bind", dest="dst_cpu_bind", default="")
60        parser.add_argument("--dst-mem-bind", dest="dst_mem_bind", default="")
61        parser.add_argument("--prealloc-pages", dest="prealloc_pages", default=False)
62        parser.add_argument("--huge-pages", dest="huge_pages", default=False)
63        parser.add_argument("--locked-pages", dest="locked_pages", default=False)
64
65        self._parser = parser
66
67    def get_engine(self, args):
68        return Engine(binary=args.binary,
69                      dst_host=args.dst_host,
70                      kernel=args.kernel,
71                      initrd=args.initrd,
72                      transport=args.transport,
73                      sleep=args.sleep,
74                      debug=args.debug,
75                      verbose=args.verbose)
76
77    def get_hardware(self, args):
78        def split_map(value):
79            if value == "":
80                return []
81            return value.split(",")
82
83        return Hardware(cpus=args.cpus,
84                        mem=args.mem,
85
86                        src_cpu_bind=split_map(args.src_cpu_bind),
87                        src_mem_bind=split_map(args.src_mem_bind),
88                        dst_cpu_bind=split_map(args.dst_cpu_bind),
89                        dst_mem_bind=split_map(args.dst_mem_bind),
90
91                        locked_pages=args.locked_pages,
92                        huge_pages=args.huge_pages,
93                        prealloc_pages=args.prealloc_pages)
94
95
96class Shell(BaseShell):
97
98    def __init__(self):
99        super(Shell, self).__init__()
100
101        parser = self._parser
102
103        parser.add_argument("--output", dest="output", default=None)
104
105        # Scenario args
106        parser.add_argument("--max-iters", dest="max_iters", default=30, type=int)
107        parser.add_argument("--max-time", dest="max_time", default=300, type=int)
108        parser.add_argument("--bandwidth", dest="bandwidth", default=125000, type=int)
109        parser.add_argument("--downtime", dest="downtime", default=500, type=int)
110
111        parser.add_argument("--pause", dest="pause", default=False, action="store_true")
112        parser.add_argument("--pause-iters", dest="pause_iters", default=5, type=int)
113
114        parser.add_argument("--post-copy", dest="post_copy", default=False, action="store_true")
115        parser.add_argument("--post-copy-iters", dest="post_copy_iters", default=5, type=int)
116
117        parser.add_argument("--auto-converge", dest="auto_converge", default=False, action="store_true")
118        parser.add_argument("--auto-converge-step", dest="auto_converge_step", default=10, type=int)
119
120        parser.add_argument("--compression-mt", dest="compression_mt", default=False, action="store_true")
121        parser.add_argument("--compression-mt-threads", dest="compression_mt_threads", default=1, type=int)
122
123        parser.add_argument("--compression-xbzrle", dest="compression_xbzrle", default=False, action="store_true")
124        parser.add_argument("--compression-xbzrle-cache", dest="compression_xbzrle_cache", default=10, type=int)
125
126    def get_scenario(self, args):
127        return Scenario(name="perfreport",
128                        downtime=args.downtime,
129                        bandwidth=args.bandwidth,
130                        max_iters=args.max_iters,
131                        max_time=args.max_time,
132
133                        pause=args.pause,
134                        pause_iters=args.pause_iters,
135
136                        post_copy=args.post_copy,
137                        post_copy_iters=args.post_copy_iters,
138
139                        auto_converge=args.auto_converge,
140                        auto_converge_step=args.auto_converge_step,
141
142                        compression_mt=args.compression_mt,
143                        compression_mt_threads=args.compression_mt_threads,
144
145                        compression_xbzrle=args.compression_xbzrle,
146                        compression_xbzrle_cache=args.compression_xbzrle_cache)
147
148    def run(self, argv):
149        args = self._parser.parse_args(argv)
150        logging.basicConfig(level=(logging.DEBUG if args.debug else
151                                   logging.INFO if args.verbose else
152                                   logging.WARN))
153
154
155        engine = self.get_engine(args)
156        hardware = self.get_hardware(args)
157        scenario = self.get_scenario(args)
158
159        try:
160            report = engine.run(hardware, scenario)
161            if args.output is None:
162                print(report.to_json())
163            else:
164                with open(args.output, "w") as fh:
165                    print(report.to_json(), file=fh)
166            return 0
167        except Exception as e:
168            print("Error: %s" % str(e), file=sys.stderr)
169            if args.debug:
170                raise
171            return 1
172
173
174class BatchShell(BaseShell):
175
176    def __init__(self):
177        super(BatchShell, self).__init__()
178
179        parser = self._parser
180
181        parser.add_argument("--filter", dest="filter", default="*")
182        parser.add_argument("--output", dest="output", default=os.getcwd())
183
184    def run(self, argv):
185        args = self._parser.parse_args(argv)
186        logging.basicConfig(level=(logging.DEBUG if args.debug else
187                                   logging.INFO if args.verbose else
188                                   logging.WARN))
189
190
191        engine = self.get_engine(args)
192        hardware = self.get_hardware(args)
193
194        try:
195            for comparison in COMPARISONS:
196                compdir = os.path.join(args.output, comparison._name)
197                for scenario in comparison._scenarios:
198                    name = os.path.join(comparison._name, scenario._name)
199                    if not fnmatch.fnmatch(name, args.filter):
200                        if args.verbose:
201                            print("Skipping %s" % name)
202                        continue
203
204                    if args.verbose:
205                        print("Running %s" % name)
206
207                    dirname = os.path.join(args.output, comparison._name)
208                    filename = os.path.join(dirname, scenario._name + ".json")
209                    if not os.path.exists(dirname):
210                        os.makedirs(dirname)
211                    report = engine.run(hardware, scenario)
212                    with open(filename, "w") as fh:
213                        print(report.to_json(), file=fh)
214        except Exception as e:
215            print("Error: %s" % str(e), file=sys.stderr)
216            if args.debug:
217                raise
218
219
220class PlotShell(object):
221
222    def __init__(self):
223        super(PlotShell, self).__init__()
224
225        self._parser = argparse.ArgumentParser(description="Migration Test Tool")
226
227        self._parser.add_argument("--output", dest="output", default=None)
228
229        self._parser.add_argument("--debug", dest="debug", default=False, action="store_true")
230        self._parser.add_argument("--verbose", dest="verbose", default=False, action="store_true")
231
232        self._parser.add_argument("--migration-iters", dest="migration_iters", default=False, action="store_true")
233        self._parser.add_argument("--total-guest-cpu", dest="total_guest_cpu", default=False, action="store_true")
234        self._parser.add_argument("--split-guest-cpu", dest="split_guest_cpu", default=False, action="store_true")
235        self._parser.add_argument("--qemu-cpu", dest="qemu_cpu", default=False, action="store_true")
236        self._parser.add_argument("--vcpu-cpu", dest="vcpu_cpu", default=False, action="store_true")
237
238        self._parser.add_argument("reports", nargs='*')
239
240    def run(self, argv):
241        args = self._parser.parse_args(argv)
242        logging.basicConfig(level=(logging.DEBUG if args.debug else
243                                   logging.INFO if args.verbose else
244                                   logging.WARN))
245
246
247        if len(args.reports) == 0:
248            print("At least one report required", file=sys.stderr)
249            return 1
250
251        if not (args.qemu_cpu or
252                args.vcpu_cpu or
253                args.total_guest_cpu or
254                args.split_guest_cpu):
255            print("At least one chart type is required", file=sys.stderr)
256            return 1
257
258        reports = []
259        for report in args.reports:
260            reports.append(Report.from_json_file(report))
261
262        plot = Plot(reports,
263                    args.migration_iters,
264                    args.total_guest_cpu,
265                    args.split_guest_cpu,
266                    args.qemu_cpu,
267                    args.vcpu_cpu)
268
269        plot.generate(args.output)
270