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        parser.add_argument("--dirty-ring-size", dest="dirty_ring_size",
64                            default=0, type=int)
65
66        self._parser = parser
67
68    def get_engine(self, args):
69        return Engine(binary=args.binary,
70                      dst_host=args.dst_host,
71                      kernel=args.kernel,
72                      initrd=args.initrd,
73                      transport=args.transport,
74                      sleep=args.sleep,
75                      debug=args.debug,
76                      verbose=args.verbose)
77
78    def get_hardware(self, args):
79        def split_map(value):
80            if value == "":
81                return []
82            return value.split(",")
83
84        return Hardware(cpus=args.cpus,
85                        mem=args.mem,
86
87                        src_cpu_bind=split_map(args.src_cpu_bind),
88                        src_mem_bind=split_map(args.src_mem_bind),
89                        dst_cpu_bind=split_map(args.dst_cpu_bind),
90                        dst_mem_bind=split_map(args.dst_mem_bind),
91
92                        locked_pages=args.locked_pages,
93                        huge_pages=args.huge_pages,
94                        prealloc_pages=args.prealloc_pages,
95
96                        dirty_ring_size=args.dirty_ring_size)
97
98
99class Shell(BaseShell):
100
101    def __init__(self):
102        super(Shell, self).__init__()
103
104        parser = self._parser
105
106        parser.add_argument("--output", dest="output", default=None)
107
108        # Scenario args
109        parser.add_argument("--max-iters", dest="max_iters", default=30, type=int)
110        parser.add_argument("--max-time", dest="max_time", default=300, type=int)
111        parser.add_argument("--bandwidth", dest="bandwidth", default=125000, type=int)
112        parser.add_argument("--downtime", dest="downtime", default=500, type=int)
113
114        parser.add_argument("--pause", dest="pause", default=False, action="store_true")
115        parser.add_argument("--pause-iters", dest="pause_iters", default=5, type=int)
116
117        parser.add_argument("--post-copy", dest="post_copy", default=False, action="store_true")
118        parser.add_argument("--post-copy-iters", dest="post_copy_iters", default=5, type=int)
119
120        parser.add_argument("--auto-converge", dest="auto_converge", default=False, action="store_true")
121        parser.add_argument("--auto-converge-step", dest="auto_converge_step", default=10, type=int)
122
123        parser.add_argument("--compression-mt", dest="compression_mt", default=False, action="store_true")
124        parser.add_argument("--compression-mt-threads", dest="compression_mt_threads", default=1, type=int)
125
126        parser.add_argument("--compression-xbzrle", dest="compression_xbzrle", default=False, action="store_true")
127        parser.add_argument("--compression-xbzrle-cache", dest="compression_xbzrle_cache", default=10, type=int)
128
129        parser.add_argument("--multifd", dest="multifd", default=False,
130                            action="store_true")
131        parser.add_argument("--multifd-channels", dest="multifd_channels",
132                            default=2, type=int)
133
134        parser.add_argument("--dirty-limit", dest="dirty_limit", default=False,
135                            action="store_true")
136
137        parser.add_argument("--x-vcpu-dirty-limit-period",
138                            dest="x_vcpu_dirty_limit_period",
139                            default=500, type=int)
140
141        parser.add_argument("--vcpu-dirty-limit",
142                            dest="vcpu_dirty_limit",
143                            default=1, type=int)
144
145    def get_scenario(self, args):
146        return Scenario(name="perfreport",
147                        downtime=args.downtime,
148                        bandwidth=args.bandwidth,
149                        max_iters=args.max_iters,
150                        max_time=args.max_time,
151
152                        pause=args.pause,
153                        pause_iters=args.pause_iters,
154
155                        post_copy=args.post_copy,
156                        post_copy_iters=args.post_copy_iters,
157
158                        auto_converge=args.auto_converge,
159                        auto_converge_step=args.auto_converge_step,
160
161                        compression_mt=args.compression_mt,
162                        compression_mt_threads=args.compression_mt_threads,
163
164                        compression_xbzrle=args.compression_xbzrle,
165                        compression_xbzrle_cache=args.compression_xbzrle_cache,
166
167                        multifd=args.multifd,
168                        multifd_channels=args.multifd_channels,
169
170                        dirty_limit=args.dirty_limit,
171                        x_vcpu_dirty_limit_period=\
172                            args.x_vcpu_dirty_limit_period,
173                        vcpu_dirty_limit=args.vcpu_dirty_limit)
174
175    def run(self, argv):
176        args = self._parser.parse_args(argv)
177        logging.basicConfig(level=(logging.DEBUG if args.debug else
178                                   logging.INFO if args.verbose else
179                                   logging.WARN))
180
181
182        engine = self.get_engine(args)
183        hardware = self.get_hardware(args)
184        scenario = self.get_scenario(args)
185
186        try:
187            report = engine.run(hardware, scenario)
188            if args.output is None:
189                print(report.to_json())
190            else:
191                with open(args.output, "w") as fh:
192                    print(report.to_json(), file=fh)
193            return 0
194        except Exception as e:
195            print("Error: %s" % str(e), file=sys.stderr)
196            if args.debug:
197                raise
198            return 1
199
200
201class BatchShell(BaseShell):
202
203    def __init__(self):
204        super(BatchShell, self).__init__()
205
206        parser = self._parser
207
208        parser.add_argument("--filter", dest="filter", default="*")
209        parser.add_argument("--output", dest="output", default=os.getcwd())
210
211    def run(self, argv):
212        args = self._parser.parse_args(argv)
213        logging.basicConfig(level=(logging.DEBUG if args.debug else
214                                   logging.INFO if args.verbose else
215                                   logging.WARN))
216
217
218        engine = self.get_engine(args)
219        hardware = self.get_hardware(args)
220
221        try:
222            for comparison in COMPARISONS:
223                compdir = os.path.join(args.output, comparison._name)
224                for scenario in comparison._scenarios:
225                    name = os.path.join(comparison._name, scenario._name)
226                    if not fnmatch.fnmatch(name, args.filter):
227                        if args.verbose:
228                            print("Skipping %s" % name)
229                        continue
230
231                    if args.verbose:
232                        print("Running %s" % name)
233
234                    dirname = os.path.join(args.output, comparison._name)
235                    filename = os.path.join(dirname, scenario._name + ".json")
236                    if not os.path.exists(dirname):
237                        os.makedirs(dirname)
238                    report = engine.run(hardware, scenario)
239                    with open(filename, "w") as fh:
240                        print(report.to_json(), file=fh)
241        except Exception as e:
242            print("Error: %s" % str(e), file=sys.stderr)
243            if args.debug:
244                raise
245
246
247class PlotShell(object):
248
249    def __init__(self):
250        super(PlotShell, self).__init__()
251
252        self._parser = argparse.ArgumentParser(description="Migration Test Tool")
253
254        self._parser.add_argument("--output", dest="output", default=None)
255
256        self._parser.add_argument("--debug", dest="debug", default=False, action="store_true")
257        self._parser.add_argument("--verbose", dest="verbose", default=False, action="store_true")
258
259        self._parser.add_argument("--migration-iters", dest="migration_iters", default=False, action="store_true")
260        self._parser.add_argument("--total-guest-cpu", dest="total_guest_cpu", default=False, action="store_true")
261        self._parser.add_argument("--split-guest-cpu", dest="split_guest_cpu", default=False, action="store_true")
262        self._parser.add_argument("--qemu-cpu", dest="qemu_cpu", default=False, action="store_true")
263        self._parser.add_argument("--vcpu-cpu", dest="vcpu_cpu", default=False, action="store_true")
264
265        self._parser.add_argument("reports", nargs='*')
266
267    def run(self, argv):
268        args = self._parser.parse_args(argv)
269        logging.basicConfig(level=(logging.DEBUG if args.debug else
270                                   logging.INFO if args.verbose else
271                                   logging.WARN))
272
273
274        if len(args.reports) == 0:
275            print("At least one report required", file=sys.stderr)
276            return 1
277
278        if not (args.qemu_cpu or
279                args.vcpu_cpu or
280                args.total_guest_cpu or
281                args.split_guest_cpu):
282            print("At least one chart type is required", file=sys.stderr)
283            return 1
284
285        reports = []
286        for report in args.reports:
287            reports.append(Report.from_json_file(report))
288
289        plot = Plot(reports,
290                    args.migration_iters,
291                    args.total_guest_cpu,
292                    args.split_guest_cpu,
293                    args.qemu_cpu,
294                    args.vcpu_cpu)
295
296        plot.generate(args.output)
297