1""" 2Support for VirtualBox using the VBoxManage command 3 4.. versionadded:: 2016.3.0 5 6If the ``vboxdrv`` kernel module is not loaded, this module can automatically 7load it by configuring ``autoload_vboxdrv`` in ``/etc/salt/minion``: 8 9.. code-block:: yaml 10 11 autoload_vboxdrv: True 12 13The default for this setting is ``False``. 14 15:depends: virtualbox 16""" 17 18 19import logging 20import os.path 21import re 22 23# pylint: disable=import-error,no-name-in-module 24import salt.utils.files 25import salt.utils.path 26from salt.exceptions import CommandExecutionError 27 28# pylint: enable=import-error,no-name-in-module 29 30 31LOG = logging.getLogger(__name__) 32 33UUID_RE = re.compile("[^{}]".format("a-zA-Z0-9._-")) 34NAME_RE = re.compile("[^{}]".format("a-zA-Z0-9._-")) 35 36 37def __virtual__(): 38 """ 39 Only load the module if VBoxManage is installed 40 """ 41 if vboxcmd(): 42 if __opts__.get("autoload_vboxdrv", False) is True: 43 if not __salt__["kmod.is_loaded"]("vboxdrv"): 44 __salt__["kmod.load"]("vboxdrv") 45 return True 46 return ( 47 False, 48 "The vboxmanaged execution module failed to load: VBoxManage is not installed.", 49 ) 50 51 52def vboxcmd(): 53 """ 54 Return the location of the VBoxManage command 55 56 CLI Example: 57 58 .. code-block:: bash 59 60 salt '*' vboxmanage.vboxcmd 61 """ 62 return salt.utils.path.which("VBoxManage") 63 64 65def list_ostypes(): 66 """ 67 List the available OS Types 68 69 CLI Example: 70 71 .. code-block:: bash 72 73 salt '*' vboxmanage.list_ostypes 74 """ 75 return list_items("ostypes", True, "ID") 76 77 78def list_nodes_min(): 79 """ 80 Return a list of registered VMs, with minimal information 81 82 CLI Example: 83 84 .. code-block:: bash 85 86 salt '*' vboxmanage.list_nodes_min 87 """ 88 ret = {} 89 cmd = "{} list vms".format(vboxcmd()) 90 for line in salt.modules.cmdmod.run(cmd).splitlines(): 91 if not line.strip(): 92 continue 93 comps = line.split() 94 name = comps[0].replace('"', "") 95 ret[name] = True 96 return ret 97 98 99def list_nodes_full(): 100 """ 101 Return a list of registered VMs, with detailed information 102 103 CLI Example: 104 105 .. code-block:: bash 106 107 salt '*' vboxmanage.list_nodes_full 108 """ 109 return list_items("vms", True, "Name") 110 111 112def list_nodes(): 113 """ 114 Return a list of registered VMs 115 116 CLI Example: 117 118 .. code-block:: bash 119 120 salt '*' vboxmanage.list_nodes 121 """ 122 ret = {} 123 nodes = list_nodes_full() 124 for node in nodes: 125 ret[node] = { 126 "id": nodes[node]["UUID"], 127 "image": nodes[node]["Guest OS"], 128 "name": nodes[node]["Name"], 129 "state": None, 130 "private_ips": [], 131 "public_ips": [], 132 } 133 ret[node]["size"] = "{} RAM, {} CPU".format( 134 nodes[node]["Memory size"], 135 nodes[node]["Number of CPUs"], 136 ) 137 return ret 138 139 140def start(name): 141 """ 142 Start a VM 143 144 CLI Example: 145 146 .. code-block:: bash 147 148 salt '*' vboxmanage.start my_vm 149 """ 150 ret = {} 151 cmd = "{} startvm {}".format(vboxcmd(), name) 152 ret = salt.modules.cmdmod.run(cmd).splitlines() 153 return ret 154 155 156def stop(name): 157 """ 158 Stop a VM 159 160 CLI Example: 161 162 .. code-block:: bash 163 164 salt '*' vboxmanage.stop my_vm 165 """ 166 cmd = "{} controlvm {} poweroff".format(vboxcmd(), name) 167 ret = salt.modules.cmdmod.run(cmd).splitlines() 168 return ret 169 170 171def register(filename): 172 """ 173 Register a VM 174 175 CLI Example: 176 177 .. code-block:: bash 178 179 salt '*' vboxmanage.register my_vm_filename 180 """ 181 if not os.path.isfile(filename): 182 raise CommandExecutionError( 183 "The specified filename ({}) does not exist.".format(filename) 184 ) 185 186 cmd = "{} registervm {}".format(vboxcmd(), filename) 187 ret = salt.modules.cmdmod.run_all(cmd) 188 if ret["retcode"] == 0: 189 return True 190 return ret["stderr"] 191 192 193def unregister(name, delete=False): 194 """ 195 Unregister a VM 196 197 CLI Example: 198 199 .. code-block:: bash 200 201 salt '*' vboxmanage.unregister my_vm_filename 202 """ 203 nodes = list_nodes_min() 204 if name not in nodes: 205 raise CommandExecutionError( 206 "The specified VM ({}) is not registered.".format(name) 207 ) 208 209 cmd = "{} unregistervm {}".format(vboxcmd(), name) 210 if delete is True: 211 cmd += " --delete" 212 ret = salt.modules.cmdmod.run_all(cmd) 213 if ret["retcode"] == 0: 214 return True 215 return ret["stderr"] 216 217 218def destroy(name): 219 """ 220 Unregister and destroy a VM 221 222 CLI Example: 223 224 .. code-block:: bash 225 226 salt '*' vboxmanage.destroy my_vm 227 """ 228 return unregister(name, True) 229 230 231def create( 232 name, 233 groups=None, 234 ostype=None, 235 register=True, 236 basefolder=None, 237 new_uuid=None, 238 **kwargs 239): 240 """ 241 Create a new VM 242 243 CLI Example: 244 245 .. code-block:: bash 246 247 salt 'hypervisor' vboxmanage.create <name> 248 """ 249 nodes = list_nodes_min() 250 if name in nodes: 251 raise CommandExecutionError( 252 "The specified VM ({}) is already registered.".format(name) 253 ) 254 255 params = "" 256 257 if name: 258 if NAME_RE.search(name): 259 raise CommandExecutionError("New VM name contains invalid characters") 260 params += " --name {}".format(name) 261 262 if groups: 263 if isinstance(groups, str): 264 groups = [groups] 265 if isinstance(groups, list): 266 params += " --groups {}".format(",".join(groups)) 267 else: 268 raise CommandExecutionError( 269 "groups must be either a string or a list of strings" 270 ) 271 272 ostypes = list_ostypes() 273 if ostype not in ostypes: 274 raise CommandExecutionError( 275 "The specified OS type ({}) is not available.".format(name) 276 ) 277 else: 278 params += " --ostype " + ostype 279 280 if register is True: 281 params += " --register" 282 283 if basefolder: 284 if not os.path.exists(basefolder): 285 raise CommandExecutionError( 286 "basefolder {} was not found".format(basefolder) 287 ) 288 params += " --basefolder {}".format(basefolder) 289 290 if new_uuid: 291 if NAME_RE.search(new_uuid): 292 raise CommandExecutionError("New UUID contains invalid characters") 293 params += " --uuid {}".format(new_uuid) 294 295 cmd = "{} create {}".format(vboxcmd(), params) 296 ret = salt.modules.cmdmod.run_all(cmd) 297 if ret["retcode"] == 0: 298 return True 299 return ret["stderr"] 300 301 302def clonevm( 303 name=None, 304 uuid=None, 305 new_name=None, 306 snapshot_uuid=None, 307 snapshot_name=None, 308 mode="machine", 309 options=None, 310 basefolder=None, 311 new_uuid=None, 312 register=False, 313 groups=None, 314 **kwargs 315): 316 """ 317 Clone a new VM from an existing VM 318 319 CLI Example: 320 321 .. code-block:: bash 322 323 salt 'hypervisor' vboxmanage.clonevm <name> <new_name> 324 """ 325 if (name and uuid) or (not name and not uuid): 326 raise CommandExecutionError( 327 "Either a name or a uuid must be specified, but not both." 328 ) 329 330 params = "" 331 nodes_names = list_nodes_min() 332 nodes_uuids = list_items("vms", True, "UUID").keys() 333 if name: 334 if name not in nodes_names: 335 raise CommandExecutionError( 336 "The specified VM ({}) is not registered.".format(name) 337 ) 338 params += " " + name 339 elif uuid: 340 if uuid not in nodes_uuids: 341 raise CommandExecutionError( 342 "The specified VM ({}) is not registered.".format(name) 343 ) 344 params += " " + uuid 345 346 if snapshot_name and snapshot_uuid: 347 raise CommandExecutionError( 348 "Either a snapshot_name or a snapshot_uuid may be specified, but not both" 349 ) 350 351 if snapshot_name: 352 if NAME_RE.search(snapshot_name): 353 raise CommandExecutionError("Snapshot name contains invalid characters") 354 params += " --snapshot {}".format(snapshot_name) 355 elif snapshot_uuid: 356 if UUID_RE.search(snapshot_uuid): 357 raise CommandExecutionError("Snapshot name contains invalid characters") 358 params += " --snapshot {}".format(snapshot_uuid) 359 360 valid_modes = ("machine", "machineandchildren", "all") 361 if mode and mode not in valid_modes: 362 raise CommandExecutionError( 363 'Mode must be one of: {} (default "machine")'.format(", ".join(valid_modes)) 364 ) 365 else: 366 params += " --mode " + mode 367 368 valid_options = ("link", "keepallmacs", "keepnatmacs", "keepdisknames") 369 if options and options not in valid_options: 370 raise CommandExecutionError( 371 "If specified, options must be one of: {}".format(", ".join(valid_options)) 372 ) 373 else: 374 params += " --options " + options 375 376 if new_name: 377 if NAME_RE.search(new_name): 378 raise CommandExecutionError("New name contains invalid characters") 379 params += " --name {}".format(new_name) 380 381 if groups: 382 if isinstance(groups, str): 383 groups = [groups] 384 if isinstance(groups, list): 385 params += " --groups {}".format(",".join(groups)) 386 else: 387 raise CommandExecutionError( 388 "groups must be either a string or a list of strings" 389 ) 390 391 if basefolder: 392 if not os.path.exists(basefolder): 393 raise CommandExecutionError( 394 "basefolder {} was not found".format(basefolder) 395 ) 396 params += " --basefolder {}".format(basefolder) 397 398 if new_uuid: 399 if NAME_RE.search(new_uuid): 400 raise CommandExecutionError("New UUID contains invalid characters") 401 params += " --uuid {}".format(new_uuid) 402 403 if register is True: 404 params += " --register" 405 406 cmd = "{} clonevm {}".format(vboxcmd(), name) 407 ret = salt.modules.cmdmod.run_all(cmd) 408 if ret["retcode"] == 0: 409 return True 410 return ret["stderr"] 411 412 413def clonemedium( 414 medium, 415 uuid_in=None, 416 file_in=None, 417 uuid_out=None, 418 file_out=None, 419 mformat=None, 420 variant=None, 421 existing=False, 422 **kwargs 423): 424 """ 425 Clone a new VM from an existing VM 426 427 CLI Example: 428 429 .. code-block:: bash 430 431 salt 'hypervisor' vboxmanage.clonemedium <name> <new_name> 432 """ 433 params = "" 434 valid_mediums = ("disk", "dvd", "floppy") 435 if medium in valid_mediums: 436 params += medium 437 else: 438 raise CommandExecutionError( 439 "Medium must be one of: {}.".format(", ".join(valid_mediums)) 440 ) 441 442 if (uuid_in and file_in) or (not uuid_in and not file_in): 443 raise CommandExecutionError( 444 "Either uuid_in or file_in must be used, but not both." 445 ) 446 447 if uuid_in: 448 if medium == "disk": 449 item = "hdds" 450 elif medium == "dvd": 451 item = "dvds" 452 elif medium == "floppy": 453 item = "floppies" 454 455 items = list_items(item) 456 457 if uuid_in not in items: 458 raise CommandExecutionError("UUID {} was not found".format(uuid_in)) 459 params += " " + uuid_in 460 elif file_in: 461 if not os.path.exists(file_in): 462 raise CommandExecutionError("File {} was not found".format(file_in)) 463 params += " " + file_in 464 465 if (uuid_out and file_out) or (not uuid_out and not file_out): 466 raise CommandExecutionError( 467 "Either uuid_out or file_out must be used, but not both." 468 ) 469 470 if uuid_out: 471 params += " " + uuid_out 472 elif file_out: 473 try: 474 # pylint: disable=resource-leakage 475 salt.utils.files.fopen(file_out, "w").close() 476 # pylint: enable=resource-leakage 477 os.unlink(file_out) 478 params += " " + file_out 479 except OSError: 480 raise CommandExecutionError("{} is not a valid filename".format(file_out)) 481 482 if mformat: 483 valid_mformat = ("VDI", "VMDK", "VHD", "RAW") 484 if mformat not in valid_mformat: 485 raise CommandExecutionError( 486 "If specified, mformat must be one of: {}".format( 487 ", ".join(valid_mformat) 488 ) 489 ) 490 else: 491 params += " --format " + mformat 492 493 valid_variant = ("Standard", "Fixed", "Split2G", "Stream", "ESX") 494 if variant and variant not in valid_variant: 495 if not os.path.exists(file_in): 496 raise CommandExecutionError( 497 "If specified, variant must be one of: {}".format( 498 ", ".join(valid_variant) 499 ) 500 ) 501 else: 502 params += " --variant " + variant 503 504 if existing: 505 params += " --existing" 506 507 cmd = "{} clonemedium {}".format(vboxcmd(), params) 508 ret = salt.modules.cmdmod.run_all(cmd) 509 if ret["retcode"] == 0: 510 return True 511 return ret["stderr"] 512 513 514def list_items(item, details=False, group_by="UUID"): 515 """ 516 Return a list of a specific type of item. The following items are available: 517 518 vms 519 runningvms 520 ostypes 521 hostdvds 522 hostfloppies 523 intnets 524 bridgedifs 525 hostonlyifs 526 natnets 527 dhcpservers 528 hostinfo 529 hostcpuids 530 hddbackends 531 hdds 532 dvds 533 floppies 534 usbhost 535 usbfilters 536 systemproperties 537 extpacks 538 groups 539 webcams 540 screenshotformats 541 542 CLI Example: 543 544 .. code-block:: bash 545 546 salt 'hypervisor' vboxmanage.items <item> 547 salt 'hypervisor' vboxmanage.items <item> details=True 548 salt 'hypervisor' vboxmanage.items <item> details=True group_by=Name 549 550 Some items do not display well, or at all, unless ``details`` is set to 551 ``True``. By default, items are grouped by the ``UUID`` field, but not all 552 items contain that field. In those cases, another field must be specified. 553 """ 554 types = ( 555 "vms", 556 "runningvms", 557 "ostypes", 558 "hostdvds", 559 "hostfloppies", 560 "intnets", 561 "bridgedifs", 562 "hostonlyifs", 563 "natnets", 564 "dhcpservers", 565 "hostinfo", 566 "hostcpuids", 567 "hddbackends", 568 "hdds", 569 "dvds", 570 "floppies", 571 "usbhost", 572 "usbfilters", 573 "systemproperties", 574 "extpacks", 575 "groups", 576 "webcams", 577 "screenshotformats", 578 ) 579 580 if item not in types: 581 raise CommandExecutionError("Item must be one of: {}.".format(", ".join(types))) 582 583 flag = "" 584 if details is True: 585 flag = " -l" 586 587 ret = {} 588 tmp_id = None 589 tmp_dict = {} 590 cmd = "{} list{} {}".format(vboxcmd(), flag, item) 591 for line in salt.modules.cmdmod.run(cmd).splitlines(): 592 if not line.strip(): 593 continue 594 comps = line.split(":") 595 if len(comps) < 1: 596 continue 597 if tmp_id is not None: 598 ret[tmp_id] = tmp_dict 599 line_val = ":".join(comps[1:]).strip() 600 if comps[0] == group_by: 601 tmp_id = line_val 602 tmp_dict = {} 603 tmp_dict[comps[0]] = line_val 604 return ret 605