Merge pull request #29499 from rallytime/esxi-proxy

Initial commit of ESXi Proxy Minion
This commit is contained in:
Mike Place 2015-12-08 14:10:13 -07:00
commit 3ae096b7ac
8 changed files with 4554 additions and 1 deletions

View file

@ -10,6 +10,7 @@ Full list of builtin proxy modules
:toctree:
:template: autosummary.rst.tmpl
esxi
fx2
junos
rest_sample

View file

@ -0,0 +1,6 @@
===============
salt.proxy.esxi
===============
.. automodule:: salt.proxy.esxi
:members:

93
salt/grains/esxi.py Normal file
View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
'''
Generate baseline proxy minion grains for ESXi hosts.
., versionadded:: 2015.8.4
'''
# Import Python Libs
from __future__ import absolute_import
import logging
# Import Salt Libs
from salt.exceptions import SaltSystemExit
import salt.utils
import salt.modules.vsphere
__proxyenabled__ = ['esxi']
__virtualname__ = 'esxi'
log = logging.getLogger(__file__)
GRAINS_CACHE = {}
def __virtual__():
if not salt.utils.is_proxy():
return False
else:
return __virtualname__
def esxi():
return _grains()
def kernel():
return {'kernel': 'proxy'}
def os():
if not GRAINS_CACHE:
GRAINS_CACHE.update(_grains())
try:
return {'os': GRAINS_CACHE.get('fullName')}
except AttributeError:
return {'os': 'Unknown'}
def os_family():
return {'os_family': 'proxy'}
def _find_credentials(host):
'''
Cycle through all the possible credentials and return the first one that
works.
'''
user_names = [__pillar__['proxy'].get('username', 'root')]
passwords = __pillar__['proxy']['passwords']
for user in user_names:
for password in passwords:
try:
# Try to authenticate with the given user/password combination
ret = salt.modules.vsphere.system_info(host=host,
username=user,
password=password)
except SaltSystemExit:
# If we can't authenticate, continue on to try the next password.
continue
# If we have data returned from above, we've successfully authenticated.
if ret:
return user, password
# We've reached the end of the list without successfully authenticating.
raise SaltSystemExit('Cannot complete login due to an incorrect user name or password.')
def _grains():
'''
Get the grains from the proxied device.
'''
host = __pillar__['proxy']['host']
username, password = _find_credentials(host)
protocol = __pillar__['proxy'].get('protocol')
port = __pillar__['proxy'].get('port')
ret = salt.modules.vsphere.system_info(host=host,
username=username,
password=password,
protocol=protocol,
port=port)
GRAINS_CACHE.update(ret)
return GRAINS_CACHE

72
salt/modules/esxi.py Normal file
View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
'''
Glues the VMware vSphere Execution Module to the VMware ESXi Proxy Minions to the
:doc:`esxi proxymodule </ref/proxy/all/salt.proxy.esxi>`.
.. versionadded:: 2015.8.4
Depends: :doc:`vSphere Remote Execution Module (salt.modules.vsphere)
</ref/modules/all/salt.modules.vsphere>`
For documentation on commands that you can direct to an ESXi host via proxy,
look in the documentation for :doc:`salt.modules.vsphere
</ref/modules/all/salt.modules.vsphere>`.
This execution module calls through to a function in the ESXi proxy module
called ``ch_config``, which looks up the function passed in the ``command``
parameter in :doc:`salt.modules.vsphere </ref/modules/all/salt.modules.vsphere>`
and calls it.
To execute commands with an ESXi Proxy Minion using the vSphere Execution Module,
use the ``esxi.cmd <vsphere-function-name>`` syntax. Both args and kwargs needed
for various vsphere execution module functions must be passed through in a kwarg-
type manor.
.. code-block:: bash
salt 'esxi-proxy' esxi.cmd system_info
salt 'exsi-proxy' esxi.cmd get_service_policy service_name='ssh'
'''
from __future__ import absolute_import
# Import python libs
import logging
import salt.utils
log = logging.getLogger(__name__)
__proxyenabled__ = ['esxi']
__virtualname__ = 'esxi'
def __virtual__():
'''
Only work on proxy
'''
if salt.utils.is_proxy():
return __virtualname__
return False
def cmd(command, *args, **kwargs):
proxy_prefix = __opts__['proxy']['proxytype']
proxy_cmd = proxy_prefix + '.ch_config'
host = __pillar__['proxy']['host']
username, password = __proxy__[proxy_prefix + '.find_credentials'](host)
kwargs['host'] = host
kwargs['username'] = username
kwargs['password'] = password
protocol = __pillar__['proxy'].get('protocol')
if protocol:
kwargs['protocol'] = protocol
port = __pillar__['proxy'].get('port')
if port:
kwargs['port'] = port
return __proxy__[proxy_cmd](command, *args, **kwargs)

3014
salt/modules/vsphere.py Normal file

File diff suppressed because it is too large Load diff

