mirror of
https://github.com/saltstack/salt.git
synced 2025-04-10 23:01:39 +00:00
544 lines
15 KiB
Python
544 lines
15 KiB
Python
"""
|
|
.. _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:`Getting Started with Saltify <getting-started-with-saltify>` documentation.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
|
|
import salt.client
|
|
import salt.config as config
|
|
import salt.utils.cloud
|
|
from salt._compat import ipaddress
|
|
from salt.exceptions import SaltCloudException, SaltCloudSystemExit
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
try:
|
|
# noinspection PyUnresolvedReferences
|
|
from smbprotocol.exceptions import InternalError as smbSessionError
|
|
|
|
HAS_SMB = True
|
|
except ImportError:
|
|
HAS_SMB = False
|
|
|
|
try:
|
|
# noinspection PyUnresolvedReferences
|
|
# noinspection PyUnresolvedReferences
|
|
from requests.exceptions import (
|
|
ConnectionError,
|
|
ConnectTimeout,
|
|
InvalidSchema,
|
|
ProxyError,
|
|
ReadTimeout,
|
|
RetryError,
|
|
SSLError,
|
|
)
|
|
from winrm.exceptions import WinRMTransportError
|
|
|
|
HAS_WINRM = True
|
|
except ImportError:
|
|
HAS_WINRM = False
|
|
|
|
|
|
def __virtual__():
|
|
"""
|
|
Needs no special configuration
|
|
"""
|
|
return True
|
|
|
|
|
|
def _get_active_provider_name():
|
|
try:
|
|
return __active_provider_name__.value()
|
|
except AttributeError:
|
|
return __active_provider_name__
|
|
|
|
|
|
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.
|
|
"""
|
|
with salt.client.LocalClient() as local:
|
|
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.
|
|
"""
|
|
with salt.client.LocalClient() as local:
|
|
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
|
|
ssh_host = config.get_cloud_config_value(
|
|
"ssh_host", vm_, __opts__, default=""
|
|
)
|
|
with salt.client.LocalClient() as local:
|
|
if ssh_host:
|
|
log.info("trying to ping %s", ssh_host)
|
|
count = "n" if salt.utils.platform.is_windows() else "c"
|
|
cmd = f"ping -{count} 1 {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, str):
|
|
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__, _get_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_SMB:
|
|
log.error("smbprotocol 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 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",
|
|
f"salt/cloud/{name}/destroying",
|
|
args={"name": name},
|
|
sock_dir=opts["sock_dir"],
|
|
transport=opts["transport"],
|
|
)
|
|
|
|
vm_ = get_configured_provider()
|
|
with salt.client.LocalClient() as local:
|
|
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",
|
|
f"salt/cloud/{name}/destroyed",
|
|
args={"name": name},
|
|
sock_dir=opts["sock_dir"],
|
|
transport=opts["transport"],
|
|
)
|
|
|
|
return {"Destroyed": f"{name} was destroyed."}
|
|
|
|
|
|
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."
|
|
)
|
|
|
|
with salt.client.LocalClient() as local:
|
|
return local.cmd(name, "system.reboot")
|