salt/cloud/clouds/saltify.py
# -*- coding: utf-8 -*-
'''
.. _saltify-module:
Saltify Module
==============
The Saltify module is designed to install Salt on a remote machine, virtual or
bare metal, using SSH. This module is useful for provisioning machines which
are already installed, but not Salted.
.. versionchanged:: 2018.3.0
The wake_on_lan capability, and actions destroy, reboot, and query functions were added.
Use of this module requires some configuration in cloud profile and provider
files as described in the
:ref:`Gettting Started with Saltify <getting-started-with-saltify>` documentation.
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import time
# Import salt libs
import salt.utils.cloud
import salt.config as config
import salt.client
import salt.ext.six as six
from salt._compat import ipaddress
from salt.exceptions import SaltCloudException, SaltCloudSystemExit
# Get logging started
log = logging.getLogger(__name__)
try:
# noinspection PyUnresolvedReferences
from impacket.smbconnection import SessionError as smbSessionError
from impacket.smb3 import SessionError as smb3SessionError
HAS_IMPACKET = True
except ImportError:
HAS_IMPACKET = False
try:
# noinspection PyUnresolvedReferences
from winrm.exceptions import WinRMTransportError
# noinspection PyUnresolvedReferences
from requests.exceptions import (
ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
ProxyError, RetryError, InvalidSchema)
HAS_WINRM = True
except ImportError:
HAS_WINRM = False
def __virtual__():
'''
Needs no special configuration
'''
return True
def avail_locations(call=None):
'''
This function returns a list of locations available.
.. code-block:: bash
salt-cloud --list-locations my-cloud-provider
[ saltify will always return an empty dictionary ]
'''
return {}
def avail_images(call=None):
'''
This function returns a list of images available for this cloud provider.
.. code-block:: bash
salt-cloud --list-images saltify
returns a list of available profiles.
..versionadded:: 2018.3.0
'''
vm_ = get_configured_provider()
return {'Profiles': [profile for profile in vm_['profiles']]}
def avail_sizes(call=None):
'''
This function returns a list of sizes available for this cloud provider.
.. code-block:: bash
salt-cloud --list-sizes saltify
[ saltify always returns an empty dictionary ]
'''
return {}
def list_nodes(call=None):
'''
List the nodes which have salt-cloud:driver:saltify grains.
.. code-block:: bash
salt-cloud -Q
returns a list of dictionaries of defined standard fields.
..versionadded:: 2018.3.0
'''
nodes = _list_nodes_full(call)
return _build_required_items(nodes)
def _build_required_items(nodes):
ret = {}
for name, grains in nodes.items():
if grains:
private_ips = []
public_ips = []
ips = grains['ipv4'] + grains['ipv6']
for adrs in ips:
ip_ = ipaddress.ip_address(adrs)
if not ip_.is_loopback:
if ip_.is_private:
private_ips.append(adrs)
else:
public_ips.append(adrs)
ret[name] = {
'id': grains['id'],
'image': grains['salt-cloud']['profile'],
'private_ips': private_ips,
'public_ips': public_ips,
'size': '',
'state': 'running'
}
return ret
def list_nodes_full(call=None):
'''
Lists complete information for all nodes.
.. code-block:: bash
salt-cloud -F
returns a list of dictionaries.
for 'saltify' minions, returns dict of grains (enhanced).
..versionadded:: 2018.3.0
'''
ret = _list_nodes_full(call)
for key, grains in ret.items(): # clean up some hyperverbose grains -- everything is too much
try:
del grains['cpu_flags'], grains['disks'], grains['pythonpath'], grains['dns'], grains['gpus']
except KeyError:
pass # ignore absence of things we are eliminating
except TypeError:
del ret[key] # eliminate all reference to unexpected (None) values.
reqs = _build_required_items(ret)
for name in ret:
ret[name].update(reqs[name])
return ret
def _list_nodes_full(call=None):
'''
List the nodes, ask all 'saltify' minions, return dict of grains.
'''
local = salt.client.LocalClient()
return local.cmd('salt-cloud:driver:saltify', 'grains.items', '',
tgt_type='grain')
def list_nodes_select(call=None):
'''
Return a list of the minions that have salt-cloud grains, with
select fields.
'''
return salt.utils.cloud.list_nodes_select(
list_nodes_full('function'), __opts__['query.selection'], call,
)
def show_instance(name, call=None):
'''
List the a single node, return dict of grains.
'''
local = salt.client.LocalClient()
ret = local.cmd(name, 'grains.items')
ret.update(_build_required_items(ret))
return ret
def create(vm_):
'''
if configuration parameter ``deploy`` is ``True``,
Provision a single machine, adding its keys to the salt master
else,
Test ssh connections to the machine
Configuration parameters:
- deploy: (see above)
- provider: name of entry in ``salt/cloud.providers.d/???`` file
- ssh_host: IP address or DNS name of the new machine
- ssh_username: name used to log in to the new machine
- ssh_password: password to log in (unless key_filename is used)
- key_filename: (optional) SSH private key for passwordless login
- ssh_port: (default=22) TCP port for SSH connection
- wake_on_lan_mac: (optional) hardware (MAC) address for wake on lan
- wol_sender_node: (optional) salt minion to send wake on lan command
- wol_boot_wait: (default=30) seconds to delay while client boots
- force_minion_config: (optional) replace the minion configuration files on the new machine
See also
:ref:`Miscellaneous Salt Cloud Options <misc-salt-cloud-options>`
and
:ref:`Getting Started with Saltify <getting-started-with-saltify>`
CLI Example:
.. code-block:: bash
salt-cloud -p mymachine my_new_id
'''
deploy_config = config.get_cloud_config_value(
'deploy', vm_, __opts__, default=False)
# If ssh_host is not set, default to the minion name
if not config.get_cloud_config_value('ssh_host', vm_, __opts__, default=''):
vm_['ssh_host'] = vm_['name']
if deploy_config:
wol_mac = config.get_cloud_config_value(
'wake_on_lan_mac', vm_, __opts__, default='')
wol_host = config.get_cloud_config_value(
'wol_sender_node', vm_, __opts__, default='')
if wol_mac and wol_host:
good_ping = False
local = salt.client.LocalClient()
ssh_host = config.get_cloud_config_value(
'ssh_host', vm_, __opts__, default='')
if ssh_host:
log.info('trying to ping %s', ssh_host)
count = 'n' if salt.utils.platform.is_windows() else 'c'
cmd = 'ping -{} 1 {}'.format(count, ssh_host)
good_ping = local.cmd(wol_host, 'cmd.retcode', [cmd]) == 0
if good_ping:
log.info('successful ping.')
else:
log.info('sending wake-on-lan to %s using node %s',
wol_mac, wol_host)
if isinstance(wol_mac, six.string_types):
wol_mac = [wol_mac] # a smart user may have passed more params
ret = local.cmd(wol_host, 'network.wol', wol_mac)
log.info('network.wol returned value %s', ret)
if ret and ret[wol_host]:
sleep_time = config.get_cloud_config_value(
'wol_boot_wait', vm_, __opts__, default=30)
if sleep_time > 0.0:
log.info('delaying %d seconds for boot', sleep_time)
time.sleep(sleep_time)
log.info('Provisioning existing machine %s', vm_['name'])
ret = __utils__['cloud.bootstrap'](vm_, __opts__)
else:
ret = _verify(vm_)
return ret
def get_configured_provider():
'''
Return the first configured instance.
'''
return config.is_provider_configured(
__opts__,
__active_provider_name__ or 'saltify',
()
)
def _verify(vm_):
'''
Verify credentials for an existing system
'''
log.info('Verifying credentials for %s', vm_['name'])
win_installer = config.get_cloud_config_value(
'win_installer', vm_, __opts__)
if win_installer:
log.debug('Testing Windows authentication method for %s', vm_['name'])
if not HAS_IMPACKET:
log.error('Impacket library not found')
return False
# Test Windows connection
kwargs = {
'host': vm_['ssh_host'],
'username': config.get_cloud_config_value(
'win_username', vm_, __opts__, default='Administrator'),
'password': config.get_cloud_config_value(
'win_password', vm_, __opts__, default='')
}
# Test SMB connection
try:
log.debug('Testing SMB protocol for %s', vm_['name'])
if __utils__['smb.get_conn'](**kwargs) is False:
return False
except (smbSessionError, smb3SessionError) as exc:
log.error('Exception: %s', exc)
return False
# Test WinRM connection
use_winrm = config.get_cloud_config_value(
'use_winrm', vm_, __opts__, default=False)
if use_winrm:
log.debug('WinRM protocol requested for %s', vm_['name'])
if not HAS_WINRM:
log.error('WinRM library not found')
return False
kwargs['port'] = config.get_cloud_config_value(
'winrm_port', vm_, __opts__, default=5986)
kwargs['timeout'] = 10
try:
log.debug('Testing WinRM protocol for %s', vm_['name'])
return __utils__['cloud.wait_for_winrm'](**kwargs) is not None
except (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
ProxyError, RetryError, InvalidSchema, WinRMTransportError) as exc:
log.error('Exception: %s', exc)
return False
return True
else:
log.debug('Testing SSH authentication method for %s', vm_['name'])
# Test SSH connection
kwargs = {
'host': vm_['ssh_host'],
'port': config.get_cloud_config_value(
'ssh_port', vm_, __opts__, default=22
),
'username': config.get_cloud_config_value(
'ssh_username', vm_, __opts__, default='root'
),
'password': config.get_cloud_config_value(
'password', vm_, __opts__, search_global=False
),
'key_filename': config.get_cloud_config_value(
'key_filename', vm_, __opts__, search_global=False,
default=config.get_cloud_config_value(
'ssh_keyfile', vm_, __opts__, search_global=False,
default=None
)
),
'gateway': vm_.get('gateway', None),
'maxtries': 1
}
log.debug('Testing SSH protocol for %s', vm_['name'])
try:
return __utils__['cloud.wait_for_passwd'](**kwargs) is True
except SaltCloudException as exc:
log.error('Exception: %s', exc)
return False
def destroy(name, call=None):
''' Destroy a node.
.. versionadded:: 2018.3.0
Disconnect a minion from the master, and remove its keys.
Optionally, (if ``remove_config_on_destroy`` is ``True``),
disables salt-minion from running on the minion, and
erases the Salt configuration files from it.
Optionally, (if ``shutdown_on_destroy`` is ``True``),
orders the minion to halt.
CLI Example:
.. code-block:: bash
salt-cloud --destroy mymachine
'''
if call == 'function':
raise SaltCloudSystemExit(
'The destroy action must be called with -d, --destroy, '
'-a, or --action.'
)
opts = __opts__
__utils__['cloud.fire_event'](
'event',
'destroying instance',
'salt/cloud/{0}/destroying'.format(name),
args={'name': name},
sock_dir=opts['sock_dir'],
transport=opts['transport']
)
vm_ = get_configured_provider()
local = salt.client.LocalClient()
my_info = local.cmd(name, 'grains.get', ['salt-cloud'])
try:
vm_.update(my_info[name]) # get profile name to get config value
except (IndexError, TypeError):
pass
if config.get_cloud_config_value(
'remove_config_on_destroy', vm_, opts, default=True
):
ret = local.cmd(name, # prevent generating new keys on restart
'service.disable',
['salt-minion'])
if ret and ret[name]:
log.info('disabled salt-minion service on %s', name)
ret = local.cmd(name, 'config.get', ['conf_file'])
if ret and ret[name]:
confile = ret[name]
ret = local.cmd(name, 'file.remove', [confile])
if ret and ret[name]:
log.info('removed minion %s configuration file %s',
name, confile)
ret = local.cmd(name, 'config.get', ['pki_dir'])
if ret and ret[name]:
pki_dir = ret[name]
ret = local.cmd(name, 'file.remove', [pki_dir])
if ret and ret[name]:
log.info(
'removed minion %s key files in %s',
name,
pki_dir)
if config.get_cloud_config_value(
'shutdown_on_destroy', vm_, opts, default=False
):
ret = local.cmd(name, 'system.shutdown')
if ret and ret[name]:
log.info('system.shutdown for minion %s successful', name)
__utils__['cloud.fire_event'](
'event',
'destroyed instance',
'salt/cloud/{0}/destroyed'.format(name),
args={'name': name},
sock_dir=opts['sock_dir'],
transport=opts['transport']
)
return {'Destroyed': '{0} was destroyed.'.format(name)}
def reboot(name, call=None):
'''
Reboot a saltify minion.
..versionadded:: 2018.3.0
name
The name of the VM to reboot.
CLI Example:
.. code-block:: bash
salt-cloud -a reboot vm_name
'''
if call != 'action':
raise SaltCloudException(
'The reboot action must be called with -a or --action.'
)
local = salt.client.LocalClient()
return local.cmd(name, 'system.reboot')