Mini Shell
"""
Manage Parallels Desktop VMs with ``prlctl`` and ``prlsrvctl``. Only some of
the prlctl commands implemented so far. Of those that have been implemented,
not all of the options may have been provided yet. For a complete reference,
see the `Parallels Desktop Reference Guide
<http://download.parallels.com/desktop/v9/ga/docs/en_US/Parallels%20Command%20Line%20Reference%20Guide.pdf>`_.
This module requires the prlctl binary to be installed to run most functions.
To run parallels.prlsrvctl, the prlsrvctl binary is required.
What has not been implemented yet can be accessed through ``parallels.prlctl``
and ``parallels.prlsrvctl`` (note the preceding double dash ``--`` as
necessary):
.. code-block:: bash
salt '*' parallels.prlctl installtools macvm runas=macdev
salt -- '*' parallels.prlctl capture 'macvm --file macvm.display.png' runas=macdev
salt -- '*' parallels.prlsrvctl set '--mem-limit auto' runas=macdev
.. versionadded:: 2016.3.0
"""
import logging
import re
import shlex
import salt.utils.data
import salt.utils.path
import salt.utils.yaml
from salt.exceptions import CommandExecutionError, SaltInvocationError
__virtualname__ = "parallels"
__func_alias__ = {
"exec_": "exec",
}
log = logging.getLogger(__name__)
# Match any GUID
GUID_REGEX = re.compile(
r"{?([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})}?", re.I
)
def _normalize_args(args):
"""
Return args as a list of strings
"""
if isinstance(args, str):
return shlex.split(args)
if isinstance(args, (tuple, list)):
return [str(arg) for arg in args]
else:
return [str(args)]
def _find_guids(guid_string):
"""
Return the set of GUIDs found in guid_string
:param str guid_string:
String containing zero or more GUIDs. Each GUID may or may not be
enclosed in {}
Example data (this string contains two distinct GUIDs):
PARENT_SNAPSHOT_ID SNAPSHOT_ID
{a5b8999f-5d95-4aff-82de-e515b0101b66}
{a5b8999f-5d95-4aff-82de-e515b0101b66} *{a7345be5-ab66-478c-946e-a6c2caf14909}
"""
guids = []
for found_guid in re.finditer(GUID_REGEX, guid_string):
if found_guid.groups():
guids.append(found_guid.group(0).strip("{}"))
return sorted(list(set(guids)))
def prlsrvctl(sub_cmd, args=None, runas=None):
"""
Execute a prlsrvctl command
.. versionadded:: 2016.11.0
:param str sub_cmd:
prlsrvctl subcommand to execute
:param str args:
The arguments supplied to ``prlsrvctl <sub_cmd>``
:param str runas:
The user that the prlsrvctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.prlsrvctl info runas=macdev
salt '*' parallels.prlsrvctl usb list runas=macdev
salt -- '*' parallels.prlsrvctl set '--mem-limit auto' runas=macdev
"""
if not salt.utils.path.which("prlsrvctl"):
raise CommandExecutionError("prlsrvctl utility not available")
# Construct command
cmd = ["prlsrvctl", sub_cmd]
if args:
cmd.extend(_normalize_args(args))
# Execute command and return output
return __salt__["cmd.run"](cmd, runas=runas)
def prlctl(sub_cmd, args=None, runas=None):
"""
Execute a prlctl command
:param str sub_cmd:
prlctl subcommand to execute
:param str args:
The arguments supplied to ``prlctl <sub_cmd>``
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.prlctl user list runas=macdev
salt '*' parallels.prlctl exec 'macvm uname' runas=macdev
salt -- '*' parallels.prlctl capture 'macvm --file macvm.display.png' runas=macdev
"""
if not salt.utils.path.which("prlctl"):
raise CommandExecutionError("prlctl utility not available")
# Construct command
cmd = ["prlctl", sub_cmd]
if args:
cmd.extend(_normalize_args(args))
# Execute command and return output
return __salt__["cmd.run"](cmd, runas=runas)
def list_vms(name=None, info=False, all=False, args=None, runas=None, template=False):
"""
List information about the VMs
:param str name:
Name/ID of VM to list
.. versionchanged:: 2016.11.0
No longer implies ``info=True``
:param str info:
List extra information
:param bool all:
List all non-template VMs
:param tuple args:
Additional arguments given to ``prctl list``
:param str runas:
The user that the prlctl command will be run as
:param bool template:
List the available virtual machine templates. The real virtual
machines will not be included in the output
.. versionadded:: 2016.11.0
Example:
.. code-block:: bash
salt '*' parallels.list_vms runas=macdev
salt '*' parallels.list_vms name=macvm info=True runas=macdev
salt '*' parallels.list_vms info=True runas=macdev
salt '*' parallels.list_vms ' -o uuid,status' all=True runas=macdev
"""
# Construct argument list
if args is None:
args = []
else:
args = _normalize_args(args)
if name:
args.extend([name])
if info:
args.append("--info")
if all:
args.append("--all")
if template:
args.append("--template")
# Execute command and return output
return prlctl("list", args, runas=runas)
def clone(name, new_name, linked=False, template=False, runas=None):
"""
Clone a VM
.. versionadded:: 2016.11.0
:param str name:
Name/ID of VM to clone
:param str new_name:
Name of the new VM
:param bool linked:
Create a linked virtual machine.
:param bool template:
Create a virtual machine template instead of a real virtual machine.
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.clone macvm macvm_new runas=macdev
salt '*' parallels.clone macvm macvm_templ template=True runas=macdev
"""
args = [salt.utils.data.decode(name), "--name", salt.utils.data.decode(new_name)]
if linked:
args.append("--linked")
if template:
args.append("--template")
return prlctl("clone", args, runas=runas)
def delete(name, runas=None):
"""
Delete a VM
.. versionadded:: 2016.11.0
:param str name:
Name/ID of VM to clone
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.exec macvm 'find /etc/paths.d' runas=macdev
"""
return prlctl("delete", salt.utils.data.decode(name), runas=runas)
def exists(name, runas=None):
"""
Query whether a VM exists
.. versionadded:: 2016.11.0
:param str name:
Name/ID of VM
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.exists macvm runas=macdev
"""
vm_info = list_vms(name, info=True, runas=runas).splitlines()
for info_line in vm_info:
if f"Name: {name}" in info_line:
return True
return False
def start(name, runas=None):
"""
Start a VM
:param str name:
Name/ID of VM to start
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.start macvm runas=macdev
"""
return prlctl("start", salt.utils.data.decode(name), runas=runas)
def stop(name, kill=False, runas=None):
"""
Stop a VM
:param str name:
Name/ID of VM to stop
:param bool kill:
Perform a hard shutdown
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.stop macvm runas=macdev
salt '*' parallels.stop macvm kill=True runas=macdev
"""
# Construct argument list
args = [salt.utils.data.decode(name)]
if kill:
args.append("--kill")
# Execute command and return output
return prlctl("stop", args, runas=runas)
def restart(name, runas=None):
"""
Restart a VM by gracefully shutting it down and then restarting
it
:param str name:
Name/ID of VM to restart
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.restart macvm runas=macdev
"""
return prlctl("restart", salt.utils.data.decode(name), runas=runas)
def reset(name, runas=None):
"""
Reset a VM by performing a hard shutdown and then a restart
:param str name:
Name/ID of VM to reset
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.reset macvm runas=macdev
"""
return prlctl("reset", salt.utils.data.decode(name), runas=runas)
def status(name, runas=None):
"""
Status of a VM
:param str name:
Name/ID of VM whose status will be returned
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.status macvm runas=macdev
"""
return prlctl("status", salt.utils.data.decode(name), runas=runas)
def exec_(name, command, runas=None):
"""
Run a command on a VM
:param str name:
Name/ID of VM whose exec will be returned
:param str command:
Command to run on the VM
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.exec macvm 'find /etc/paths.d' runas=macdev
"""
# Construct argument list
args = [salt.utils.data.decode(name)]
args.extend(_normalize_args(command))
# Execute command and return output
return prlctl("exec", args, runas=runas)
def snapshot_id_to_name(name, snap_id, strict=False, runas=None):
"""
Attempt to convert a snapshot ID to a snapshot name. If the snapshot has
no name or if the ID is not found or invalid, an empty string will be returned
:param str name:
Name/ID of VM whose snapshots are inspected
:param str snap_id:
ID of the snapshot
:param bool strict:
Raise an exception if a name cannot be found for the given ``snap_id``
:param str runas:
The user that the prlctl command will be run as
Example data
.. code-block:: yaml
ID: {a5b8999f-5d95-4aff-82de-e515b0101b66}
Name: original
Date: 2016-03-04 10:50:34
Current: yes
State: poweroff
Description: original state
CLI Example:
.. code-block:: bash
salt '*' parallels.snapshot_id_to_name macvm a5b8999f-5d95-4aff-82de-e515b0101b66 runas=macdev
"""
# Validate VM name and snapshot ID
name = salt.utils.data.decode(name)
if not re.match(GUID_REGEX, snap_id):
raise SaltInvocationError(
f'Snapshot ID "{salt.utils.data.decode(snap_id)}" is not a GUID'
)
# Get the snapshot information of the snapshot having the requested ID
info = prlctl("snapshot-list", [name, "--id", snap_id], runas=runas)
# Parallels desktop returned no information for snap_id
if not info:
raise SaltInvocationError(f'No snapshots for VM "{name}" have ID "{snap_id}"')
# Try to interpret the information
try:
data = salt.utils.yaml.safe_load(info)
except salt.utils.yaml.YAMLError as err:
log.warning("Could not interpret snapshot data returned from prlctl: %s", err)
data = {}
# Find the snapshot name
if isinstance(data, dict):
snap_name = data.get("Name", "")
# If snapshot name is of type NoneType, then the snapshot is unnamed
if snap_name is None:
snap_name = ""
else:
log.warning(
"Could not interpret snapshot data returned from prlctl: "
"data is not formed as a dictionary: %s",
data,
)
snap_name = ""
# Raise or return the result
if not snap_name and strict:
raise SaltInvocationError(
'Could not find a snapshot name for snapshot ID "{}" of VM "{}"'.format(
snap_id, name
)
)
return salt.utils.data.decode(snap_name)
def snapshot_name_to_id(name, snap_name, strict=False, runas=None):
"""
Attempt to convert a snapshot name to a snapshot ID. If the name is not
found an empty string is returned. If multiple snapshots share the same
name, a list will be returned
:param str name:
Name/ID of VM whose snapshots are inspected
:param str snap_name:
Name of the snapshot
:param bool strict:
Raise an exception if multiple snapshot IDs are found
:param str runas:
The user that the prlctl command will be run as
CLI Example:
.. code-block:: bash
salt '*' parallels.snapshot_id_to_name macvm original runas=macdev
"""
# Validate VM and snapshot names
name = salt.utils.data.decode(name)
snap_name = salt.utils.data.decode(snap_name)
# Get a multiline string containing all the snapshot GUIDs
info = prlctl("snapshot-list", name, runas=runas)
# Get a set of all snapshot GUIDs in the string
snap_ids = _find_guids(info)
# Try to match the snapshot name to an ID
named_ids = []
for snap_id in snap_ids:
if snapshot_id_to_name(name, snap_id, runas=runas) == snap_name:
named_ids.append(snap_id)
# Return one or more IDs having snap_name or raise an error upon
# non-singular names
if not named_ids:
raise SaltInvocationError(
f'No snapshots for VM "{name}" have name "{snap_name}"'
)
elif len(named_ids) == 1:
return named_ids[0]
else:
multi_msg = 'Multiple snapshots for VM "{}" have name "{}"'.format(
name, snap_name
)
if strict:
raise SaltInvocationError(multi_msg)
else:
log.warning(multi_msg)
return named_ids
def _validate_snap_name(name, snap_name, strict=True, runas=None):
"""
Validate snapshot name and convert to snapshot ID
:param str name:
Name/ID of VM whose snapshot name is being validated
:param str snap_name:
Name/ID of snapshot
:param bool strict:
Raise an exception if multiple snapshot IDs are found
:param str runas:
The user that the prlctl command will be run as
"""
snap_name = salt.utils.data.decode(snap_name)
# Try to convert snapshot name to an ID without {}
if re.match(GUID_REGEX, snap_name):
return snap_name.strip("{}")
else:
return snapshot_name_to_id(name, snap_name, strict=strict, runas=runas)
def list_snapshots(name, snap_name=None, tree=False, names=False, runas=None):
"""
List the snapshots
:param str name:
Name/ID of VM whose snapshots will be listed
:param str snap_id:
Name/ID of snapshot to display information about. If ``tree=True`` is
also specified, display the snapshot subtree having this snapshot as
the root snapshot
:param bool tree:
List snapshots in tree format rather than tabular format
:param bool names:
List snapshots as ID, name pairs
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.list_snapshots macvm runas=macdev
salt '*' parallels.list_snapshots macvm tree=True runas=macdev
salt '*' parallels.list_snapshots macvm snap_name=original runas=macdev
salt '*' parallels.list_snapshots macvm names=True runas=macdev
"""
# Validate VM and snapshot names
name = salt.utils.data.decode(name)
if snap_name:
snap_name = _validate_snap_name(name, snap_name, runas=runas)
# Construct argument list
args = [name]
if tree:
args.append("--tree")
if snap_name:
args.extend(["--id", snap_name])
# Execute command
res = prlctl("snapshot-list", args, runas=runas)
# Construct ID, name pairs
if names:
# Find all GUIDs in the result
snap_ids = _find_guids(res)
# Try to find the snapshot names
ret = "{:<38} {}\n".format("Snapshot ID", "Snapshot Name")
for snap_id in snap_ids:
snap_name = snapshot_id_to_name(name, snap_id, runas=runas)
ret += f"{{{snap_id}}} {salt.utils.data.decode(snap_name)}\n"
return ret
# Return information directly from parallels desktop
else:
return res
def snapshot(name, snap_name=None, desc=None, runas=None):
"""
Create a snapshot
:param str name:
Name/ID of VM to take a snapshot of
:param str snap_name:
Name of snapshot
:param str desc:
Description of snapshot
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.create_snapshot macvm snap_name=macvm-original runas=macdev
salt '*' parallels.create_snapshot macvm snap_name=macvm-updates desc='clean install with updates' runas=macdev
"""
# Validate VM and snapshot names
name = salt.utils.data.decode(name)
if snap_name:
snap_name = salt.utils.data.decode(snap_name)
# Construct argument list
args = [name]
if snap_name:
args.extend(["--name", snap_name])
if desc:
args.extend(["--description", desc])
# Execute command and return output
return prlctl("snapshot", args, runas=runas)
def delete_snapshot(name, snap_name, runas=None, all=False):
"""
Delete a snapshot
.. note::
Deleting a snapshot from which other snapshots are dervied will not
delete the derived snapshots
:param str name:
Name/ID of VM whose snapshot will be deleted
:param str snap_name:
Name/ID of snapshot to delete
:param str runas:
The user that the prlctl command will be run as
:param bool all:
Delete all snapshots having the name given
.. versionadded:: 2016.11.0
Example:
.. code-block:: bash
salt '*' parallels.delete_snapshot macvm 'unneeded snapshot' runas=macdev
salt '*' parallels.delete_snapshot macvm 'Snapshot for linked clone' all=True runas=macdev
"""
# strict means raise an error if multiple snapshot IDs found for the name given
strict = not all
# Validate VM and snapshot names
name = salt.utils.data.decode(name)
snap_ids = _validate_snap_name(name, snap_name, strict=strict, runas=runas)
if isinstance(snap_ids, str):
snap_ids = [snap_ids]
# Delete snapshot(s)
ret = {}
for snap_id in snap_ids:
snap_id = snap_id.strip("{}")
# Construct argument list
args = [name, "--id", snap_id]
# Execute command
ret[snap_id] = prlctl("snapshot-delete", args, runas=runas)
# Return results
ret_keys = list(ret.keys())
if len(ret_keys) == 1:
return ret[ret_keys[0]]
else:
return ret
def revert_snapshot(name, snap_name, runas=None):
"""
Revert a VM to a snapshot
:param str name:
Name/ID of VM to revert to a snapshot
:param str snap_name:
Name/ID of snapshot to revert to
:param str runas:
The user that the prlctl command will be run as
Example:
.. code-block:: bash
salt '*' parallels.revert_snapshot macvm base-with-updates runas=macdev
"""
# Validate VM and snapshot names
name = salt.utils.data.decode(name)
snap_name = _validate_snap_name(name, snap_name, runas=runas)
# Construct argument list
args = [name, "--id", snap_name]
# Execute command and return output
return prlctl("snapshot-switch", args, runas=runas)
Zerion Mini Shell 1.0