380
salt/proxy/esxi.py Normal file
View file

@ -0,0 +1,380 @@
# -*- coding: utf-8 -*-
'''
Proxy Minion interface module for managing VMWare ESXi hosts.
.. versionadded:: 2015.8.4
:depends: pyVmomi
**Special Note: SaltStack thanks** `Adobe Corporation <http://adobe.com/>`_
**for their support in creating this Proxy Minion integration.**
This proxy minion enables VMware ESXi (hereafter referred to as simply 'ESXi')
hosts to be treated individually like a Salt Minion.
Since the ESXi host may not necessarily run on an OS capable of hosting a
Python stack, the ESXi host can't run a Salt Minion directly. Salt's
"Proxy Minion" functionality enables you to designate another machine to host
a minion process that "proxies" communication from the Salt Master. The master
does not know nor care that the target is not a "real" Salt Minion.
More in-depth conceptual reading on Proxy Minions can be found in the
:doc:`Proxy Minion </topics/proxyminion/index>` section of Salt's
documentation.
Configuration
=============
To use this integration proxy module, please configure the following:
Pillar
------
Proxy minions get their configuration from Salt's Pillar. Every proxy must
have a stanza in Pillar and a reference in the Pillar top-file that matches
the ID. At a minimum for communication with the ESXi host, the pillar should
look like this:
.. code-block:: yaml
proxy:
proxytype: esxi
host: <ip or dns name of esxi host>
username: <ESXi username>
passwords:
- first_password
- second_password
- third_password
proxytype
^^^^^^^^^
The ``proxytype`` key and value pair is critical, as it tells Salt which
interface to load from the ``proxy`` directory in Salt's install hierarchy,
or from ``/srv/salt/_proxy`` on the Salt Master (if you have created your
own proxy module, for example). To use this ESXi Proxy Module, set this to
``esxi``.
host
^^^^
The location, or ip/dns, of the ESXi host. Required.
username
^^^^^^^^
The username used to login to the ESXi host, such as ``root``. Required.
passwords
^^^^^^^^^
A list of passwords to be used to try and login to the ESXi host. At least
one password in this list is required.
The proxy integration will try the passwords listed in order. It is
configured this way so you can have a regular password and the password you
may be updating for an ESXi host either via the
:doc:`vsphere.update_host_password </ref/modules/all/salt.modules.vsphere>`
function or via an :doc:`ESXi state </ref/modules/all/salt.states.esxi>`
function. This way, after the password is changed, you should not need to
restart the proxy minion--it should just pick up the the new password provided in
the list. You can then change pillar at will to move that password to the
front and retire the unused ones.
This also allows you to use any number of potential fallback passwords.
.. note::
When a password is changed on the host to one in the list of possible
passwords, the further down on the list the password is, the longer
individual commands will take to return. This is due to the nature of
pyVmomi's login system. We have to wait for the first attempt to fail
before trying the next password on the list.
This scenario is especially true, and even slower, when the proxy
minion first starts. If the correct password is not the first password
on the list, it may take up to a minute for ``test.ping`` to respond
with a ``True`` result. Once the initial authorization is complete, the
responses for commands will be faster.
To avoid these longer waiting periods, SaltStack recommends moving
correct password to the top of the list and restarting the proxy minion
at your earliest convenience.
protocol
^^^^^^^^
If the ESXi host is not using the default protocol, set this value to an
alternate protocol. Default is ``https``.
port
^^^^
If the ESXi host is not using the default port, set this value to an
alternate port. Default is ``443``.
Salt Proxy
----------
After your pillar is in place, you can test the proxy. The proxy can run on
any machine that has network connectivity to your Salt Master and to the
ESXi host in question. SaltStack recommends that the machine running the
salt-proxy process also run a regular minion, though it is not strictly
necessary.
On the machine that will run the proxy, make sure there is an ``/etc/salt/proxy``
file with at least the following in it:
.. code-block:: yaml
master: <ip or hostname of salt-master>
You can then start the salt-proxy process with:
.. code-block:: bash
salt-proxy --proxyid <id you want to give the host>
You may want to add ``-l debug`` to run the above in the foreground in
debug mode just to make sure everything is OK.
Next, accept the key for the proxy on your salt-master, just like you
would for a regular minion:
.. code-block:: bash
salt-key -a <id you gave the esxi host>
You can confirm that the pillar data is in place for the proxy:
.. code-block:: bash
salt <id> pillar.items
And now you should be able to ping the ESXi host to make sure it is
responding:
.. code-block:: bash
salt <id> test.ping
At this point you can execute one-off commands against the host. For
example, you can get the ESXi host's system information:
.. code-block:: bash
salt <id> esxi.cmd system_info
Note that you don't need to provide credentials or an ip/hostname. Salt
knows to use the credentials you stored in Pillar.
It's important to understand how this particular proxy works.
:doc:`Salt.modules.vsphere </ref/modules/all/salt.modules.vsphere>` is a
standard Salt execution module. If you pull up the docs for it you'll see
that almost every function in the module takes credentials and a target
host. When credentials and a host aren't passed, Salt runs commands
through ``pyVmomi``against the local machine. If you wanted, you could run
functions from this module on any host where an appropriate version of
``pyVmomi`` is installed, and that host would reach out over the network
and communicate with the ESXi host.
``esxi.cmd`` acts as a "shim" between the execution module and the proxy. Its
first parameter is always the function from salt.modules.vsphere. If the
function takes more positional or keyword arguments you can append them to the
call. It's this shim that speaks to the ESXi host through the proxy, arranging
for the credentials and hostname to be pulled from the Pillar section for this
Proxy Minion.
Because of the presence of the shim, to lookup documentation for what
functions you can use to interface with the ESXi host, you'll want to
look in :doc:`salt.modules.vsphere </ref/modules/all/salt.modules.vsphere>`
instead of :doc:`salt.modules.esxi </ref/modules/all/salt.modules.esxi>`.
States
------
Associated states are thoroughly documented in
:doc:`salt.states.vsphere </ref/states/all/salt.states.vsphere>`. Look there
to find an example structure for Pillar as well as an example ``.sls`` file f
or standing up an ESXi host from scratch.
'''
# Import Python Libs
from __future__ import absolute_import
import logging
# Import Salt Libs
from salt.exceptions import SaltSystemExit
# This must be present or the Salt loader won't load this module.
__proxyenabled__ = ['esxi']
# Variables are scoped to this module so we can have persistent data
# across calls to fns in here.
GRAINS_CACHE = {}
DETAILS = {}
# Set up logging
log = logging.getLogger(__file__)
# Define the module's virtual name
__virtualname__ = 'esxi'
def __virtual__():
'''
Only load if the ESXi execution module is available.
'''
if 'vsphere.system_info' in __salt__:
return __virtualname__
return False, 'The ESXi Proxy Minion module did not load.'
def init(opts):
'''
This function gets called when the proxy starts up. For
ESXi devices, the host, login credentials, and, if configured,
the protocol and port are cached.
'''
if 'host' not in opts['proxy']:
log.critical('No \'host\' key found in pillar for this proxy.')
return False
if 'username' not in opts['proxy']:
log.critical('No \'username\' key found in pillar for this proxy.')
return False
if 'passwords' not in opts['proxy']:
log.critical('No \'passwords\' key found in pillar for this proxy.')
host = opts['proxy']['host']
# Get the correct login details
try:
username, password = find_credentials(host)
except SaltSystemExit as err:
log.critical('Error: {0}'.format(err))
return False
# Set configuration details
DETAILS['host'] = host
DETAILS['username'] = username
DETAILS['password'] = password
DETAILS['protocol'] = opts['proxy'].get('protocol')
DETAILS['port'] = opts['proxy'].get('port')
def grains():
'''
Get the grains from the proxy device.
'''
if not GRAINS_CACHE:
return _grains(DETAILS['host'],
DETAILS['protocol'],
DETAILS['port'])
return GRAINS_CACHE
def grains_refresh():
'''
Refresh the grains from the proxy device.
'''
GRAINS_CACHE = {}
return grains()
def ping():
'''
Check to see if the host is responding. Returns False if the host didn't
respond, True otherwise.
CLI Example::
.. code-block:: bash
salt esxi-host test.ping
'''
find_credentials(DETAILS['host'])
try:
__salt__['vsphere.system_info'](host=DETAILS['host'],
username=DETAILS['username'],
password=DETAILS['password'])
except SaltSystemExit as err:
log.warning(err)
return False
return True
def shutdown():
'''
Shutdown the connection to the proxy device. For this proxy,
shutdown is a no-op.
'''
log.debug('ESXi proxy shutdown() called...')
def ch_config(cmd, *args, **kwargs):
'''
This function is called by the
:doc:`salt.modules.esxi.cmd </ref/modules/all/salt.modules.esxi>` shim.
It then calls whatever is passed in ``cmd`` inside the
:doc:`salt.modules.vsphere </ref/modules/all/salt.modules.vsphere>` module.
Passes the return through from the vsphere module.
cmd
The command to call inside salt.modules.vsphere
args
Arguments that need to be passed to that command.
kwargs
Keyword arguments that need to be passed to that command.
'''
# Strip the __pub_ keys...is there a better way to do this?
for k in kwargs.keys():
if k.startswith('__pub_'):
kwargs.pop(k)
if 'vsphere.' + cmd not in __salt__:
return {'retcode': -1, 'message': 'vsphere.' + cmd + ' is not available.'}
else:
return __salt__['vsphere.' + cmd](*args, **kwargs)
def find_credentials(host):
'''
Cycle through all the possible credentials and return the first one that
works.
'''
user_names = [__pillar__['proxy'].get('username', 'root')]
passwords = __pillar__['proxy']['passwords']
for user in user_names:
for password in passwords:
try:
# Try to authenticate with the given user/password combination
ret = __salt__['vsphere.system_info'](host=host,
username=user,
password=password)
except SaltSystemExit:
# If we can't authenticate, continue on to try the next password.
continue
# If we have data returned from above, we've successfully authenticated.
if ret:
DETAILS['username'] = user
DETAILS['password'] = password
return user, password
# We've reached the end of the list without successfully authenticating.
raise SaltSystemExit('Cannot complete login due to an incorrect user name or password.')
def _grains(host, protocol=None, port=None):
'''
Helper function to the grains from the proxied device.
'''
username, password = find_credentials(DETAILS['host'])
ret = __salt__['vsphere.system_info'](host=host,
username=username,
password=password,
protocol=protocol,
port=port)
GRAINS_CACHE.update(ret)
return GRAINS_CACHE

