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