Mini Shell
"""
Proxmox Cloud Module
======================
.. versionadded:: 2014.7.0
The Proxmox cloud module is used to control access to cloud providers using
the Proxmox system (KVM / OpenVZ / LXC).
Set up the cloud configuration at ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/proxmox.conf``:
.. code-block:: yaml
my-proxmox-config:
# Proxmox account information
user: myuser@pam or myuser@pve
password: mypassword
url: hypervisor.domain.tld
port: 8006
driver: proxmox
verify_ssl: True
.. warning::
This cloud provider will be removed from Salt in version 3009.0 in favor of
the `saltext.proxmox Salt Extension
<https://github.com/salt-extensions/saltext-proxmox>`_
:maintainer: Frank Klaassen <frank@cloudright.nl>
:depends: requests >= 2.2.1
:depends: IPy >= 0.81
"""
import logging
import pprint
import re
import socket
import time
import urllib
import salt.config as config
import salt.utils.cloud
import salt.utils.json
from salt.exceptions import (
SaltCloudExecutionFailure,
SaltCloudExecutionTimeout,
SaltCloudSystemExit,
)
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
from IPy import IP
HAS_IPY = True
except ImportError:
HAS_IPY = False
# Get logging started
log = logging.getLogger(__name__)
__virtualname__ = "proxmox"
__deprecated__ = (
3009,
"proxmox",
"https://github.com/salt-extensions/saltext-proxmox",
)
def __virtual__():
"""
Check for PROXMOX configurations
"""
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_configured_provider():
"""
Return the first configured instance.
"""
return config.is_provider_configured(
__opts__, _get_active_provider_name() or __virtualname__, ("user",)
)
def get_dependencies():
"""
Warn if dependencies aren't met.
"""
deps = {"requests": HAS_REQUESTS, "IPy": HAS_IPY}
return config.check_driver_dependencies(__virtualname__, deps)
url = None
port = None
ticket = None
csrf = None
verify_ssl = None
api = None
def _authenticate():
"""
Retrieve CSRF and API tickets for the Proxmox API
"""
global url, port, ticket, csrf, verify_ssl
url = config.get_cloud_config_value(
"url", get_configured_provider(), __opts__, search_global=False
)
port = config.get_cloud_config_value(
"port", get_configured_provider(), __opts__, default=8006, search_global=False
)
username = (
config.get_cloud_config_value(
"user", get_configured_provider(), __opts__, search_global=False
),
)
passwd = config.get_cloud_config_value(
"password", get_configured_provider(), __opts__, search_global=False
)
verify_ssl = config.get_cloud_config_value(
"verify_ssl",
get_configured_provider(),
__opts__,
default=True,
search_global=False,
)
connect_data = {"username": username, "password": passwd}
full_url = f"https://{url}:{port}/api2/json/access/ticket"
response = requests.post(
full_url, verify=verify_ssl, data=connect_data, timeout=120
)
response.raise_for_status()
returned_data = response.json()
ticket = {"PVEAuthCookie": returned_data["data"]["ticket"]}
csrf = str(returned_data["data"]["CSRFPreventionToken"])
def query(conn_type, option, post_data=None):
"""
Execute the HTTP request to the API
"""
if ticket is None or csrf is None or url is None:
log.debug("Not authenticated yet, doing that now..")
_authenticate()
full_url = f"https://{url}:{port}/api2/json/{option}"
log.debug("%s: %s (%s)", conn_type, full_url, post_data)
httpheaders = {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "salt-cloud-proxmox",
}
if conn_type == "post":
httpheaders["CSRFPreventionToken"] = csrf
response = requests.post(
full_url,
verify=verify_ssl,
data=post_data,
cookies=ticket,
headers=httpheaders,
timeout=120,
)
elif conn_type == "put":
httpheaders["CSRFPreventionToken"] = csrf
response = requests.put(
full_url,
verify=verify_ssl,
data=post_data,
cookies=ticket,
headers=httpheaders,
timeout=120,
)
elif conn_type == "delete":
httpheaders["CSRFPreventionToken"] = csrf
response = requests.delete(
full_url,
verify=verify_ssl,
data=post_data,
cookies=ticket,
headers=httpheaders,
timeout=120,
)
elif conn_type == "get":
response = requests.get(
full_url, verify=verify_ssl, cookies=ticket, timeout=120
)
try:
response.raise_for_status()
except requests.exceptions.RequestException:
# Log the details of the response.
log.error("Error in %s query to %s:\n%s", conn_type, full_url, response.text)
raise
try:
returned_data = response.json()
if "data" not in returned_data:
raise SaltCloudExecutionFailure
return returned_data["data"]
except Exception: # pylint: disable=broad-except
log.error("Error in trying to process JSON")
log.error(response)
def _get_vm_by_name(name, allDetails=False):
"""
Since Proxmox works based op id's rather than names as identifiers this
requires some filtering to retrieve the required information.
"""
vms = get_resources_vms(includeConfig=allDetails)
if name in vms:
return vms[name]
log.info('VM with name "%s" could not be found.', name)
return False
def _get_vm_by_id(vmid, allDetails=False):
"""
Retrieve a VM based on the ID.
"""
for vm_name, vm_details in get_resources_vms(includeConfig=allDetails).items():
if str(vm_details["vmid"]) == str(vmid):
return vm_details
log.info('VM with ID "%s" could not be found.', vmid)
return False
def _get_next_vmid():
"""
Proxmox allows the use of alternative ids instead of autoincrementing.
Because of that its required to query what the first available ID is.
"""
return int(query("get", "cluster/nextid"))
def _check_ip_available(ip_addr):
"""
Proxmox VMs refuse to start when the IP is already being used.
This function can be used to prevent VMs being created with duplicate
IP's or to generate a warning.
"""
for vm_name, vm_details in get_resources_vms(includeConfig=True).items():
vm_config = vm_details["config"]
if ip_addr in vm_config["ip_address"] or vm_config["ip_address"] == ip_addr:
log.debug('IP "%s" is already defined', ip_addr)
return False
log.debug("IP '%s' is available to be defined", ip_addr)
return True
def _parse_proxmox_upid(node, vm_=None):
"""
Upon requesting a task that runs for a longer period of time a UPID is given.
This includes information about the job and can be used to lookup information in the log.
"""
ret = {}
upid = node
# Parse node response
node = node.split(":")
if node[0] == "UPID":
ret["node"] = str(node[1])
ret["pid"] = str(node[2])
ret["pstart"] = str(node[3])
ret["starttime"] = str(node[4])
ret["type"] = str(node[5])
ret["vmid"] = str(node[6])
ret["user"] = str(node[7])
# include the upid again in case we'll need it again
ret["upid"] = str(upid)
if vm_ is not None and "technology" in vm_:
ret["technology"] = str(vm_["technology"])
return ret
def _lookup_proxmox_task(upid):
"""
Retrieve the (latest) logs and retrieve the status for a UPID.
This can be used to verify whether a task has completed.
"""
log.debug("Getting creation status for upid: %s", upid)
tasks = query("get", "cluster/tasks")
if tasks:
for task in tasks:
if task["upid"] == upid:
log.debug("Found upid task: %s", task)
return task
return False
def get_resources_nodes(call=None, resFilter=None):
"""
Retrieve all hypervisors (nodes) available on this environment
CLI Example:
.. code-block:: bash
salt-cloud -f get_resources_nodes my-proxmox-config
"""
log.debug("Getting resource: nodes.. (filter: %s)", resFilter)
resources = query("get", "cluster/resources")
ret = {}
for resource in resources:
if "type" in resource and resource["type"] == "node":
name = resource["node"]
ret[name] = resource
if resFilter is not None:
log.debug("Filter given: %s, returning requested resource: nodes", resFilter)
return ret[resFilter]
log.debug("Filter not given: %s, returning all resource: nodes", ret)
return ret
def get_resources_vms(call=None, resFilter=None, includeConfig=True):
"""
Retrieve all VMs available on this environment
CLI Example:
.. code-block:: bash
salt-cloud -f get_resources_vms my-proxmox-config
"""
timeoutTime = time.time() + 60
while True:
log.debug("Getting resource: vms.. (filter: %s)", resFilter)
resources = query("get", "cluster/resources")
ret = {}
badResource = False
for resource in resources:
if "type" in resource and resource["type"] in ["openvz", "qemu", "lxc"]:
try:
name = resource["name"]
except KeyError:
badResource = True
log.debug("No name in VM resource %s", repr(resource))
break
ret[name] = resource
if includeConfig:
# Requested to include the detailed configuration of a VM
ret[name]["config"] = get_vmconfig(
ret[name]["vmid"], ret[name]["node"], ret[name]["type"]
)
if time.time() > timeoutTime:
raise SaltCloudExecutionTimeout("FAILED to get the proxmox resources vms")
# Carry on if there wasn't a bad resource return from Proxmox
if not badResource:
break
time.sleep(0.5)
if resFilter is not None:
log.debug("Filter given: %s, returning requested resource: nodes", resFilter)
return ret[resFilter]
log.debug("Filter not given: %s, returning all resource: nodes", ret)
return ret
def script(vm_):
"""
Return the script deployment object
"""
script_name = config.get_cloud_config_value("script", vm_, __opts__)
if not script_name:
script_name = "bootstrap-salt"
return salt.utils.cloud.os_script(
script_name,
vm_,
__opts__,
salt.utils.cloud.salt_config_to_yaml(
salt.utils.cloud.minion_config(__opts__, vm_)
),
)
def avail_locations(call=None):
"""
Return a list of the hypervisors (nodes) which this Proxmox PVE machine manages
CLI Example:
.. code-block:: bash
salt-cloud --list-locations my-proxmox-config
"""
if call == "action":
raise SaltCloudSystemExit(
"The avail_locations function must be called with "
"-f or --function, or with the --list-locations option"
)
# could also use the get_resources_nodes but speed is ~the same
nodes = query("get", "nodes")
ret = {}
for node in nodes:
name = node["node"]
ret[name] = node
return ret
def avail_images(call=None, location="local"):
"""
Return a list of the images that are on the provider
CLI Example:
.. code-block:: bash
salt-cloud --list-images my-proxmox-config
"""
if call == "action":
raise SaltCloudSystemExit(
"The avail_images function must be called with "
"-f or --function, or with the --list-images option"
)
ret = {}
for host_name, host_details in avail_locations().items():
for item in query("get", f"nodes/{host_name}/storage/{location}/content"):
ret[item["volid"]] = item
return ret
def list_nodes(call=None):
"""
Return a list of the VMs that are managed by the provider
CLI Example:
.. code-block:: bash
salt-cloud -Q my-proxmox-config
"""
if call == "action":
raise SaltCloudSystemExit(
"The list_nodes function must be called with -f or --function."
)
ret = {}
for vm_name, vm_details in get_resources_vms(includeConfig=True).items():
log.debug("VM_Name: %s", vm_name)
log.debug("vm_details: %s", vm_details)
# Limit resultset on what Salt-cloud demands:
ret[vm_name] = {}
ret[vm_name]["id"] = str(vm_details["vmid"])
ret[vm_name]["image"] = str(vm_details["vmid"])
ret[vm_name]["size"] = str(vm_details["disk"])
ret[vm_name]["state"] = str(vm_details["status"])
# Figure out which is which to put it in the right column
private_ips = []
public_ips = []
if (
"ip_address" in vm_details["config"]
and vm_details["config"]["ip_address"] != "-"
):
ips = vm_details["config"]["ip_address"].split(" ")
for ip_ in ips:
if IP(ip_).iptype() == "PRIVATE":
private_ips.append(str(ip_))
else:
public_ips.append(str(ip_))
ret[vm_name]["private_ips"] = private_ips
ret[vm_name]["public_ips"] = public_ips
return ret
def list_nodes_full(call=None):
"""
Return a list of the VMs that are on the provider
CLI Example:
.. code-block:: bash
salt-cloud -F my-proxmox-config
"""
if call == "action":
raise SaltCloudSystemExit(
"The list_nodes_full function must be called with -f or --function."
)
return get_resources_vms(includeConfig=True)
def list_nodes_select(call=None):
"""
Return a list of the VMs that are on the provider, with select fields
CLI Example:
.. code-block:: bash
salt-cloud -S my-proxmox-config
"""
return salt.utils.cloud.list_nodes_select(
list_nodes_full(),
__opts__["query.selection"],
call,
)
def _stringlist_to_dictionary(input_string):
"""
Convert a stringlist (comma separated settings) to a dictionary
The result of the string setting1=value1,setting2=value2 will be a python dictionary:
{'setting1':'value1','setting2':'value2'}
"""
return dict(item.strip().split("=") for item in input_string.split(",") if item)
def _dictionary_to_stringlist(input_dict):
"""
Convert a dictionary to a stringlist (comma separated settings)
The result of the dictionary {'setting1':'value1','setting2':'value2'} will be:
setting1=value1,setting2=value2
"""
return ",".join(f"{k}={input_dict[k]}" for k in sorted(input_dict.keys()))
def _reconfigure_clone(vm_, vmid):
"""
If we cloned a machine, see if we need to reconfigure any of the options such as net0,
ide2, etc. This enables us to have a different cloud-init ISO mounted for each VM that's brought up
:param vm_:
:return:
"""
if not vm_.get("technology") == "qemu":
log.warning("Reconfiguring clones is only available under `qemu`")
return
# Determine which settings can be reconfigured.
query_path = "nodes/{}/qemu/{}/config"
valid_settings = set(_get_properties(query_path.format("{node}", "{vmid}"), "POST"))
log.info("Configuring cloned VM")
# Modify the settings for the VM one at a time so we can see any problems with the values
# as quickly as possible
for setting in vm_:
postParams = None
if setting == "vmid":
pass # vmid gets passed in the URL and can't be reconfigured
elif re.match(r"^net(\d+)$", setting):
# net strings are a list of comma seperated settings. We need to merge the settings so that
# the setting in the profile only changes the settings it touches and the other settings
# are left alone. An example of why this is necessary is because the MAC address is set
# in here and generally you don't want to alter or have to know the MAC address of the new
# instance, but you may want to set the VLAN bridge
data = query("get", "nodes/{}/qemu/{}/config".format(vm_["host"], vmid))
# Generate a dictionary of settings from the existing string
new_setting = {}
if setting in data:
new_setting.update(_stringlist_to_dictionary(data[setting]))
# Merge the new settings (as a dictionary) into the existing dictionary to get the
# new merged settings
new_setting.update(_stringlist_to_dictionary(vm_[setting]))
# Convert the dictionary back into a string list
postParams = {setting: _dictionary_to_stringlist(new_setting)}
elif setting == "sshkeys":
postParams = {setting: urllib.parse.quote(vm_[setting], safe="")}
elif setting in valid_settings:
postParams = {setting: vm_[setting]}
if postParams:
query(
"post",
"nodes/{}/qemu/{}/config".format(vm_["host"], vmid),
postParams,
)
def create(vm_):
"""
Create a single VM from a data dict
CLI Example:
.. code-block:: bash
salt-cloud -p proxmox-ubuntu vmhostname
"""
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 "proxmox",
vm_["profile"],
vm_=vm_,
)
is False
):
return False
except AttributeError:
pass
ret = {}
__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"])
if "use_dns" in vm_ and "ip_address" not in vm_:
use_dns = vm_["use_dns"]
if use_dns:
from socket import gaierror, gethostbyname
try:
ip_address = gethostbyname(str(vm_["name"]))
except gaierror:
log.debug("Resolving of %s failed", vm_["name"])
else:
vm_["ip_address"] = str(ip_address)
try:
newid = _get_next_vmid()
data = create_node(vm_, newid)
except Exception as exc: # pylint: disable=broad-except
msg = str(exc)
if (
isinstance(exc, requests.exceptions.RequestException)
and exc.response is not None
):
msg = msg + "\n" + exc.response.text
log.error(
"Error creating %s on PROXMOX\n\n"
"The following exception was thrown when trying to "
"run the initial deployment: \n%s",
vm_["name"],
msg,
# Show the traceback if the debug logging level is enabled
exc_info_on_loglevel=logging.DEBUG,
)
return False
ret["creation_data"] = data
name = vm_["name"] # hostname which we know
vmid = data["vmid"] # vmid which we have received
host = data["node"] # host which we have received
nodeType = data["technology"] # VM tech (Qemu / OpenVZ)
agent_get_ip = vm_.get("agent_get_ip", False)
if agent_get_ip is False:
# Determine which IP to use in order of preference:
if "ip_address" in vm_:
ip_address = str(vm_["ip_address"])
elif "public_ips" in data:
ip_address = str(data["public_ips"][0]) # first IP
elif "private_ips" in data:
ip_address = str(data["private_ips"][0]) # first IP
else:
raise SaltCloudExecutionFailure("Could not determine an IP address to use")
log.debug("Using IP address %s", ip_address)
# wait until the vm has been created so we can start it
if not wait_for_created(data["upid"], timeout=300):
return {"Error": f"Unable to create {name}, command timed out"}
if vm_.get("clone") is True:
_reconfigure_clone(vm_, vmid)
# VM has been created. Starting..
if not start(name, vmid, call="action"):
log.error("Node %s (%s) failed to start!", name, vmid)
raise SaltCloudExecutionFailure
# Wait until the VM has fully started
log.debug('Waiting for state "running" for vm %s on %s', vmid, host)
if not wait_for_state(vmid, "running"):
return {"Error": f"Unable to start {name}, command timed out"}
if agent_get_ip is True:
try:
ip_address = salt.utils.cloud.wait_for_fun(
_find_agent_ip, vm_=vm_, vmid=vmid
)
except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc:
try:
# If VM was created but we can't connect, destroy it.
destroy(vm_["name"])
except SaltCloudSystemExit:
pass
finally:
raise SaltCloudSystemExit(str(exc))
log.debug("Using IP address %s", ip_address)
ssh_username = config.get_cloud_config_value(
"ssh_username", vm_, __opts__, default="root"
)
ssh_password = config.get_cloud_config_value(
"password",
vm_,
__opts__,
)
ret["ip_address"] = ip_address
ret["username"] = ssh_username
ret["password"] = ssh_password
vm_["ssh_host"] = ip_address
vm_["password"] = ssh_password
ret = __utils__["cloud.bootstrap"](vm_, __opts__)
# Report success!
log.info("Created Cloud VM '%s'", vm_["name"])
log.debug("'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data))
__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"],
)
return ret
def preferred_ip(vm_, ips):
"""
Return either an 'ipv4' (default) or 'ipv6' address depending on 'protocol' option.
The list of 'ipv4' IPs is filtered by ignore_cidr() to remove any unreachable private addresses.
"""
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:
ignore_ip = ignore_cidr(vm_, ip)
if ignore_ip:
continue
try:
socket.inet_pton(family, ip)
return ip
except Exception: # pylint: disable=broad-except
continue
return False
def ignore_cidr(vm_, ip):
"""
Return True if we are to ignore the specified IP.
"""
from ipaddress import ip_address, ip_network
cidrs = config.get_cloud_config_value(
"ignore_cidr", vm_, __opts__, default=[], search_global=False
)
if cidrs and isinstance(cidrs, str):
cidrs = [cidrs]
for cidr in cidrs or []:
if ip_address(ip) in ip_network(cidr):
log.warning("IP %r found within %r; ignoring it.", ip, cidr)
return True
return False
def _find_agent_ip(vm_, vmid):
"""
If VM is started we would return the IP-addresses that are returned by the qemu agent on the VM.
"""
# This functionality is only available on qemu
if not vm_.get("technology") == "qemu":
log.warning("Find agent IP is only available under `qemu`")
return
# Create an empty list of IP-addresses:
ips = []
endpoint = "nodes/{}/qemu/{}/agent/network-get-interfaces".format(vm_["host"], vmid)
interfaces = query("get", endpoint)
# If we get a result from the agent, parse it
for interface in interfaces["result"]:
# Skip interface if hardware-address is 00:00:00:00:00:00 (loopback interface)
if str(interface.get("hardware-address")) == "00:00:00:00:00:00":
continue
# Skip entries without ip-addresses information
if "ip-addresses" not in interface:
continue
for if_addr in interface["ip-addresses"]:
ip_addr = if_addr.get("ip-address")
if ip_addr is not None:
ips.append(str(ip_addr))
if len(ips) > 0:
return preferred_ip(vm_, ips)
raise SaltCloudExecutionFailure
def _import_api():
"""
Download https://<url>/pve-docs/api-viewer/apidoc.js
Extract content of pveapi var (json formatted)
Load this json content into global variable "api"
"""
global api
full_url = f"https://{url}:{port}/pve-docs/api-viewer/apidoc.js"
returned_data = requests.get(full_url, verify=verify_ssl, timeout=120)
re_filter = re.compile(" (?:pveapi|apiSchema) = (.*)^;", re.DOTALL | re.MULTILINE)
api_json = re_filter.findall(returned_data.text)[0]
api = salt.utils.json.loads(api_json)
def _get_properties(path="", method="GET", forced_params=None):
"""
Return the parameter list from api for defined path and HTTP method
"""
if api is None:
_import_api()
sub = api
path_levels = [level for level in path.split("/") if level != ""]
search_path = ""
props = []
parameters = set([] if forced_params is None else forced_params)
# Browse all path elements but last
for elem in path_levels[:-1]:
search_path += "/" + elem
# Lookup for a dictionary with path = "requested path" in list" and return its children
sub = next(item for item in sub if item["path"] == search_path)["children"]
# Get leaf element in path
search_path += "/" + path_levels[-1]
sub = next(item for item in sub if item["path"] == search_path)
try:
# get list of properties for requested method
props = sub["info"][method]["parameters"]["properties"].keys()
except KeyError as exc:
log.error('method not found: "%s"', exc)
for prop in props:
numerical = re.match(r"(\w+)\[n\]", prop)
# generate (arbitrarily) 10 properties for duplicatable properties identified by:
# "prop[n]"
if numerical:
for i in range(10):
parameters.add(numerical.group(1) + str(i))
else:
parameters.add(prop)
return parameters
def create_node(vm_, newid):
"""
Build and submit the requestdata to create a new node
"""
newnode = {}
if "technology" not in vm_:
vm_["technology"] = "openvz" # default virt tech if none is given
if vm_["technology"] not in ["qemu", "openvz", "lxc"]:
# Wrong VM type given
log.error(
"Wrong VM type. Valid options are: qemu, openvz (proxmox3) or lxc"
" (proxmox4)"
)
raise SaltCloudExecutionFailure
if "host" not in vm_:
# Use globally configured/default location
vm_["host"] = config.get_cloud_config_value(
"default_host", get_configured_provider(), __opts__, search_global=False
)
if vm_["host"] is None:
# No location given for the profile
log.error("No host given to create this VM on")
raise SaltCloudExecutionFailure
# Required by both OpenVZ and Qemu (KVM)
vmhost = vm_["host"]
newnode["vmid"] = newid
for prop in "cpuunits", "description", "memory", "onboot":
if prop in vm_: # if the property is set, use it for the VM request
newnode[prop] = vm_[prop]
if vm_["technology"] == "openvz":
# OpenVZ related settings, using non-default names:
newnode["hostname"] = vm_["name"]
newnode["ostemplate"] = vm_["image"]
# optional VZ settings
for prop in (
"cpus",
"disk",
"ip_address",
"nameserver",
"password",
"swap",
"poolid",
"storage",
):
if prop in vm_: # if the property is set, use it for the VM request
newnode[prop] = vm_[prop]
elif vm_["technology"] == "lxc":
# LXC related settings, using non-default names:
newnode["hostname"] = vm_["name"]
newnode["ostemplate"] = vm_["image"]
static_props = (
"cpuunits",
"cpulimit",
"rootfs",
"cores",
"description",
"memory",
"onboot",
"net0",
"password",
"nameserver",
"swap",
"storage",
"rootfs",
)
for prop in _get_properties("/nodes/{node}/lxc", "POST", static_props):
if prop in vm_: # if the property is set, use it for the VM request
newnode[prop] = vm_[prop]
if "pubkey" in vm_:
newnode["ssh-public-keys"] = vm_["pubkey"]
# inform user the "disk" option is not supported for LXC hosts
if "disk" in vm_:
log.warning(
'The "disk" option is not supported for LXC hosts and was ignored'
)
# LXC specific network config
# OpenVZ allowed specifying IP and gateway. To ease migration from
# Proxmox 3, I've mapped the ip_address and gw to a generic net0 config.
# If you need more control, please use the net0 option directly.
# This also assumes a /24 subnet.
if "ip_address" in vm_ and "net0" not in vm_:
newnode["net0"] = (
"bridge=vmbr0,ip=" + vm_["ip_address"] + "/24,name=eth0,type=veth"
)
# gateway is optional and does not assume a default
if "gw" in vm_:
newnode["net0"] = newnode["net0"] + ",gw=" + vm_["gw"]
elif vm_["technology"] == "qemu":
# optional Qemu settings
static_props = (
"acpi",
"cores",
"cpu",
"pool",
"storage",
"sata0",
"ostype",
"ide2",
"net0",
)
for prop in _get_properties("/nodes/{node}/qemu", "POST", static_props):
if prop in vm_: # if the property is set, use it for the VM request
# If specified, vmid will override newid.
newnode[prop] = vm_[prop]
# The node is ready. Lets request it to be added
__utils__["cloud.fire_event"](
"event",
"requesting instance",
"salt/cloud/{}/requesting".format(vm_["name"]),
args={
"kwargs": __utils__["cloud.filter_event"](
"requesting", newnode, list(newnode)
),
},
sock_dir=__opts__["sock_dir"],
)
log.debug("Preparing to generate a node using these parameters: %s ", newnode)
if "clone" in vm_ and vm_["clone"] is True and vm_["technology"] == "qemu":
postParams = {}
postParams["newid"] = newnode["vmid"]
if "pool" in vm_:
postParams["pool"] = vm_["pool"]
for prop in "description", "format", "full", "name":
if (
"clone_" + prop in vm_
): # if the property is set, use it for the VM request
postParams[prop] = vm_["clone_" + prop]
try:
int(vm_["clone_from"])
except ValueError:
if ":" in vm_["clone_from"]:
vmhost = vm_["clone_from"].split(":")[0]
vm_["clone_from"] = vm_["clone_from"].split(":")[1]
node = query(
"post",
"nodes/{}/qemu/{}/clone".format(vmhost, vm_["clone_from"]),
postParams,
)
else:
node = query("post", "nodes/{}/{}".format(vmhost, vm_["technology"]), newnode)
result = _parse_proxmox_upid(node, vm_)
# When cloning, the upid contains the clone_from vmid instead of the new vmid
result["vmid"] = newnode["vmid"]
return result
def show_instance(name, call=None):
"""
Show the details from Proxmox concerning an instance
"""
if call != "action":
raise SaltCloudSystemExit(
"The show_instance action must be called with -a or --action."
)
nodes = list_nodes_full()
__utils__["cloud.cache_node"](nodes[name], _get_active_provider_name(), __opts__)
return nodes[name]
def get_vmconfig(vmid, node=None, node_type="openvz"):
"""
Get VM configuration
"""
if node is None:
# We need to figure out which node this VM is on.
for host_name, host_details in avail_locations().items():
for item in query("get", f"nodes/{host_name}/{node_type}"):
if item["vmid"] == vmid:
node = host_name
# If we reached this point, we have all the information we need
data = query("get", f"nodes/{node}/{node_type}/{vmid}/config")
return data
def wait_for_created(upid, timeout=300):
"""
Wait until a the vm has been created successfully
"""
start_time = time.time()
info = _lookup_proxmox_task(upid)
if not info:
log.error(
"wait_for_created: No task information retrieved based on given criteria."
)
raise SaltCloudExecutionFailure
while True:
if "status" in info and info["status"] == "OK":
log.debug("Host has been created!")
return True
time.sleep(3) # Little more patience, we're not in a hurry
if time.time() - start_time > timeout:
log.debug("Timeout reached while waiting for host to be created")
return False
info = _lookup_proxmox_task(upid)
def wait_for_state(vmid, state, timeout=300):
"""
Wait until a specific state has been reached on a node
"""
start_time = time.time()
node = get_vm_status(vmid=vmid)
if not node:
log.error("wait_for_state: No VM retrieved based on given criteria.")
raise SaltCloudExecutionFailure
while True:
if node["status"] == state:
log.debug('Host %s is now in "%s" state!', node["name"], state)
return True
time.sleep(1)
if time.time() - start_time > timeout:
log.debug(
"Timeout reached while waiting for %s to become %s", node["name"], state
)
return False
node = get_vm_status(vmid=vmid)
log.debug(
'State for %s is: "%s" instead of "%s"', node["name"], node["status"], state
)
def destroy(name, call=None):
"""
Destroy a node.
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."
)
__utils__["cloud.fire_event"](
"event",
"destroying instance",
f"salt/cloud/{name}/destroying",
args={"name": name},
sock_dir=__opts__["sock_dir"],
transport=__opts__["transport"],
)
vmobj = _get_vm_by_name(name)
if vmobj is not None:
# stop the vm
if get_vm_status(vmid=vmobj["vmid"])["status"] != "stopped":
stop(name, vmobj["vmid"], "action")
# wait until stopped
if not wait_for_state(vmobj["vmid"], "stopped"):
return {"Error": f"Unable to stop {name}, command timed out"}
# required to wait a bit here, otherwise the VM is sometimes
# still locked and destroy fails.
time.sleep(3)
query("delete", "nodes/{}/{}".format(vmobj["node"], vmobj["id"]))
__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__
)
return {"Destroyed": f"{name} was destroyed."}
def set_vm_status(status, name=None, vmid=None):
"""
Convenience function for setting VM status
"""
log.debug("Set status to %s for %s (%s)", status, name, vmid)
if vmid is not None:
log.debug("set_vm_status: via ID - VMID %s (%s): %s", vmid, name, status)
vmobj = _get_vm_by_id(vmid)
else:
log.debug("set_vm_status: via name - VMID %s (%s): %s", vmid, name, status)
vmobj = _get_vm_by_name(name)
if not vmobj or "node" not in vmobj or "type" not in vmobj or "vmid" not in vmobj:
log.error("Unable to set status %s for %s (%s)", status, name, vmid)
raise SaltCloudExecutionTimeout
log.debug("VM_STATUS: Has desired info (%s). Setting status..", vmobj)
data = query(
"post",
"nodes/{}/{}/{}/status/{}".format(
vmobj["node"], vmobj["type"], vmobj["vmid"], status
),
)
result = _parse_proxmox_upid(data, vmobj)
if result is not False and result is not None:
log.debug("Set_vm_status action result: %s", result)
return True
return False
def get_vm_status(vmid=None, name=None):
"""
Get the status for a VM, either via the ID or the hostname
"""
if vmid is not None:
log.debug("get_vm_status: VMID %s", vmid)
vmobj = _get_vm_by_id(vmid)
elif name is not None:
log.debug("get_vm_status: name %s", name)
vmobj = _get_vm_by_name(name)
else:
log.debug("get_vm_status: No ID or NAME given")
raise SaltCloudExecutionFailure
log.debug("VM found: %s", vmobj)
if vmobj is not None and "node" in vmobj:
log.debug("VM_STATUS: Has desired info. Retrieving.. (%s)", vmobj["name"])
data = query(
"get",
"nodes/{}/{}/{}/status/current".format(
vmobj["node"], vmobj["type"], vmobj["vmid"]
),
)
return data
log.error("VM or requested status not found..")
return False
def start(name, vmid=None, call=None):
"""
Start a node.
CLI Example:
.. code-block:: bash
salt-cloud -a start mymachine
"""
if call != "action":
raise SaltCloudSystemExit(
"The start action must be called with -a or --action."
)
log.debug("Start: %s (%s) = Start", name, vmid)
if not set_vm_status("start", name, vmid=vmid):
log.error("Unable to bring VM %s (%s) up..", name, vmid)
raise SaltCloudExecutionFailure
# xxx: TBD: Check here whether the status was actually changed to 'started'
return {"Started": f"{name} was started."}
def stop(name, vmid=None, call=None):
"""
Stop a node ("pulling the plug").
CLI Example:
.. code-block:: bash
salt-cloud -a stop mymachine
"""
if call != "action":
raise SaltCloudSystemExit("The stop action must be called with -a or --action.")
if not set_vm_status("stop", name, vmid=vmid):
log.error("Unable to bring VM %s (%s) down..", name, vmid)
raise SaltCloudExecutionFailure
# xxx: TBD: Check here whether the status was actually changed to 'stopped'
return {"Stopped": f"{name} was stopped."}
def shutdown(name=None, vmid=None, call=None):
"""
Shutdown a node via ACPI.
CLI Example:
.. code-block:: bash
salt-cloud -a shutdown mymachine
"""
if call != "action":
raise SaltCloudSystemExit(
"The shutdown action must be called with -a or --action."
)
if not set_vm_status("shutdown", name, vmid=vmid):
log.error("Unable to shut VM %s (%s) down..", name, vmid)
raise SaltCloudExecutionFailure
# xxx: TBD: Check here whether the status was actually changed to 'stopped'
return {"Shutdown": f"{name} was shutdown."}
Zerion Mini Shell 1.0