File: //opt/saltstack/salt/lib/python3.10/site-packages/salt/cloud/clouds/dimensiondata.py
"""
Dimension Data Cloud Module
===========================
This is a cloud module for the Dimension Data Cloud,
using the existing Libcloud driver for Dimension Data.
.. code-block:: yaml
# Note: This example is for /etc/salt/cloud.providers
# or any file in the
# /etc/salt/cloud.providers.d/ directory.
my-dimensiondata-config:
user_id: my_username
key: myPassword!
region: dd-na
driver: dimensiondata
:maintainer: Anthony Shaw <anthonyshaw@apache.org>
:depends: libcloud >= 1.2.1
"""
import logging
import pprint
import socket
import salt.config as config
import salt.utils.cloud
from salt.cloud.libcloudfuncs import * # pylint: disable=redefined-builtin,wildcard-import,unused-wildcard-import
from salt.exceptions import (
SaltCloudExecutionFailure,
SaltCloudExecutionTimeout,
SaltCloudSystemExit,
)
from salt.utils.functools import namespaced_function
from salt.utils.versions import Version
# Import libcloud
try:
import libcloud
from libcloud.compute.base import NodeAuthPassword, NodeDriver, NodeState
from libcloud.compute.providers import get_driver
from libcloud.compute.types import Provider
from libcloud.loadbalancer.base import Member
from libcloud.loadbalancer.providers import get_driver as get_driver_lb
from libcloud.loadbalancer.types import Provider as Provider_lb
# This work-around for Issue #32743 is no longer needed for libcloud >=
# 1.4.0. However, older versions of libcloud must still be supported with
# this work-around. This work-around can be removed when the required
# minimum version of libcloud is 2.0.0 (See PR #40837 - which is
# implemented in Salt 2018.3.0).
if Version(libcloud.__version__) < Version("1.4.0"):
# See https://github.com/saltstack/salt/issues/32743
import libcloud.security
libcloud.security.CA_CERTS_PATH.append("/etc/ssl/certs/YaST-CA.pem")
HAS_LIBCLOUD = True
except ImportError:
HAS_LIBCLOUD = False
try:
from netaddr import all_matching_cidrs # pylint: disable=unused-import
HAS_NETADDR = True
except ImportError:
HAS_NETADDR = False
# Some of the libcloud functions need to be in the same namespace as the
# functions defined in the module, so we create new function objects inside
# this module namespace
get_size = namespaced_function(get_size, globals())
get_image = namespaced_function(get_image, globals())
avail_locations = namespaced_function(avail_locations, globals())
avail_images = namespaced_function(avail_images, globals())
avail_sizes = namespaced_function(avail_sizes, globals())
script = namespaced_function(script, globals())
destroy = namespaced_function(destroy, globals())
reboot = namespaced_function(reboot, globals())
list_nodes = namespaced_function(list_nodes, globals())
list_nodes_full = namespaced_function(list_nodes_full, globals())
list_nodes_select = namespaced_function(list_nodes_select, globals())
show_instance = namespaced_function(show_instance, globals())
get_node = namespaced_function(get_node, globals())
# Get logging started
log = logging.getLogger(__name__)
__virtualname__ = "dimensiondata"
def __virtual__():
"""
Set up the libcloud functions and check for dimensiondata configurations.
"""
if get_configured_provider() is False:
return False
if get_dependencies() is False:
return False
for provider, details in __opts__["providers"].items():
if "dimensiondata" not in details:
continue
return __virtualname__
def _get_active_provider_name():
try:
return __active_provider_name__.value()
except AttributeError:
return __active_provider_name__
def get_configured_provider():
"""
Return the first configured instance.
"""
return config.is_provider_configured(
__opts__,
_get_active_provider_name() or "dimensiondata",
("user_id", "key", "region"),
)
def get_dependencies():
"""
Warn if dependencies aren't met.
"""
deps = {"libcloud": HAS_LIBCLOUD, "netaddr": HAS_NETADDR}
return config.check_driver_dependencies(__virtualname__, deps)
def _query_node_data(vm_, data):
running = False
try:
node = show_instance(vm_["name"], "action") # pylint: disable=not-callable
running = node["state"] == NodeState.RUNNING
log.debug(
"Loaded node data for %s:\nname: %s\nstate: %s",
vm_["name"],
pprint.pformat(node["name"]),
node["state"],
)
except Exception as err: # pylint: disable=broad-except
log.error(
"Failed to get nodes list: %s",
err,
# Show the traceback if the debug logging level is enabled
exc_info_on_loglevel=logging.DEBUG,
)
# Trigger a failure in the wait for IP function
return running
if not running:
# Still not running, trigger another iteration
return
private = node["private_ips"]
public = node["public_ips"]
if private and not public:
log.warning(
"Private IPs returned, but not public. Checking for misidentified IPs."
)
for private_ip in private:
private_ip = preferred_ip(vm_, [private_ip])
if private_ip is False:
continue
if salt.utils.cloud.is_public_ip(private_ip):
log.warning("%s is a public IP", private_ip)
data.public_ips.append(private_ip)
else:
log.warning("%s is a private IP", private_ip)
if private_ip not in data.private_ips:
data.private_ips.append(private_ip)
if ssh_interface(vm_) == "private_ips" and data.private_ips:
return data
if private:
data.private_ips = private
if ssh_interface(vm_) == "private_ips":
return data
if public:
data.public_ips = public
if ssh_interface(vm_) != "private_ips":
return data
log.debug("Contents of the node data:")
log.debug(data)
def create(vm_):
"""
Create a single VM from a data dict
"""
try:
# Check for required profile parameters before sending any API calls.
if (
vm_["profile"]
and config.is_profile_configured(
__opts__, _get_active_provider_name() or "dimensiondata", vm_["profile"]
)
is False
):
return False
except AttributeError:
pass
__utils__["cloud.fire_event"](
"event",
"starting create",
"salt/cloud/{}/creating".format(vm_["name"]),
args=__utils__["cloud.filter_event"](
"creating", vm_, ["name", "profile", "provider", "driver"]
),
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
log.info("Creating Cloud VM %s", vm_["name"])
conn = get_conn()
location = conn.ex_get_location_by_id(vm_["location"])
images = conn.list_images(location=location)
image = [x for x in images if x.id == vm_["image"]][0]
network_domains = conn.ex_list_network_domains(location=location)
try:
network_domain = [
y for y in network_domains if y.name == vm_["network_domain"]
][0]
except IndexError:
network_domain = conn.ex_create_network_domain(
location=location,
name=vm_["network_domain"],
plan="ADVANCED",
description="",
)
try:
vlan = [
y
for y in conn.ex_list_vlans(
location=location, network_domain=network_domain
)
if y.name == vm_["vlan"]
][0]
except (IndexError, KeyError):
# Use the first VLAN in the network domain
vlan = conn.ex_list_vlans(location=location, network_domain=network_domain)[0]
kwargs = {
"name": vm_["name"],
"image": image,
"ex_description": vm_["description"],
"ex_network_domain": network_domain,
"ex_vlan": vlan,
"ex_is_started": vm_["is_started"],
}
event_data = _to_event_data(kwargs)
__utils__["cloud.fire_event"](
"event",
"requesting instance",
"salt/cloud/{}/requesting".format(vm_["name"]),
args=__utils__["cloud.filter_event"](
"requesting", event_data, list(event_data)
),
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
# Initial password (excluded from event payload)
initial_password = NodeAuthPassword(vm_["auth"])
kwargs["auth"] = initial_password
try:
data = conn.create_node(**kwargs)
except Exception as exc: # pylint: disable=broad-except
log.error(
"Error creating %s on DIMENSIONDATA\n\n"
"The following exception was thrown by libcloud when trying to "
"run the initial deployment: \n%s",
vm_["name"],
exc,
exc_info_on_loglevel=logging.DEBUG,
)
return False
try:
data = __utils__["cloud.wait_for_ip"](
_query_node_data,
update_args=(vm_, data),
timeout=config.get_cloud_config_value(
"wait_for_ip_timeout", vm_, __opts__, default=25 * 60
),
interval=config.get_cloud_config_value(
"wait_for_ip_interval", vm_, __opts__, default=30
),
max_failures=config.get_cloud_config_value(
"wait_for_ip_max_failures", vm_, __opts__, default=60
),
)
except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc:
try:
# It might be already up, let's destroy it!
destroy(vm_["name"]) # pylint: disable=not-callable
except SaltCloudSystemExit:
pass
finally:
raise SaltCloudSystemExit(str(exc))
log.debug("VM is now running")
if ssh_interface(vm_) == "private_ips":
ip_address = preferred_ip(vm_, data.private_ips)
else:
ip_address = preferred_ip(vm_, data.public_ips)
log.debug("Using IP address %s", ip_address)
if __utils__["cloud.get_salt_interface"](vm_, __opts__) == "private_ips":
salt_ip_address = preferred_ip(vm_, data.private_ips)
log.info("Salt interface set to: %s", salt_ip_address)
else:
salt_ip_address = preferred_ip(vm_, data.public_ips)
log.debug("Salt interface set to: %s", salt_ip_address)
if not ip_address:
raise SaltCloudSystemExit("No IP addresses could be found.")
vm_["salt_host"] = salt_ip_address
vm_["ssh_host"] = ip_address
vm_["password"] = vm_["auth"]
ret = __utils__["cloud.bootstrap"](vm_, __opts__)
ret.update(data.__dict__)
if "password" in data.extra:
del data.extra["password"]
log.info("Created Cloud VM '%s'", vm_["name"])
log.debug(
"'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data.__dict__)
)
__utils__["cloud.fire_event"](
"event",
"created instance",
"salt/cloud/{}/created".format(vm_["name"]),
args=__utils__["cloud.filter_event"](
"created", vm_, ["name", "profile", "provider", "driver"]
),
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
return ret
def create_lb(kwargs=None, call=None):
r"""
Create a load-balancer configuration.
CLI Example:
.. code-block:: bash
salt-cloud -f create_lb dimensiondata \
name=dev-lb port=80 protocol=http \
members=w1,w2,w3 algorithm=ROUND_ROBIN
"""
conn = get_conn()
if call != "function":
raise SaltCloudSystemExit(
"The create_lb function must be called with -f or --function."
)
if not kwargs or "name" not in kwargs:
log.error("A name must be specified when creating a health check.")
return False
if "port" not in kwargs:
log.error("A port or port-range must be specified for the load-balancer.")
return False
if "networkdomain" not in kwargs:
log.error("A network domain must be specified for the load-balancer.")
return False
if "members" in kwargs:
members = []
ip = ""
membersList = kwargs.get("members").split(",")
log.debug("MemberList: %s", membersList)
for member in membersList:
try:
log.debug("Member: %s", member)
node = get_node(conn, member) # pylint: disable=not-callable
log.debug("Node: %s", node)
ip = node.private_ips[0]
except Exception as err: # pylint: disable=broad-except
log.error(
"Failed to get node ip: %s",
err,
# Show the traceback if the debug logging level is enabled
exc_info_on_loglevel=logging.DEBUG,
)
members.append(Member(ip, ip, kwargs["port"]))
else:
members = None
log.debug("Members: %s", members)
networkdomain = kwargs["networkdomain"]
name = kwargs["name"]
port = kwargs["port"]
protocol = kwargs.get("protocol", None)
algorithm = kwargs.get("algorithm", None)
lb_conn = get_lb_conn(conn)
network_domains = conn.ex_list_network_domains()
network_domain = [y for y in network_domains if y.name == networkdomain][0]
log.debug("Network Domain: %s", network_domain.id)
lb_conn.ex_set_current_network_domain(network_domain.id)
event_data = _to_event_data(kwargs)
__utils__["cloud.fire_event"](
"event",
"create load_balancer",
"salt/cloud/loadbalancer/creating",
args=event_data,
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
lb = lb_conn.create_balancer(name, port, protocol, algorithm, members)
event_data = _to_event_data(kwargs)
__utils__["cloud.fire_event"](
"event",
"created load_balancer",
"salt/cloud/loadbalancer/created",
args=event_data,
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
return _expand_balancer(lb)
def _expand_balancer(lb):
"""
Convert the libcloud load-balancer object into something more serializable.
"""
ret = {}
ret.update(lb.__dict__)
return ret
def preferred_ip(vm_, ips):
"""
Return the preferred Internet protocol. Either 'ipv4' (default) or 'ipv6'.
"""
proto = config.get_cloud_config_value(
"protocol", vm_, __opts__, default="ipv4", search_global=False
)
family = socket.AF_INET
if proto == "ipv6":
family = socket.AF_INET6
for ip in ips:
try:
socket.inet_pton(family, ip)
return ip
except Exception: # pylint: disable=broad-except
continue
return False
def ssh_interface(vm_):
"""
Return the ssh_interface type to connect to. Either 'public_ips' (default)
or 'private_ips'.
"""
return config.get_cloud_config_value(
"ssh_interface", vm_, __opts__, default="public_ips", search_global=False
)
def stop(name, call=None):
"""
Stop a VM in DimensionData.
name:
The name of the VM to stop.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
"""
conn = get_conn()
node = get_node(conn, name) # pylint: disable=not-callable
log.debug("Node of Cloud VM: %s", node)
status = conn.ex_shutdown_graceful(node)
log.debug("Status of Cloud VM: %s", status)
return status
def start(name, call=None):
"""
Stop a VM in DimensionData.
:param str name:
The name of the VM to stop.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
"""
conn = get_conn()
node = get_node(conn, name) # pylint: disable=not-callable
log.debug("Node of Cloud VM: %s", node)
status = conn.ex_start_node(node)
log.debug("Status of Cloud VM: %s", status)
return status
def get_conn():
"""
Return a conn object for the passed VM data
"""
vm_ = get_configured_provider()
driver = get_driver(Provider.DIMENSIONDATA)
region = config.get_cloud_config_value("region", vm_, __opts__)
user_id = config.get_cloud_config_value("user_id", vm_, __opts__)
key = config.get_cloud_config_value("key", vm_, __opts__)
if key is not None:
log.debug("DimensionData authenticating using password")
return driver(user_id, key, region=region)
def get_lb_conn(dd_driver=None):
"""
Return a load-balancer conn object
"""
vm_ = get_configured_provider()
region = config.get_cloud_config_value("region", vm_, __opts__)
user_id = config.get_cloud_config_value("user_id", vm_, __opts__)
key = config.get_cloud_config_value("key", vm_, __opts__)
if not dd_driver:
raise SaltCloudSystemExit(
"Missing dimensiondata_driver for get_lb_conn method."
)
return get_driver_lb(Provider_lb.DIMENSIONDATA)(user_id, key, region=region)
def _to_event_data(obj):
"""
Convert the specified object into a form that can be serialised by msgpack as event data.
:param obj: The object to convert.
"""
if obj is None:
return None
if isinstance(obj, bool):
return obj
if isinstance(obj, int):
return obj
if isinstance(obj, float):
return obj
if isinstance(obj, str):
return obj
if isinstance(obj, bytes):
return obj
if isinstance(obj, dict):
return obj
if isinstance(obj, NodeDriver): # Special case for NodeDriver (cyclic references)
return obj.name
if isinstance(obj, list):
return [_to_event_data(item) for item in obj]
event_data = {}
for attribute_name in dir(obj):
if attribute_name.startswith("_"):
continue
attribute_value = getattr(obj, attribute_name)
if callable(attribute_value): # Strip out methods
continue
event_data[attribute_name] = _to_event_data(attribute_value)
return event_data