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