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