935
salt/states/esxi.py Normal file
View file

@ -0,0 +1,935 @@
# -*- coding: utf-8 -*-
'''
Manage VMware ESXi Hosts.
.. versionadded:: 2015.8.4
Dependencies
~~~~~~~~~~~~
- pyVmomi Python Module
- ESXCLI
.. note::
Be aware that some functionality in this state module may depend on the
type of license attached to the ESXi host.
For example, certain services are only available to manipulate service state
or policies with a VMware vSphere Enterprise or Enterprise Plus license, while
others are available with a Standard license. The ``ntpd`` service is restricted
to an Enterprise Plus license, while ``ssh`` is available via the Standard
license.
Please see the `vSphere Comparison`_ page for more information.
.. _vSphere Comparison: https://www.vmware.com/products/vsphere/compare
'''
# Import Python Libs
from __future__ import absolute_import
import logging
# Import Salt Libs
import salt.utils
import salt.ext.six as six
from salt.exceptions import CommandExecutionError
# Get Logging Started
log = logging.getLogger(__name__)
def __virtual__():
return 'esxi.cmd' in __salt__
def coredump_configured(name, enabled, dump_ip, host_vnic='vmk0', dump_port=6500):
'''
Ensures a host's core dump configuration.
name
Name of the state.
enabled
Sets whether or not ESXi core dump collection should be enabled.
This is a boolean value set to ``True`` or ``False`` to enable
or disable core dumps.
Note that ESXi requires that the core dump must be enabled before
any other parameters may be set. This also affects the ``changes``
results in the state return dictionary. If ``enabled`` is ``False``,
we can't obtain any previous settings to compare other state variables,
resulting in many ``old`` references returning ``None``.
Once ``enabled`` is ``True`` the ``changes`` dictionary comparisons
will be more accurate. This is due to the way the system coredemp
network configuration command returns data.
dump_ip
The IP address of host that will accept the dump.
host_vnic
Host VNic port through which to communicate. Defaults to ``vmk0``.
dump_port
TCP port to use for the dump. Defaults to ``6500``.
Example:
.. code-block:: yaml
configure-host-coredump:
esxi.coredump_configured:
- enabled: True
- dump_ip: 'my-coredump-ip.example.com`
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
enabled_msg = 'ESXi requires that the core dump must be enabled ' \
'before any other parameters may be set.'
current_config = __salt__[esxi_cmd]('get_coredump_network_config')
if isinstance(current_config, six.string_types):
ret['comment'] = 'Error: {0}'.format(current_config)
return ret
elif ret.get('stderr'):
ret['comment'] = 'Error: {0}'.format(ret.get('stderr'))
return ret
else:
current_enabled = current_config.get('enabled')
if current_enabled != enabled:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('coredump_network_enable',
enabled=enabled)
if response['retcode'] != 0:
ret['comment'] = 'Error: {0}'.format(ret['stderr'])
return ret
# Allow users to disable core dump, but then return since
# nothing else can be set if core dump is disabled.
if not enabled:
ret['result'] = True
ret['comment'] = enabled_msg
return ret
ret['changes'].update({'enabled':
{'old': current_enabled,
'new': enabled}})
elif not enabled:
# If current_enabled and enabled match, but are both False,
# We must return before configuring anything. This isn't a
# failure as core dump may be disabled intentionally.
ret['result'] = True
ret['comment'] = enabled_msg
return ret
changes = False
current_ip = current_config.get('ip')
if current_ip != dump_ip:
ret['changes'].update({'dump_ip':
{'old': current_ip,
'new': dump_ip}})
changes = True
current_vnic = current_config.get('host_vnic')
if current_vnic != host_vnic:
ret['changes'].update({'host_vnic':
{'old': current_vnic,
'new': host_vnic}})
changes = True
current_port = current_config.get('port')
if current_port != str(dump_port):
ret['changes'].update({'dump_port':
{'old': current_port,
'new': str(dump_port)}})
changes = True
# Only run the command if not using test=True and changes were detected.
if not __opts__['test'] and changes is True:
response = __salt__[esxi_cmd]('set_coredump_network_config',
dump_ip=dump_ip,
host_vnic=host_vnic,
dump_port=dump_port)
if response.get('success') is False:
msg = response.get('stderr')
if not msg:
msg = response.get('stdout')
ret['comment'] = 'Error: {0}'.format(msg)
return ret
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'Core Dump configuration is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Core dump configuration will change.'
return ret
def password_present(name, password):
'''
Ensures the given password is set on the ESXi host. Passwords cannot be obtained from
host, so if a password is set in this state, the ``vsphere.update_host_password``
function will always run (except when using test=True functionality) and the state's
changes dictionary will always be populated.
The username for which the password will change is the same username that is used to
authenticate against the ESXi host via the Proxy Minion. For example, if the pillar
definition for the proxy username is defined as ``root``, then the username that the
password will be updated for via this state is ``root``.
name
Name of the state.
password
The new password to change on the host.
Example:
.. code-block:: yaml
configure-host-password:
esxi.password_present:
- password: 'new-bad-password'
'''
ret = {'name': name,
'result': True,
'changes': {'old': 'unknown',
'new': '********'},
'comment': 'Host password was updated.'}
esxi_cmd = 'esxi.cmd'
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Host password will change.'
return ret
else:
try:
__salt__[esxi_cmd]('update_host_password',
new_password=password)
except CommandExecutionError as err:
ret['result'] = False
ret['comment'] = 'Error: {0}'.format(err)
return ret
return ret
def ntp_configured(name,
service_running,
ntp_servers=None,
service_policy=None,
service_restart=False,
update_datetime=False):
'''
Ensures a host's NTP server configuration such as setting NTP servers, ensuring the
NTP daemon is running or stopped, or restarting the NTP daemon for the ESXi host.
name
Name of the state.
service_running
Ensures the running state of the ntp deamon for the host. Boolean value where
``True`` indicates that ntpd should be running and ``False`` indicates that it
should be stopped.
ntp_servers
A list of servers that should be added to the ESXi host's NTP configuration.
service_policy
The policy to set for the NTP service.
service_restart
If set to ``True``, the ntp daemon will be restarted, regardless of its previous
running state. Default is ``False``.
update_datetime
If set to ``True``, the date/time on the given host will be updated to UTC.
Default setting is ``False``. This option should be used with caution since
network delays and execution delays can result in time skews.
Example:
.. code-block:: yaml
configure-host-ntp:
esxi.ntp_configured:
- service_running: True
- ntp_servers:
- 192.174.1.100
- 192.174.1.200
- service_policy: 'automatic'
- service_restart: True
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
host = __pillar__['proxy']['host']
ntpd = 'ntpd'
ntp_config = __salt__[esxi_cmd]('get_ntp_config').get(host)
ntp_running = __salt__[esxi_cmd]('get_service_running',
service_name=ntpd).get(host)
error = ntp_running.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ntp_running = ntp_running.get(ntpd)
# Configure NTP Servers for the Host
if ntp_servers and set(ntp_servers) != set(ntp_config):
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('set_ntp_config',
ntp_servers=ntp_servers).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
# Set changes dictionary for ntp_servers
ret['changes'].update({'ntp_servers':
{'old': ntp_config,
'new': ntp_servers}})
# Configure service_running state
if service_running != ntp_running:
# Only run the command if not using test=True
if not __opts__['test']:
# Start ntdp if service_running=True
if ntp_running is True:
response = __salt__[esxi_cmd]('service_start',
service_name=ntpd).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
# Stop ntpd if service_running=False
else:
response = __salt__[esxi_cmd]('service_stop',
service_name=ntpd).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_running':
{'old': ntp_running,
'new': service_running}})
# Configure service_policy
if service_policy:
current_service_policy = __salt__[esxi_cmd]('get_service_policy',
service_name=ntpd).get(host)
error = current_service_policy.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
current_service_policy = current_service_policy.get(ntpd)
if service_policy != current_service_policy:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('set_service_policy',
service_name=ntpd,
service_policy=service_policy).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_policy':
{'old': current_service_policy,
'new': service_policy}})
# Update datetime, if requested.
if update_datetime:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('update_host_datetime').get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'update_datetime':
{'old': '',
'new': 'Host datetime was updated.'}})
# Restart ntp_service if service_restart=True
if service_restart:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('service_restart',
service_name=ntpd).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_restart':
{'old': ntp_running,
'new': 'NTP Deamon Restarted.'}})
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'NTP is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'NTP state will change.'
return ret
def vmotion_configured(name, enabled, device='vmk0'):
'''
Configures a host's VMotion properties such as enabling VMotion and setting
the device VirtualNic that VMotion will use.
name
Name of the state.
enabled
Ensures whether or not VMotion should be enabled on a host as a boolean
value where ``True`` indicates that VSAN should be enabled and ``False``
indicates that VMotion should be disabled.
device
The device that uniquely identifies the VirtualNic that will be used for
VMotion for the host. Defaults to ``vmk0``.
Example:
.. code-block:: yaml
configure-vmotion:
esxi.vmotion_configured:
- enabled: True
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
host = __pillar__['proxy']['host']
current_vmotion_enabled = __salt__[esxi_cmd]('get_vmotion_enabled').get(host)
current_vmotion_enabled = current_vmotion_enabled.get('VMotion Enabled')
# Configure VMotion Enabled state, if changed.
if enabled != current_vmotion_enabled:
# Only run the command if not using test=True
if not __opts__['test']:
# Enable VMotion if enabled=True
if enabled is True:
response = __salt__[esxi_cmd]('vmotion_enable',
device=device).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
# Disable VMotion if enabled=False
else:
response = __salt__[esxi_cmd]('vmotion_disable').get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'enabled':
{'old': current_vmotion_enabled,
'new': enabled}})
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'VMotion configuration is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'VMotion configuration will change.'
return ret
def vsan_configured(name, enabled, add_disks_to_vsan=False):
'''
Configures a host's VSAN properties such as enabling or disabling VSAN, or
adding VSAN-eligible disks to the VSAN system for the host.
name
Name of the state.
enabled
Ensures whether or not VSAN should be enabled on a host as a boolean
value where ``True`` indicates that VSAN should be enabled and ``False``
indicates that VSAN should be disabled.
add_disks_to_vsan
If set to ``True``, any VSAN-eligible disks for the given host will be added
to the host's VSAN system. Default is ``False``.
Example:
.. code-block:: yaml
configure-host-vsan:
esxi.vsan_configured:
- enabled: True
- add_disks_to_vsan: True
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
host = __pillar__['proxy']['host']
current_vsan_enabled = __salt__[esxi_cmd]('get_vsan_enabled').get(host)
error = current_vsan_enabled.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
current_vsan_enabled = current_vsan_enabled.get('VSAN Enabled')
# Configure VSAN Enabled state, if changed.
if enabled != current_vsan_enabled:
# Only run the command if not using test=True
if not __opts__['test']:
# Enable VSAN if enabled=True
if enabled is True:
response = __salt__[esxi_cmd]('vsan_enable').get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
# Disable VSAN if enabled=False
else:
response = __salt__[esxi_cmd]('vsan_disable').get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'enabled':
{'old': current_vsan_enabled,
'new': enabled}})
# Add any eligible disks to VSAN, if requested.
if add_disks_to_vsan:
current_eligible_disks = __salt__[esxi_cmd]('get_vsan_eligible_disks').get(host)
error = current_eligible_disks.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
disks = current_eligible_disks.get('Eligible')
if disks and isinstance(disks, list):
ret['changes'].update({'add_disks_to_vsan':
{'old': '',
'new': disks}})
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('vsan_add_disks').get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'VSAN configuration is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'VSAN configuration will change.'
return ret
def ssh_configured(name,
service_running,
ssh_key=None,
ssh_key_file=None,
service_policy=None,
service_restart=False,
certificate_verify=False):
'''
Manage the SSH configuration for a host including whether or not SSH is running or
the presence of a given SSH key. Note: Only one ssh key can be uploaded for root.
Uploading a second key will replace any existing key.
name
Name of the state.
service_running
Ensures whether or not the SSH service should be running on a host. Represented
as a boolean value where ``True`` indicates that SSH should be running and
``False`` indicates that SSH should stopped.
In order to update SSH keys, the SSH service must be running.
ssh_key
Public SSH key to added to the authorized_keys file on the ESXi host. You can
use ``ssh_key`` or ``ssh_key_file``, but not both.
ssh_key_file
File containing the public SSH key to be added to the authorized_keys file on
the ESXi host. You can use ``ssh_key_file`` or ``ssh_key``, but not both.
service_policy
The policy to set for the NTP service.
service_restart
If set to ``True``, the SSH service will be restarted, regardless of its
previous running state. Default is ``False``.
certificate_verify
If set to ``True``, the SSL connection must present a valid certificate.
Default is ``False``.
Example:
.. code-block:: yaml
configure-host-ssh:
esxi.ssh_configured:
- service_running: True
- ssh_key_file: /etc/salt/ssh_keys/my_key.pub
- service_policy: 'automatic'
- service_restart: True
- certificate_verify: True
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
host = __pillar__['proxy']['host']
ssh = 'ssh'
ssh_running = __salt__[esxi_cmd]('get_service_running',
service_name=ssh).get(host)
error = ssh_running.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ssh_running = ssh_running.get(ssh)
# Configure SSH service_running state, if changed.
if service_running != ssh_running:
# Only actually run the command if not using test=True
if not __opts__['test']:
# Start SSH if service_running=True
if service_running is True:
enable = __salt__[esxi_cmd]('service_start',
service_name=ssh).get(host)
error = enable.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
# Disable SSH if service_running=False
else:
disable = __salt__[esxi_cmd]('service_stop',
service_name=ssh).get(host)
error = disable.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_running':
{'old': ssh_running,
'new': service_running}})
# If uploading an SSH key or SSH key file, see if there's a current
# SSH key and compare the current key to the key set in the state.
current_ssh_key, ssh_key_changed = None, False
if ssh_key or ssh_key_file:
current_ssh_key = __salt__[esxi_cmd]('get_ssh_key',
certificate_verify=certificate_verify)
error = current_ssh_key.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
current_ssh_key = current_ssh_key.get('key')
if current_ssh_key:
clean_current_key = _strip_key(current_ssh_key).split(' ')
if not ssh_key:
ssh_key = ''
# Open ssh key file and read in contents to create one key string
with salt.utils.fopen(ssh_key_file, 'r') as key_file:
for line in key_file:
if line.startswith('#'):
# Commented line
continue
ssh_key = ssh_key + line
clean_ssh_key = _strip_key(ssh_key).split(' ')
# Check that the first two list items of clean key lists are equal.
if clean_current_key[0] != clean_ssh_key[0] or clean_current_key[1] != clean_ssh_key[1]:
ssh_key_changed = True
# Upload SSH key, if changed.
if ssh_key_changed:
if not __opts__['test']:
# Upload key
response = __salt__[esxi_cmd]('upload_ssh_key',
ssh_key=ssh_key,
ssh_key_file=ssh_key_file,
certificate_verify=certificate_verify)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'SSH Key':
{'old': current_ssh_key,
'new': ssh_key if ssh_key else ssh_key_file}})
# Configure service_policy
if service_policy:
current_service_policy = __salt__[esxi_cmd]('get_service_policy',
service_name=ssh).get(host)
error = current_service_policy.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
current_service_policy = current_service_policy.get(ssh)
if service_policy != current_service_policy:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('set_service_policy',
service_name=ssh,
service_policy=service_policy).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_policy':
{'old': current_service_policy,
'new': service_policy}})
# Restart ssh_service if service_restart=True
if service_restart:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('service_restart',
service_name=ssh).get(host)
error = response.get('Error')
if error:
ret['comment'] = 'Error: {0}'.format(error)
return ret
ret['changes'].update({'service_restart':
{'old': ssh_running,
'new': 'SSH service restarted.'}})
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'SSH service is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'SSH service state will change.'
return ret
def syslog_configured(name,
syslog_configs,
firewall=True,
reset_service=True,
reset_syslog_config=False,
reset_configs=None):
'''
Ensures the specified syslog configuration parameters. By default,
this state will reset the syslog service after any new or changed
parameters are set successfully.
name
Name of the state.
syslog_configs
Name of parameter to set (corresponds to the command line switch for
esxcli without the double dashes (--))
Valid syslog_config values are ``logdir``, ``loghost``, ``logdir-unique``,
``default-rotate``, ``default-size``, and ``default-timeout``.
Each syslog_config option also needs a configuration value to set.
For example, ``loghost`` requires URLs or IP addresses to use for
logging. Multiple log servers can be specified by listing them,
comma-separated, but without spaces before or after commas
(reference: https://blogs.vmware.com/vsphere/2012/04/configuring-multiple-syslog-servers-for-esxi-5.html)
firewall
Enable the firewall rule set for syslog. Defaults to ``True``.
reset_service
After a successful parameter set, reset the service. Defaults to ``True``.
reset_syslog_config
Resets the syslog service to it's default settings. Defaults to ``False``.
If set to ``True``, default settings defined by the list of syslog configs
in ``reset_configs`` will be reset before running any other syslog settings.
reset_configs
A comma-delimited list of parameters to reset. Only runs if
``reset_syslog_config`` is set to ``True``. If ``reset_syslog_config`` is set
to ``True``, but no syslog configs are listed in ``reset_configs``, then
``reset_configs`` will be set to ``all`` by default.
See ``syslog_configs`` parameter above for a list of valid options.
Example:
.. code-block:: yaml
configure-host-syslog:
esxi.syslog_configured:
- syslog_configs:
loghost: ssl://localhost:5432,tcp://10.1.0.1:1514
default-timeout: 120
- firewall: True
- reset_service: True
- reset_syslog_config: True
- reset_configs: loghost,default-timeout
'''
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
esxi_cmd = 'esxi.cmd'
if reset_syslog_config:
if not reset_configs:
reset_configs = 'all'
# Only run the command if not using test=True
if not __opts__['test']:
reset = __salt__[esxi_cmd]('reset_syslog_config',
syslog_config=reset_configs)
for key, val in reset.iteritems():
if isinstance(val, bool):
continue
if not val.get('success'):
msg = val.get('message')
if not msg:
msg = 'There was an error resetting a syslog config \'{0}\'.' \
'Please check debug logs.'.format(val)
ret['comment'] = 'Error: {0}'.format(msg)
return ret
ret['changes'].update({'reset_syslog_config':
{'old': '',
'new': reset_configs}})
current_firewall = __salt__[esxi_cmd]('get_firewall_status')
if not current_firewall.get('success'):
ret['comment'] = 'There was an error obtaining firewall statuses. ' \
'Please check debug logs.'
return ret
current_firewall = current_firewall.get('rulesets').get('syslog')
if current_firewall != firewall:
# Only run the command if not using test=True
if not __opts__['test']:
enabled = __salt__[esxi_cmd]('enable_firewall_ruleset',
ruleset_enable=firewall,
ruleset_name='syslog')
if enabled.get('retcode') != 0:
err = enabled.get('stderr')
out = enabled.get('stdout')
ret['comment'] = 'Error: {0}'.format(err if err else out)
return ret
ret['changes'].update({'firewall':
{'old': current_firewall,
'new': firewall}})
current_syslog_config = __salt__[esxi_cmd]('get_syslog_config')
for key, val in syslog_configs.iteritems():
# The output of get_syslog_config has different keys than the keys
# Used to set syslog_config values. We need to look them up first.
try:
lookup_key = _lookup_syslog_config(key)
except KeyError:
ret['comment'] = '\'{0}\' is not a valid config variable.'.format(key)
return ret
current_val = current_syslog_config[lookup_key]
if current_val != val:
# Only run the command if not using test=True
if not __opts__['test']:
response = __salt__[esxi_cmd]('set_syslog_config',
syslog_config=key,
config_value=val,
firewall=firewall,
reset_service=reset_service)
success = response.get('success')
if not success:
msg = response.get('message')
if not msg:
msg = 'There was an error setting syslog config \'{0}\'. ' \
'Please check debug logs.'.format(key)
ret['comment'] = msg
return ret
if not ret['changes'].get('syslog_config'):
ret['changes'].update({'syslog_config': {}})
ret['changes']['syslog_config'].update({key:
{'old': current_val,
'new': val}})
ret['result'] = True
if ret['changes'] == {}:
ret['comment'] = 'Syslog is already in the desired state.'
return ret
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Syslog state will change.'
return ret
def _lookup_syslog_config(config):
'''
Helper function that looks up syslog_config keys available from
``vsphere.get_syslog_config``.
'''
lookup = {'default-timeout': 'Default Network Retry Timeout',
'logdir': 'Local Log Output',
'default-size': 'Local Logging Default Rotation Size',
'logdir-unique': 'Log To Unique Subdirectory',
'default-rotate': 'Local Logging Default Rotations',
'loghost': 'Remote Host'}
return lookup.get(config)
def _strip_key(key_string):
'''
Strips an SSH key string of white space and line endings and returns the new string.
key_string
The string to be stripped.
'''
key_string.strip()
key_string.replace('\n', '')
key_string.replace('\r\n', '')
return key_string

View file

@ -7,7 +7,13 @@ Connection library for VMWare
This is a base library used by a number of VMWare services such as VMWare
ESX, ESXi, and vCenter servers.
:depends: pyVmomi Python Module
Dependencies
~~~~~~~~~~~~
- pyVmomi Python Module
- ESXCLI: This dependency is only needed to use the ``esxcli`` function. No other
functions in this module rely on ESXCLI.
'''
# Import Python Libs
@ -18,6 +24,8 @@ import time
# Import Salt Libs
from salt.exceptions import SaltSystemExit
import salt.modules.cmdmod
import salt.utils
# Import Third Party Libs
@ -42,6 +50,50 @@ def __virtual__():
return False, 'Missing dependency: The salt.utils.vmware module requires pyVmomi.'
def esxcli(host, user, pwd, cmd, protocol=None, port=None, esxi_host=None):
'''
Shell out and call the specified esxcli commmand, parse the result
and return something sane.
:param host: ESXi or vCenter host to connect to
:param user: User to connect as, usually root
:param pwd: Password to connect with
:param port: TCP port
:param cmd: esxcli command and arguments
:param esxi_host: If `host` is a vCenter host, then esxi_host is the
ESXi machine on which to execute this command
:return: Dictionary
'''
esx_cmd = salt.utils.which('esxicli')
if not esx_cmd:
log.error('Missing dependency: The salt.utils.vmware.esxcli function requires ESXCLI.')
return False
if not esxi_host:
# Then we are connecting directly to an ESXi server,
# 'host' points at that server, and esxi_host is a reference to the
# ESXi instance we are manipulating
esx_cmd += ' -s {0} -u {1} -p {2} --protocol={3} --portnumber={4} {5}'.format(host,
user,
pwd,
protocol,
port,
cmd)
else:
esx_cmd += ' -s {0} -h {1} -u {2} -p {3} --protocol={4} --portnumber={5} {6}'.format(host,
esxi_host,
user,
pwd,
protocol,
port,
cmd)
ret = salt.modules.cmdmod.run_all(esx_cmd)
return ret
def get_service_instance(host, username, password, protocol=None, port=None):
'''
Authenticate with a vCenter server or ESX/ESXi host and return the service instance object.