Mini Shell
"""
.. _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")
Zerion Mini Shell 1.0