#!/usr/bin/env python
#
# deploy.py - Implements "cot deploy" command
#
# June 2014, Kevin A. Keim
# Copyright (c) 2014-2015 the COT project developers.
# See the COPYRIGHT.txt file at the top-level directory of this distribution
#
# This file is part of the Common OVF Tool (COT) project.
# It is subject to the license terms in the LICENSE.txt file found in the
# top-level directory of this distribution. No part of COT, including this
# file, may be copied, modified, propagated, or distributed except
# according to the terms contained in the LICENSE.txt file.
"""Module for deploying VM descriptions to a hypervisor to instantiate VMs.
**Classes**
.. autosummary::
:nosignatures:
COTDeploy
COTDeployESXi
"""
import logging
import re
import shlex
import getpass
from distutils.version import StrictVersion
from .submodule import COTReadOnlySubmodule
from COT.helper_tools import check_call, get_ovftool_version
from COT.data_validation import InvalidInputError
logger = logging.getLogger(__name__)
[docs]class COTDeploy(COTReadOnlySubmodule):
"""Semi-abstract class for submodules used to deploy a VM to a hypervisor.
Provides some baseline parameters and input validation that are expected
to be common across all concrete subclasses.
Inherited attributes:
:attr:`~COTGenericSubmodule.UI`,
:attr:`~COTReadOnlySubmodule.package`,
Attributes:
:attr:`generic_parser`,
:attr:`parser`,
:attr:`subparsers`,
:attr:`hypervisor`,
:attr:`configuration`,
:attr:`username`,
:attr:`password`,
:attr:`power_on`,
:attr:`vm_name`,
:attr:`network_map`
"""
def __init__(self, UI):
"""Instantiate this submodule with the given UI."""
super(COTDeploy, self).__init__(UI)
# User inputs
self._hypervisor = None
self._configuration = None
self.username = None
"""Server login username."""
self.password = None
"""Server login password."""
self._power_on = False
self.vm_name = None
"""Name of the created virtual machine"""
self.network_map = None
"""Mapping of network names to networks"""
# Internal attributes
self.generic_parser = None
"""Generic parser object providing args that most subclasses will use.
Subclasses can call
``self.subparsers.add_parser(parents=[self.generic_parser])``
to automatically inherit this set of args
"""
self.parser = None
"""Subparser providing ``cot deploy PACKAGE ...`` CLI."""
self.subparsers = None
"""Subparser grouping for hypervisor-specific sub-subparsers.
Subclasses should generally have their :func:`create_subparser`
implementations create their sub-subparsers under :attr:`subparsers`
and NOT under :attr:`parent`.
"""
@property
def hypervisor(self):
"""Hypervisor to deploy to.
:raise: :exc:`InvalidInputError` if not a recognized value.
"""
return self._hypervisor
@hypervisor.setter
[docs] def hypervisor(self, value):
if value != "esxi":
raise InvalidInputError("'{0}' is not a supported hypervisor"
.format(value))
self._hypervisor = value
@property
def configuration(self):
"""VM configuration profile to use for deployment.
:raise: :exc:`InvalidInputError` if not a profile defined in the VM.
"""
return self._configuration
@configuration.setter
[docs] def configuration(self, value):
if self.vm is not None:
profiles = self.vm.get_configuration_profile_ids()
if value is not None and not (value in profiles):
raise InvalidInputError(
"'Configuration '{0}' is not a recognized "
"profile for '{1}'.\nValid options are:\n{2}"
.format(value, self.package, "\n".join(profiles)))
self._configuration = value
@property
def power_on(self):
"""Whether to automatically power on the VM after deployment."""
return self._power_on
@power_on.setter
[docs] def power_on(self, value):
if value is not True and value is not False:
raise InvalidInputError("power_on accepts boolean values only")
self._power_on = value
[docs] def ready_to_run(self):
"""Check whether the module is ready to :meth:`run`.
:returns: ``(True, ready_message)`` or ``(False, reason_why_not)``
"""
if self.hypervisor is None:
return False, "HYPERVISOR is a mandatory argument"
return super(COTDeploy, self).ready_to_run()
[docs] def create_subparser(self, parent):
"""Add subparser for the CLI of this submodule.
.. note::
Unlike most submodules, this one has subparsers of its own -
``'cot deploy PACKAGE <hypervisor>'`` so subclasses of this module
should call ``super().create_subparser(parent)`` (to create the main
'deploy' subparser) then call ``self.subparsers.add_parser()`` to add
their own sub-subparser.
:param object parent: Subparser grouping object returned by
:func:`ArgumentParser.add_subparsers`
:returns: ``('deploy', subparser)``
"""
import argparse
# Create a generic parser with arguments to be shared by all
self.generic_parser = argparse.ArgumentParser(add_help=False)
self.generic_parser.add_argument('-u', '--username',
help="Server login username")
self.generic_parser.add_argument('-p', '--password',
help="Server login password")
self.generic_parser.add_argument(
'-c', '--configuration',
help="Use the specified configuration profile defined in the OVF. "
"If unspecified and the OVF has multiple profiles, the user will "
"be prompted or the default configuration will be used.")
self.generic_parser.add_argument(
'-n', '--vm-name',
help="Name to use for the VM (if applicable) and any files "
"created. If unspecified, the name of the OVF will be used.")
self.generic_parser.add_argument(
'-P', '--power-on', action='store_true',
help="Power on the created VM to begin booting immediately.")
self.generic_parser.add_argument(
'-N', '--network-map', action='append', nargs='+',
metavar=('OVF_NET1=HOST_NET1', 'OVF_NET2=HOST_NET2'),
help="Map networks named in the OVF to networks (bridges, "
"vSwitches, etc.) in the hypervisor environment. This argument "
"may be repeated as needed to specify multiple mappings.")
# Create 'cot deploy' parser
self.parser = parent.add_parser(
'deploy',
usage=self.UI.fill_usage("deploy", [
"PACKAGE esxi ...",
]),
help="Create a new VM on the target hypervisor from the given OVF",
description="""Deploy a virtual machine to a specified server.""")
self.parser.add_argument('PACKAGE', help="OVF descriptor or OVA file")
self.subparsers = self.parser.add_subparsers(
prog="cot deploy",
dest='HYPERVISOR',
metavar='hypervisors supported:')
self.parser.set_defaults(instance=self)
return 'deploy', self.parser
[docs]class COTDeployESXi(COTDeploy):
"""Submodule for deploying VMs on ESXi and VMware vCenter/vSphere.
Inherited attributes:
:attr:`~COTGenericSubmodule.UI`,
:attr:`~COTReadOnlySubmodule.package`,
:attr:`generic_parser`,
:attr:`parser`,
:attr:`subparsers`,
:attr:`hypervisor`,
:attr:`configuration`,
:attr:`username`,
:attr:`password`,
:attr:`power_on`,
:attr:`vm_name`,
:attr:`network_map`
Attributes:
:attr:`locator`,
:attr:`datastore`,
:attr:`ovftool_args`
"""
def __init__(self, UI):
"""Instantiate this submodule with the given UI."""
super(COTDeployESXi, self).__init__(UI)
self.locator = None
"""vSphere target locator."""
self.datastore = None
"""ESXi datastore to deploy to."""
self._ovftool_args = []
@property
def ovftool_args(self):
"""List of CLI arguments to pass through to ``ovftool``."""
return list(self._ovftool_args)
@ovftool_args.setter
[docs] def ready_to_run(self):
"""Check whether the module is ready to :meth:`run`.
:returns: ``(True, ready_message)`` or ``(False, reason_why_not)``
"""
if self.locator is None:
return False, "LOCATOR is a mandatory argument"
return super(COTDeployESXi, self).ready_to_run()
[docs] def run(self):
"""Do the actual work of this submodule - deploying to ESXi.
:raises InvalidInputError: if :func:`ready_to_run` reports ``False``
"""
super(COTDeployESXi, self).run()
# ensure user provided proper credentials
server = self.locator.split("/")[0]
if self.username is None:
self.username = getpass.getuser()
if self.password is None:
self.password = self.UI.get_password(self.username, server)
target = ("vi://" + self.username + ":" + self.password +
"@" + self.locator)
ovftool_args = self.ovftool_args
configuration = self.configuration
vm = self.vm
# If locator is a vCenter locator "<vCenter>/datacenter/host/<host>"
# then environment properties will always be used.
# Otherwise we may need to help and/or warn the user:
if vm.get_property_array() and not re.search("/host/", self.locator):
if get_ovftool_version() < StrictVersion("4.0.0"):
self.UI.confirm_or_die(
"When deploying an OVF directly to a vSphere target "
"using ovftool prior to version 4.0.0, any OVF "
"environment properties will not be made available "
"to the new guest.\n"
"If your guest needs environment properties, please "
"either specify a vCenter target locator (such as "
"'<vCenter>/<datacenter>/host/<host>') "
"or upgrade to ovftool 4.0.0 or later.\n"
"Continue deployment without OVF environment?")
logger.warning("deploying directly to vSphere and ovftool "
"version is too low to add injectOvfEnv "
"option. OVF environment properties will "
"be ignored.")
elif not self.power_on:
self.UI.confirm_or_die(
"When deploying an OVF directly to a vSphere target, "
"OVF environment properties can only be made available to "
"the new guest if the guest is to be powered on "
"immediately.\n"
"If your guest needs environment properties, please "
"either specify the '--power-on'/'-P' option or provide "
"a vCenter target locator (such as "
"'<vCenter>/<datacenter>/host/<host>') "
"instead of a vSphere target.\n"
"Continue deployment without OVF environment?")
logger.warning("deploying directly to vSphere but "
"--power-on is not requested. OVF "
"environment properties will be ignored.")
else:
logger.debug("Since ovftool version is sufficient and user "
"requested --power-on, adding ovftool args to "
"ensure passthru of OVF environment to guest.")
ovftool_args.append("--X:injectOvfEnv")
# ensure configuration was specified
# will use ovf tool --deploymentOption
# if not specified and force not specified prompt for selection
profile_list = vm.get_configuration_profile_ids()
if profile_list and configuration is None:
if len(profile_list) == 1:
# No need to prompt the user
configuration = profile_list[0]
logger.debug("Auto-selected only profile '{0}'"
.format(configuration))
profile_info_string = None
while configuration is None:
if not profile_info_string:
profile_info_string = vm.profile_info_string(
self.UI.terminal_width() - 1, enumerate=True)
user_input = self.UI.get_input(
profile_info_string + "\nChoose a Configuration:", "0")
if user_input in profile_list:
configuration = user_input
else:
try:
i = int(user_input)
configuration = profile_list[i]
except (ValueError, IndexError):
# TODO this should be handled by the UI
print("\nInvalid input. Please try again.")
if configuration is not None:
ovftool_args.append("--deploymentOption=" + configuration)
# Get the number of serial ports in the OVA.
# ovftool does not create serial ports when deploying to a VM,
# so we'll have to fix this up manually later.
serial_count = vm.get_serial_count([configuration])
serial_count = serial_count[configuration]
# pass network settings on to ovftool
if self.network_map is not None:
for nm in self.network_map:
ovftool_args.append("--net:" + nm)
# check if user entered a name for the VM
if self.vm_name is not None:
ovftool_args.append("--name=" + self.vm_name)
# tell ovftool to power on the VM
# TODO - if serial port fixup (below) is implemented,
# do not power on VM until after serial ports are added.
if self.power_on:
ovftool_args.append("--powerOn")
# specify target datastore
if self.datastore is not None:
ovftool_args.append("--datastore=" + self.datastore)
# add package and target to the list
ovftool_args.append(self.package)
ovftool_args.append(target)
logger.debug("Final args to pass to OVFtool: {0}".format(ovftool_args))
# Create new list with 'ovftool' at front
cmd = ['ovftool'] + ovftool_args
# use the new list to call ovftool
logger.info("Deploying VM...")
check_call(cmd)
# Post-fix of serial ports (ovftool will not implement)
if serial_count > 0:
# TODO - fixup not implemented yet
# add serial ports as requested
# power on VM if power_on
logger.warning(
"Package '{0}' contains {1} serial ports, but ovftool "
"ignores serial port declarations. If these ports are "
"needed, you must add them manually to the new VM."
.format(self.package, serial_count))
[docs] def create_subparser(self, parent):
"""Add subparser for the CLI of this submodule.
This will create the shared :attr:`~COTDeploy.parser` under
:attr:`parent`, then create our own sub-subparser under
:attr:`~COTDeploy.subparsers`.
:param object parent: Subparser grouping object returned by
:func:`ArgumentParser.add_subparsers`
:returns: ``('deploy', subparser)``
"""
super(COTDeployESXi, self).create_subparser(parent)
import argparse
# Create 'cot deploy ... esxi' parser
p = self.subparsers.add_parser(
'esxi', parents=[self.generic_parser],
usage=self.UI.fill_usage("deploy PACKAGE esxi", [
"LOCATOR [-u USERNAME] [-p PASSWORD] [-c CONFIGURATION] "
"[-n VM_NAME] [-P] [-N OVF1=HOST1 [-N OVF2=HOST2 ...]] "
"[-d DATASTORE] [-o=OVFTOOL_ARGS]",
]),
formatter_class=argparse.RawDescriptionHelpFormatter,
help="Deploy to ESXi, vSphere, or vCenter",
description="Deploy OVF/OVA to ESXi/vCenter/vSphere hypervisor",
epilog=self.UI.fill_examples([
('cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin'
' -n test_vm',
"Deploy to vSphere/ESXi server 192.0.2.100 with credentials"
" admin/admin, creating a VM named 'test_vm' from foo.ova."),
('cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB',
"Deploy to vSphere/ESXi server 192.0.2.100, with username"
" admin (prompting the user to input a password at runtime),"
" creating a VM based on profile '1CPU-2.5GB' in foo.ova."),
('cot deploy foo.ova esxi "192.0.2.100/mydc/host/192.0.2.1"'
' -u administrator -N "GigabitEthernet1=VM Network"'
' -N "GigabitEthernet2=myvswitch"',
"Deploy to vSphere server 192.0.2.1 which belongs to"
" datacenter 'mydc' on vCenter server 192.0.2.100, and map"
" the two NIC networks to vSwitches. Note that in this case"
" -u specifies the vCenter login username."),
('cot deploy foo.ova esxi 192.0.2.100 -u admin -p password'
' --ovftool-args="--overwrite --acceptAllEulas"',
"Deploy with passthrough arguments to ovftool.")
]))
# ovftool uses '-ds' as shorthand for '--datastore', so let's allow it.
p.add_argument("-d", "-ds", "--datastore",
help="ESXi datastore to use for the new VM")
p.add_argument("-o", "--ovftool-args",
help="Quoted string describing additional CLI "
"""parameters to pass through to "ovftool". Examples:"""
""" -o="--foo", --ovftool-args="--foo --bar" """)
p.add_argument("LOCATOR",
help="vSphere target locator. Examples: "
'"192.0.2.100" (deploy directly to ESXi server), '
'"192.0.2.101/mydatacenter/host/192.0.2.100" '
'(deploy via vCenter server)')
p.set_defaults(instance=self)
return 'deploy', p