Mini Shell
r"""
The Linode Cloud Module
=======================
The Linode cloud module is used to interact with the Linode Cloud.
Provider
--------
The following provider parameters are supported:
- **apikey**: (required) The key to use to authenticate with the Linode API.
- **password**: (required) The default password to set on new VMs. Must be 8 characters with at least one lowercase, uppercase, and numeric.
- **poll_interval**: (optional) The rate of time in milliseconds to poll the Linode API for changes. Defaults to ``500``.
- **ratelimit_sleep**: (optional) The time in seconds to wait before retrying after a ratelimit has been enforced. Defaults to ``0``.
.. note::
APIv3 usage has been removed in favor of APIv4. To move to APIv4 now,
See the full migration guide
here https://docs.saltproject.io/en/latest/topics/cloud/linode.html#migrating-to-apiv4.
Set up the provider configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/linode.conf``:
.. code-block:: yaml
my-linode-provider:
driver: linode
apikey: f4ZsmwtB1c7f85Jdu43RgXVDFlNjuJaeIYV8QMftTqKScEB2vSosFSr...
password: F00barbazverylongp@ssword
Profile
-------
The following profile parameters are supported:
- **size**: (required) The size of the VM. This should be a Linode instance type ID (i.e. ``g6-standard-2``). Run ``salt-cloud -f avail_sizes my-linode-provider`` for options.
- **location**: (required) The location of the VM. This should be a Linode region (e.g. ``us-east``). Run ``salt-cloud -f avail_locations my-linode-provider`` for options.
- **image**: (required) The image to deploy the boot disk from. This should be an image ID (e.g. ``linode/ubuntu22.04``); official images start with ``linode/``. Run ``salt-cloud -f avail_images my-linode-provider`` for more options.
- **password**: (\*required) The default password for the VM. Must be provided at the profile or provider level.
- **assign_private_ip**: (optional) Whether or not to assign a private IP to the VM. Defaults to ``False``.
- **backups_enabled**: (optional) Whether or not to enable the backup for this VM. Backup can be configured in your Linode account Defaults to ``False``.
- **ssh_interface**: (optional) The interface with which to connect over SSH. Valid options are ``private_ips`` or ``public_ips``. Defaults to ``public_ips``.
- **ssh_pubkey**: (optional) The public key to authorize for SSH with the VM.
- **swap**: (optional) The amount of disk space to allocate for the swap partition. Defaults to ``256``.
- **clonefrom**: (optional) The name of the Linode to clone from.
Set up a profile configuration in ``/etc/salt/cloud.profiles.d/``:
.. code-block:: yaml
my-linode-profile:
# a minimal configuration
provider: my-linode-provider
size: g6-standard-1
image: linode/ubuntu22.04
location: us-east
my-linode-profile-advanced:
# an advanced configuration
provider: my-linode-provider
size: g6-standard-3
image: linode/ubuntu22.04
location: eu-west
password: bogus123X
assign_private_ip: true
ssh_interface: private_ips
ssh_pubkey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB...
swap_size: 512
Migrating to APIv4
------------------
You will need to generate a new token for your account. See https://www.linode.com/docs/products/tools/api/get-started/#create-an-api-token
There are a few changes to note:
- There has been a general move from label references to ID references. The profile configuration parameters ``location``, ``size``, and ``image`` have moved from being label based references to IDs. See the profile section for more information. In addition to these inputs being changed, ``avail_sizes``, ``avail_locations``, and ``avail_images`` now output options sorted by ID instead of label.
- The ``disk_size`` profile configuration parameter has been deprecated and will not be taken into account when creating new VMs while targeting APIv4.
:maintainer: Linode Developer Tools and Experience Team <dev-dx@linode.com>
:depends: requests
"""
import datetime
import json
import logging
import pprint
import re
import time
from abc import ABC, abstractmethod
from pathlib import Path
import salt.config as config
from salt._compat import ipaddress
from salt.exceptions import SaltCloudException, SaltCloudNotFound, SaltCloudSystemExit
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# Get logging started
log = logging.getLogger(__name__)
# The epoch of the last time a query was made
LASTCALL = int(time.mktime(datetime.datetime.now().timetuple()))
__virtualname__ = "linode"
# Only load in this module if the Linode configurations are in place
def __virtual__():
"""
Check for Linode configs.
"""
if get_configured_provider() is False:
return False
if _get_dependencies() is False:
return False
return __virtualname__
def _get_active_provider_name():
try:
return __active_provider_name__.value()
except AttributeError:
return __active_provider_name__
def _get_backup_enabled(vm_):
"""
Return True if a backup is set to enabled
"""
return config.get_cloud_config_value(
"backups_enabled",
vm_,
__opts__,
default=False,
)
def get_configured_provider():
"""
Return the first configured instance.
"""
return config.is_provider_configured(
__opts__,
_get_active_provider_name() or __virtualname__,
("apikey", "password"),
)
def _get_dependencies():
"""
Warn if dependencies aren't met.
"""
deps = {"requests": HAS_REQUESTS}
return config.check_driver_dependencies(__virtualname__, deps)
def _get_api_key():
"""
Returned the configured Linode API key.
"""
val = config.get_cloud_config_value(
"api_key",
get_configured_provider(),
__opts__,
search_global=False,
default=config.get_cloud_config_value(
"apikey", get_configured_provider(), __opts__, search_global=False
),
)
return val
def _get_ratelimit_sleep():
"""
Return the configured time to wait before retrying after a ratelimit has been enforced.
"""
return config.get_cloud_config_value(
"ratelimit_sleep",
get_configured_provider(),
__opts__,
search_global=False,
default=0,
)
def _get_poll_interval():
"""
Return the configured interval in milliseconds to poll the Linode API for changes at.
"""
return config.get_cloud_config_value(
"poll_interval",
get_configured_provider(),
__opts__,
search_global=False,
default=500,
)
def _get_password(vm_):
r"""
Return the password to use for a VM.
vm\_
The configuration to obtain the password from.
"""
return config.get_cloud_config_value(
"password",
vm_,
__opts__,
default=config.get_cloud_config_value(
"passwd", vm_, __opts__, search_global=False
),
search_global=False,
)
def _get_private_ip(vm_):
"""
Return True if a private ip address is requested
"""
return config.get_cloud_config_value(
"assign_private_ip", vm_, __opts__, default=False
)
def _get_ssh_key_files(vm_):
"""
Return the configured file paths of the SSH keys.
"""
return config.get_cloud_config_value(
"ssh_key_files", vm_, __opts__, search_global=False, default=[]
)
def _get_ssh_key(vm_):
r"""
Return the SSH pubkey.
vm\_
The configuration to obtain the public key from.
"""
return config.get_cloud_config_value(
"ssh_pubkey", vm_, __opts__, search_global=False
)
def _get_swap_size(vm_):
r"""
Returns the amount of swap space to be used in MB.
vm\_
The VM profile to obtain the swap size from.
"""
return config.get_cloud_config_value("swap", vm_, __opts__, default=256)
def _get_ssh_keys(vm_):
"""
Return all SSH keys from ``ssh_pubkey`` and ``ssh_key_files``.
"""
ssh_keys = set()
raw_pub_key = _get_ssh_key(vm_)
if raw_pub_key is not None:
ssh_keys.add(raw_pub_key)
key_files = _get_ssh_key_files(vm_)
for file in map(lambda file: Path(file).resolve(), key_files):
if not (file.exists() or file.is_file()):
raise SaltCloudSystemExit(f"Invalid SSH key file: {str(file)}")
ssh_keys.add(file.read_text())
return list(ssh_keys)
def _get_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 _validate_name(name):
"""
Checks if the provided name fits Linode's labeling parameters.
.. versionadded:: 2015.5.6
name
The VM name to validate
"""
name = str(name)
name_length = len(name)
regex = re.compile(r"^[a-zA-Z0-9][A-Za-z0-9_-]*[a-zA-Z0-9]$")
if name_length < 3 or name_length > 48:
ret = False
elif not re.match(regex, name):
ret = False
else:
ret = True
if ret is False:
log.warning(
"A Linode label may only contain ASCII letters or numbers, dashes, and "
"underscores, must begin and end with letters or numbers, and be at least "
"three characters in length."
)
return ret
class LinodeAPI(ABC):
@abstractmethod
def avail_images(self):
"""avail_images implementation"""
@abstractmethod
def avail_locations(self):
"""avail_locations implementation"""
@abstractmethod
def avail_sizes(self):
"""avail_sizes implementation"""
@abstractmethod
def boot(self, name=None, kwargs=None):
"""boot implementation"""
@abstractmethod
def clone(self, kwargs=None):
"""clone implementation"""
@abstractmethod
def create_config(self, kwargs=None):
"""create_config implementation"""
@abstractmethod
def create(self, vm_):
"""create implementation"""
@abstractmethod
def destroy(self, name):
"""destroy implementation"""
@abstractmethod
def get_config_id(self, kwargs=None):
"""get_config_id implementation"""
@abstractmethod
def list_nodes(self):
"""list_nodes implementation"""
@abstractmethod
def list_nodes_full(self):
"""list_nodes_full implementation"""
@abstractmethod
def list_nodes_min(self):
"""list_nodes_min implementation"""
@abstractmethod
def reboot(self, name):
"""reboot implementation"""
@abstractmethod
def show_instance(self, name):
"""show_instance implementation"""
@abstractmethod
def show_pricing(self, kwargs=None):
"""show_pricing implementation"""
@abstractmethod
def start(self, name):
"""start implementation"""
@abstractmethod
def stop(self, name):
"""stop implementation"""
@abstractmethod
def _get_linode_by_name(self, name):
"""_get_linode_by_name implementation"""
@abstractmethod
def _get_linode_by_id(self, linode_id):
"""_get_linode_by_id implementation"""
def get_linode(self, kwargs=None):
name = kwargs.get("name", None)
linode_id = kwargs.get("linode_id", None)
if linode_id is not None:
return self._get_linode_by_id(linode_id)
elif name is not None:
return self._get_linode_by_name(name)
raise SaltCloudSystemExit(
"The get_linode function requires either a 'name' or a 'linode_id'."
)
def list_nodes_select(self, call):
return __utils__["cloud.list_nodes_select"](
self.list_nodes_full(),
__opts__["query.selection"],
call,
)
class LinodeAPIv4(LinodeAPI):
@classmethod
def get_api_instance(cls):
if not hasattr(cls, "api_instance"):
cls.api_instance = cls()
return cls.api_instance
def _query(self, path, method="GET", data=None, headers=None):
"""
Make a call to the Linode API.
"""
api_key = _get_api_key()
ratelimit_sleep = _get_ratelimit_sleep()
if headers is None:
headers = {}
headers["Authorization"] = f"Bearer {api_key}"
headers["Content-Type"] = "application/json"
headers["User-Agent"] = "salt-cloud-linode"
url = f"https://api.linode.com/v4{path}"
decode = method != "DELETE"
result = None
log.debug("Linode API request: %s %s", method, url)
if data is not None:
log.trace("Linode API request body: %s", data)
attempt = 0
while True:
try:
result = requests.request(
method, url, json=data, headers=headers, timeout=120
)
log.debug("Linode API response status code: %d", result.status_code)
log.trace("Linode API response body: %s", result.text)
result.raise_for_status()
break
except requests.exceptions.HTTPError as exc:
err_response = exc.response
err_data = self._get_response_json(err_response)
status_code = err_response.status_code
if status_code == 429:
log.debug(
"received rate limit; retrying in %d seconds", ratelimit_sleep
)
time.sleep(ratelimit_sleep)
continue
if err_data is not None:
# Build an error from the response JSON
if "error" in err_data:
raise SaltCloudSystemExit(
"Linode API reported error: {}".format(err_data["error"])
)
elif "errors" in err_data:
api_errors = err_data["errors"]
# Build Salt exception
errors = []
for error in err_data["errors"]:
if "field" in error:
errors.append(
"field '{}': {}".format(
error.get("field"), error.get("reason")
)
)
else:
errors.append(error.get("reason"))
raise SaltCloudSystemExit(
"Linode API reported error(s): {}".format(", ".join(errors))
)
# If the response is not valid JSON or the error was not included, propagate the
# human readable status representation.
raise SaltCloudSystemExit(
f"Linode API error occurred: {err_response.reason}"
)
if decode:
return self._get_response_json(result)
return result
def avail_images(self):
response = self._query(path="/images")
ret = {}
for image in response["data"]:
ret[image["id"]] = image
return ret
def avail_locations(self):
response = self._query(path="/regions")
ret = {}
for region in response["data"]:
ret[region["id"]] = region
return ret
def avail_sizes(self):
response = self._query(path="/linode/types")
ret = {}
for instance_type in response["data"]:
ret[instance_type["id"]] = instance_type
return ret
def set_backup_schedule(self, label, linode_id, day, window, auto_enable=False):
instance = self.get_linode(kwargs={"linode_id": linode_id, "name": label})
linode_id = instance.get("id", None)
if auto_enable:
backups = instance.get("backups")
if backups and not backups.get("enabled"):
self._query(
f"/linode/instances/{linode_id}/backups/enable",
method="POST",
)
self._query(
f"/linode/instances/{linode_id}",
method="PUT",
data={"backups": {"schedule": {"day": day, "window": window}}},
)
def boot(self, name=None, kwargs=None):
instance = self.get_linode(
kwargs={"linode_id": kwargs.get("linode_id", None), "name": name}
)
config_id = kwargs.get("config_id", None)
check_running = kwargs.get("check_running", True)
linode_id = instance.get("id", None)
name = instance.get("label", None)
if check_running:
if instance["status"] == "running":
raise SaltCloudSystemExit(
"Cannot boot Linode {0} ({1}). "
"Linode {0} is already running.".format(name, linode_id)
)
self._query(
f"/linode/instances/{linode_id}/boot",
method="POST",
data={"config_id": config_id},
)
self._wait_for_linode_status(linode_id, "running")
return True
def clone(self, kwargs=None):
linode_id = kwargs.get("linode_id", None)
location = kwargs.get("location", None)
size = kwargs.get("size", None)
for item in [linode_id, location, size]:
if item is None:
raise SaltCloudSystemExit(
"The clone function requires a 'linode_id', 'location',"
"and 'size' to be provided."
)
return self._query(
f"/linode/instances/{linode_id}/clone",
method="POST",
data={"region": location, "type": size},
)
def create_config(self, kwargs=None):
name = kwargs.get("name", None)
linode_id = kwargs.get("linode_id", None)
root_disk_id = kwargs.get("root_disk_id", None)
swap_disk_id = kwargs.get("swap_disk_id", None)
data_disk_id = kwargs.get("data_disk_id", None)
if not name and not linode_id:
raise SaltCloudSystemExit(
"The create_config function requires either a 'name' or 'linode_id'"
)
required_params = [name, linode_id, root_disk_id, swap_disk_id]
for item in required_params:
if item is None:
raise SaltCloudSystemExit(
"The create_config functions requires a 'name', 'linode_id', "
"'root_disk_id', and 'swap_disk_id'."
)
devices = {
"sda": {"disk_id": int(root_disk_id)},
"sdb": {"disk_id": int(data_disk_id)} if data_disk_id is not None else None,
"sdc": {"disk_id": int(swap_disk_id)},
}
return self._query(
f"/linode/instances/{linode_id}/configs",
method="POST",
data={"label": name, "devices": devices},
)
def create(self, vm_):
name = vm_["name"]
if not _validate_name(name):
return False
__utils__["cloud.fire_event"](
"event",
"starting create",
f"salt/cloud/{name}/creating",
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", name)
result = None
pub_ssh_keys = _get_ssh_keys(vm_)
ssh_interface = _get_ssh_interface(vm_)
use_private_ip = ssh_interface == "private_ips"
assign_private_ip = _get_private_ip(vm_) or use_private_ip
password = _get_password(vm_)
swap_size = _get_swap_size(vm_)
backups_enabled = _get_backup_enabled(vm_)
clonefrom_name = vm_.get("clonefrom", None)
instance_type = vm_.get("size", None)
image = vm_.get("image", None)
should_clone = True if clonefrom_name else False
if should_clone:
# clone into new linode
clone_linode = self.get_linode(kwargs={"name": clonefrom_name})
result = clone(
{
"linode_id": clone_linode["id"],
"location": clone_linode["region"],
"size": clone_linode["type"],
}
)
# create private IP if needed
if assign_private_ip:
self._query(
"/networking/ips",
method="POST",
data={"type": "ipv4", "public": False, "linode_id": result["id"]},
)
else:
# create new linode
result = self._query(
"/linode/instances",
method="POST",
data={
"backups_enabled": backups_enabled,
"label": name,
"type": instance_type,
"region": vm_.get("location", None),
"private_ip": assign_private_ip,
"booted": True,
"root_pass": password,
"authorized_keys": pub_ssh_keys,
"image": image,
"swap_size": swap_size,
},
)
linode_id = result.get("id", None)
# wait for linode to be created
self._wait_for_event("linode_create", "linode", linode_id, "finished")
log.debug("linode '%s' has been created", name)
if should_clone:
self.boot(kwargs={"linode_id": linode_id})
# wait for linode to finish booting
self._wait_for_linode_status(linode_id, "running")
public_ips, private_ips = self._get_ips(linode_id)
data = {}
data["id"] = linode_id
data["name"] = result["label"]
data["size"] = result["type"]
data["state"] = result["status"]
data["ipv4"] = result["ipv4"]
data["ipv6"] = result["ipv6"]
data["public_ips"] = public_ips
data["private_ips"] = private_ips
if use_private_ip:
vm_["ssh_host"] = private_ips[0]
else:
vm_["ssh_host"] = public_ips[0]
# Send event that the instance has booted.
__utils__["cloud.fire_event"](
"event",
"waiting for ssh",
f"salt/cloud/{name}/waiting_for_ssh",
sock_dir=__opts__["sock_dir"],
args={"ip_address": vm_["ssh_host"]},
transport=__opts__["transport"],
)
ret = __utils__["cloud.bootstrap"](vm_, __opts__)
ret.update(data)
log.info("Created Cloud VM '%s'", name)
log.debug("'%s' VM creation details:\n%s", name, pprint.pformat(data))
__utils__["cloud.fire_event"](
"event",
"created instance",
f"salt/cloud/{name}/created",
args=__utils__["cloud.filter_event"](
"created", vm_, ["name", "profile", "provider", "driver"]
),
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
return ret
def destroy(self, name):
__utils__["cloud.fire_event"](
"event",
"destroyed instance",
f"salt/cloud/{name}/destroyed",
args={"name": name},
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
if __opts__.get("update_cachedir", False) is True:
__utils__["cloud.delete_minion_cachedir"](
name, _get_active_provider_name().split(":")[0], __opts__
)
instance = self._get_linode_by_name(name)
linode_id = instance.get("id", None)
self._query(f"/linode/instances/{linode_id}", method="DELETE")
def get_config_id(self, kwargs=None):
name = kwargs.get("name", None)
linode_id = kwargs.get("linode_id", None)
if name is None and linode_id is None:
raise SaltCloudSystemExit(
"The get_config_id function requires either a 'name' or a 'linode_id' "
"to be provided."
)
if linode_id is None:
linode_id = self.get_linode(kwargs=kwargs).get("id", None)
response = self._query(f"/linode/instances/{linode_id}/configs")
configs = response.get("data", [])
return {"config_id": configs[0]["id"]}
def list_nodes_min(self):
result = self._query("/linode/instances")
instances = result.get("data", [])
ret = {}
for instance in instances:
name = instance["label"]
ret[name] = {"id": instance["id"], "state": instance["status"]}
return ret
def list_nodes_full(self):
return self._list_linodes(full=True)
def list_nodes(self):
return self._list_linodes()
def reboot(self, name):
instance = self._get_linode_by_name(name)
linode_id = instance.get("id", None)
self._query(f"/linode/instances/{linode_id}/reboot", method="POST")
return self._wait_for_linode_status(linode_id, "running")
def show_instance(self, name):
instance = self._get_linode_by_name(name)
linode_id = instance.get("id", None)
public_ips, private_ips = self._get_ips(linode_id)
return {
"id": instance["id"],
"image": instance["image"],
"name": instance["label"],
"size": instance["type"],
"state": instance["status"],
"public_ips": public_ips,
"private_ips": private_ips,
}
def show_pricing(self, kwargs=None):
profile = __opts__["profiles"].get(kwargs["profile"], {})
if not profile:
raise SaltCloudNotFound("The requested profile was not found.")
# Make sure the profile belongs to Linode
provider = profile.get("provider", "0:0")
comps = provider.split(":")
if len(comps) < 2 or comps[1] != "linode":
raise SaltCloudException("The requested profile does not belong to Linode.")
instance_type = self._get_linode_type(profile["size"])
pricing = instance_type.get("price", {})
per_hour = pricing["hourly"]
per_day = per_hour * 24
per_week = per_day * 7
per_month = pricing["monthly"]
per_year = per_month * 12
return {
profile["profile"]: {
"per_hour": per_hour,
"per_day": per_day,
"per_week": per_week,
"per_month": per_month,
"per_year": per_year,
}
}
def start(self, name):
instance = self._get_linode_by_name(name)
linode_id = instance.get("id", None)
if instance["status"] == "running":
return {
"success": True,
"action": "start",
"state": "Running",
"msg": "Machine already running",
}
self._query(f"/linode/instances/{linode_id}/boot", method="POST")
self._wait_for_linode_status(linode_id, "running")
return {
"success": True,
"state": "Running",
"action": "start",
}
def stop(self, name):
instance = self._get_linode_by_name(name)
linode_id = instance.get("id", None)
if instance["status"] == "offline":
return {
"success": True,
"action": "stop",
"state": "Stopped",
"msg": "Machine already stopped",
}
self._query(f"/linode/instances/{linode_id}/shutdown", method="POST")
self._wait_for_linode_status(linode_id, "offline")
return {"success": True, "state": "Stopped", "action": "stop"}
def _get_linode_by_id(self, linode_id):
return self._query(f"/linode/instances/{linode_id}")
def _get_linode_by_name(self, name):
result = self._query("/linode/instances")
instances = result.get("data", [])
for instance in instances:
if instance["label"] == name:
return instance
raise SaltCloudNotFound(f"The specified name, {name}, could not be found.")
def _list_linodes(self, full=False):
result = self._query("/linode/instances")
instances = result.get("data", [])
ret = {}
for instance in instances:
node = {}
node["id"] = instance["id"]
node["image"] = instance["image"]
node["name"] = instance["label"]
node["size"] = instance["type"]
node["state"] = instance["status"]
public_ips, private_ips = self._get_ips(node["id"])
node["public_ips"] = public_ips
node["private_ips"] = private_ips
if full:
node["extra"] = instance
ret[instance["label"]] = node
return ret
def _get_linode_type(self, linode_type):
return self._query(f"/linode/types/{linode_type}")
def _get_ips(self, linode_id):
instance = self._get_linode_by_id(linode_id)
public = []
private = []
for addr in instance.get("ipv4", []):
if ipaddress.ip_address(addr).is_private:
private.append(addr)
else:
public.append(addr)
return (public, private)
def _poll(
self,
description,
getter,
condition,
timeout=None,
poll_interval=None,
):
"""
Return true in handler to signal complete.
"""
if poll_interval is None:
poll_interval = _get_poll_interval()
if timeout is None:
timeout = 120
times = (timeout * 1000) / poll_interval
curr = 0
while True:
curr += 1
result = getter()
if condition(result):
return True
elif curr <= times:
time.sleep(poll_interval / 1000)
log.info("retrying: polling for %s...", description)
else:
raise SaltCloudException(f"timed out: polling for {description}")
def _wait_for_entity_status(
self, getter, status, entity_name="item", identifier="some", timeout=None
):
return self._poll(
f"{entity_name} (id={identifier}) status to be '{status}'",
getter,
lambda item: item.get("status") == status,
timeout=timeout,
)
def _wait_for_linode_status(self, linode_id, status, timeout=None):
return self._wait_for_entity_status(
lambda: self._get_linode_by_id(linode_id),
status,
entity_name="linode",
identifier=linode_id,
timeout=timeout,
)
def _check_event_status(self, event, desired_status):
status = event.get("status")
action = event.get("action")
entity = event.get("entity")
if status == "failed":
raise SaltCloudSystemExit(
"event {} for {} (id={}) failed".format(
action, entity["type"], entity["id"]
)
)
return status == desired_status
def _wait_for_event(self, action, entity, entity_id, status, timeout=None):
event_filter = {
"+order_by": "created",
"+order": "desc",
"seen": False,
"action": action,
"entity.id": entity_id,
"entity.type": entity,
}
last_event = None
def condition(event):
return self._check_event_status(event, status)
while True:
if last_event is not None:
event_filter["+gt"] = last_event
filter_json = json.dumps(event_filter, separators=(",", ":"))
result = self._query("/account/events", headers={"X-Filter": filter_json})
events = result.get("data", [])
if len(events) == 0:
break
for event in events:
event_id = event.get("id")
event_entity = event.get("entity", None)
last_event = event_id
if not event_entity:
continue
if not (
event_entity["type"] == entity
and event_entity["id"] == entity_id
and event.get("action") == action
):
continue
if condition(event):
return True
return self._poll(
f"event {event_id} to be '{status}'",
lambda: self._query(f"/account/events/{event_id}"),
condition,
timeout=timeout,
)
return False
def _get_response_json(self, response):
json = None
try:
json = response.json()
except ValueError:
pass
return json
def avail_images(call=None):
"""
Return available Linode images.
CLI Example:
.. code-block:: bash
salt-cloud --list-images my-linode-config
salt-cloud -f avail_images my-linode-config
"""
if call == "action":
raise SaltCloudException(
"The avail_images function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().avail_images()
def avail_locations(call=None):
"""
Return available Linode datacenter locations.
CLI Example:
.. code-block:: bash
salt-cloud --list-locations my-linode-config
salt-cloud -f avail_locations my-linode-config
"""
if call == "action":
raise SaltCloudException(
"The avail_locations function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().avail_locations()
def avail_sizes(call=None):
"""
Return available Linode sizes.
CLI Example:
.. code-block:: bash
salt-cloud --list-sizes my-linode-config
salt-cloud -f avail_sizes my-linode-config
"""
if call == "action":
raise SaltCloudException(
"The avail_locations function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().avail_sizes()
def set_backup_schedule(name=None, kwargs=None, call=None):
"""
Set the backup schedule for a Linode.
name
The name (label) of the Linode. Can be used instead of
``linode_id``.
linode_id
The ID of the Linode instance to set the backup schedule for.
If provided, will be used as an alternative to ``name`` and
reduces the number of API calls to Linode by one. Will be
preferred over ``name``.
auto_enable
If ``True``, automatically enable the backup feature for the Linode
if it wasn't already enabled. Optional parameter, default to ``False``.
day
Possible values:
``Sunday``, ``Monday``, ``Tuesday``, ``Wednesday``,
``Thursday``, ``Friday``, ``Saturday``
The day of the week that your Linode's weekly Backup is taken.
If not set manually, a day will be chosen for you. Backups are
taken every day, but backups taken on this day are preferred
when selecting backups to retain for a longer period.
If not set manually, then when backups are initially enabled,
this may come back as ``Scheduling`` until the day is automatically
selected.
window
Possible values:
``W0``, ``W2``, ``W4``, ``W6``, ``W8``, ``W10``,
``W12``, ``W14``, ``W16``, ``W18``, ``W20``, ``W22``
The window in which your backups will be taken, in UTC. A backups
window is a two-hour span of time in which the backup may occur.
For example, ``W10`` indicates that your backups should be taken
between 10:00 and 12:00. If you do not choose a backup window, one
will be selected for you automatically.
If not set manually, when backups are initially enabled this may come
back as ``Scheduling`` until the window is automatically selected.
Can be called as an action (which requires a name):
.. code-block:: bash
salt-cloud -a set_backup_schedule my-linode-instance day=Monday window=W20 auto_enable=True
...or as a function (which requires either a name or linode_id):
.. code-block:: bash
salt-cloud -f set_backup_schedule my-linode-provider name=my-linode-instance day=Monday window=W20 auto_enable=True
salt-cloud -f set_backup_schedule my-linode-provider linode_id=1225876 day=Monday window=W20 auto_enable=True
"""
if name is None and call == "action":
raise SaltCloudSystemExit(
"The set_backup_schedule backup schedule "
"action requires the name of the Linode.",
)
if kwargs is None:
kwargs = {}
if call == "function":
name = kwargs.get("name", None)
linode_id = kwargs.get("linode_id")
auto_enable = str(kwargs.get("auto_enable")).lower() == "true"
if name is None and linode_id is None:
raise SaltCloudSystemExit(
"The set_backup_schedule function requires "
"either a 'name' or a 'linode_id'."
)
return LinodeAPIv4.get_api_instance().set_backup_schedule(
day=kwargs.get("day"),
window=kwargs.get("window"),
label=name,
linode_id=linode_id,
auto_enable=auto_enable,
)
def boot(name=None, kwargs=None, call=None):
"""
Boot a Linode.
name
The name of the Linode to boot. Can be used instead of ``linode_id``.
linode_id
The ID of the Linode to boot. If provided, will be used as an
alternative to ``name`` and reduces the number of API calls to
Linode by one. Will be preferred over ``name``.
config_id
The ID of the Config to boot. Required.
check_running
Defaults to True. If set to False, overrides the call to check if
the VM is running before calling the linode.boot API call. Change
``check_running`` to True is useful during the boot call in the
create function, since the new VM will not be running yet.
Can be called as an action (which requires a name):
.. code-block:: bash
salt-cloud -a boot my-instance config_id=10
...or as a function (which requires either a name or linode_id):
.. code-block:: bash
salt-cloud -f boot my-linode-config name=my-instance config_id=10
salt-cloud -f boot my-linode-config linode_id=1225876 config_id=10
"""
if name is None and call == "action":
raise SaltCloudSystemExit("The boot action requires a 'name'.")
linode_id = kwargs.get("linode_id", None)
config_id = kwargs.get("config_id", None)
if call == "function":
name = kwargs.get("name", None)
if name is None and linode_id is None:
raise SaltCloudSystemExit(
"The boot function requires either a 'name' or a 'linode_id'."
)
return LinodeAPIv4.get_api_instance().boot(name=name, kwargs=kwargs)
def clone(kwargs=None, call=None):
"""
Clone a Linode.
linode_id
The ID of the Linode to clone. Required.
location
The location of the new Linode. Required.
size
The size of the new Linode (must be greater than or equal to the clone source). Required.
CLI Example:
.. code-block:: bash
salt-cloud -f clone my-linode-config linode_id=1234567 location=us-central size=g6-standard-1
"""
if call == "action":
raise SaltCloudSystemExit(
"The clone function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().clone(kwargs=kwargs)
def create(vm_):
"""
Create a single Linode VM.
"""
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 "linode",
vm_["profile"],
vm_=vm_,
)
) is False:
return False
except AttributeError:
pass
return LinodeAPIv4.get_api_instance().create(vm_)
def create_config(kwargs=None, call=None):
"""
Creates a Linode Configuration Profile.
name
The name of the VM to create the config for.
linode_id
The ID of the Linode to create the configuration for.
root_disk_id
The Root Disk ID to be used for this config.
swap_disk_id
The Swap Disk ID to be used for this config.
data_disk_id
The Data Disk ID to be used for this config.
.. versionadded:: 2016.3.0
kernel_id
The ID of the kernel to use for this configuration profile.
"""
if call == "action":
raise SaltCloudSystemExit(
"The create_config function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().create_config(kwargs=kwargs)
def destroy(name, call=None):
"""
Destroys a Linode by name.
name
The name of VM to be be destroyed.
CLI Example:
.. code-block:: bash
salt-cloud -d vm_name
"""
if call == "function":
raise SaltCloudException(
"The destroy action must be called with -d, --destroy, -a or --action."
)
return LinodeAPIv4.get_api_instance().destroy(name)
def get_config_id(kwargs=None, call=None):
"""
Returns a config_id for a given linode.
.. versionadded:: 2015.8.0
name
The name of the Linode for which to get the config_id. Can be used instead
of ``linode_id``.
linode_id
The ID of the Linode for which to get the config_id. Can be used instead
of ``name``.
CLI Example:
.. code-block:: bash
salt-cloud -f get_config_id my-linode-config name=my-linode
salt-cloud -f get_config_id my-linode-config linode_id=1234567
"""
if call == "action":
raise SaltCloudException(
"The get_config_id function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().get_config_id(kwargs=kwargs)
def get_linode(kwargs=None, call=None):
"""
Returns data for a single named Linode.
name
The name of the Linode for which to get data. Can be used instead
``linode_id``. Note this will induce an additional API call
compared to using ``linode_id``.
linode_id
The ID of the Linode for which to get data. Can be used instead of
``name``.
CLI Example:
.. code-block:: bash
salt-cloud -f get_linode my-linode-config name=my-instance
salt-cloud -f get_linode my-linode-config linode_id=1234567
"""
if call == "action":
raise SaltCloudSystemExit(
"The get_linode function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().get_linode(kwargs=kwargs)
def list_nodes(call=None):
"""
Returns a list of linodes, keeping only a brief listing.
CLI Example:
.. code-block:: bash
salt-cloud -Q
salt-cloud --query
salt-cloud -f list_nodes my-linode-config
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
"""
if call == "action":
raise SaltCloudException(
"The list_nodes function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().list_nodes()
def list_nodes_full(call=None):
"""
List linodes, with all available information.
CLI Example:
.. code-block:: bash
salt-cloud -F
salt-cloud --full-query
salt-cloud -f list_nodes_full my-linode-config
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
"""
if call == "action":
raise SaltCloudException(
"The list_nodes_full function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().list_nodes_full()
def list_nodes_min(call=None):
"""
Return a list of the VMs that are on the provider. Only a list of VM names and
their state is returned. This is the minimum amount of information needed to
check for existing VMs.
.. versionadded:: 2015.8.0
CLI Example:
.. code-block:: bash
salt-cloud -f list_nodes_min my-linode-config
salt-cloud --function list_nodes_min my-linode-config
"""
if call == "action":
raise SaltCloudSystemExit(
"The list_nodes_min function must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().list_nodes_min()
def list_nodes_select(call=None):
"""
Return a list of the VMs that are on the provider, with select fields.
"""
return LinodeAPIv4.get_api_instance().list_nodes_select(call)
def reboot(name, call=None):
"""
Reboot a linode.
.. versionadded:: 2015.8.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 show_instance action must be called with -a or --action."
)
return LinodeAPIv4.get_api_instance().reboot(name)
def show_instance(name, call=None):
"""
Displays details about a particular Linode VM. Either a name or a linode_id must
be provided.
.. versionadded:: 2015.8.0
name
The name of the VM for which to display details.
CLI Example:
.. code-block:: bash
salt-cloud -a show_instance vm_name
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
"""
if call != "action":
raise SaltCloudException(
"The show_instance action must be called with -a or --action."
)
return LinodeAPIv4.get_api_instance().show_instance(name)
def show_pricing(kwargs=None, call=None):
"""
Show pricing for a particular profile. This is only an estimate, based on
unofficial pricing sources.
.. versionadded:: 2015.8.0
CLI Example:
.. code-block:: bash
salt-cloud -f show_pricing my-linode-config profile=my-linode-profile
"""
if call != "function":
raise SaltCloudException(
"The show_instance action must be called with -f or --function."
)
return LinodeAPIv4.get_api_instance().show_pricing(kwargs=kwargs)
def start(name, call=None):
"""
Start a VM in Linode.
name
The name of the VM to start.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
"""
if call != "action":
raise SaltCloudException("The start action must be called with -a or --action.")
return LinodeAPIv4.get_api_instance().start(name)
def stop(name, call=None):
"""
Stop a VM in Linode.
name
The name of the VM to stop.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
"""
if call != "action":
raise SaltCloudException("The stop action must be called with -a or --action.")
return LinodeAPIv4.get_api_instance().stop(name)
Zerion Mini Shell 1.0