mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
625 lines
16 KiB
Python
625 lines
16 KiB
Python
"""
|
|
Packet Cloud Module Using Packet's Python API Client
|
|
====================================================
|
|
|
|
The Packet cloud module is used to control access to the Packet VPS system.
|
|
|
|
Use of this module only requires the ``token`` parameter.
|
|
|
|
Set up the cloud configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/packet.conf``:
|
|
|
|
The Packet profile requires ``size``, ``image``, ``location``, ``project_id``
|
|
|
|
Optional profile parameters:
|
|
|
|
- ``storage_size`` - min value is 10, defines Gigabytes of storage that will be attached to device.
|
|
- ``storage_tier`` - storage_1 - Standard Plan, storage_2 - Performance Plan
|
|
- ``snapshot_count`` - int
|
|
- ``snapshot_frequency`` - string - possible values:
|
|
|
|
- 1min
|
|
- 15min
|
|
- 1hour
|
|
- 1day
|
|
- 1week
|
|
- 1month
|
|
- 1year
|
|
|
|
This driver requires Packet's client library: https://pypi.python.org/pypi/packet-python
|
|
|
|
.. code-block:: yaml
|
|
|
|
packet-provider:
|
|
minion:
|
|
master: 192.168.50.10
|
|
driver: packet
|
|
token: ewr23rdf35wC8oNjJrhmHa87rjSXzJyi
|
|
private_key: /root/.ssh/id_rsa
|
|
|
|
packet-profile:
|
|
provider: packet-provider
|
|
size: baremetal_0
|
|
image: ubuntu_16_04_image
|
|
location: ewr1
|
|
project_id: a64d000b-d47c-4d26-9870-46aac43010a6
|
|
storage_size: 10
|
|
storage_tier: storage_1
|
|
storage_snapshot_count: 1
|
|
storage_snapshot_frequency: 15min
|
|
"""
|
|
|
|
import logging
|
|
import pprint
|
|
import time
|
|
|
|
import salt.config as config
|
|
import salt.utils.cloud
|
|
from salt.cloud.libcloudfuncs import get_image, get_size, script, show_instance
|
|
from salt.exceptions import SaltCloudException, SaltCloudSystemExit
|
|
from salt.utils.functools import namespaced_function
|
|
|
|
try:
|
|
import packet
|
|
|
|
HAS_PACKET = True
|
|
except ImportError:
|
|
HAS_PACKET = False
|
|
|
|
|
|
get_size = namespaced_function(get_size, globals())
|
|
get_image = namespaced_function(get_image, globals())
|
|
|
|
script = namespaced_function(script, globals())
|
|
|
|
show_instance = namespaced_function(show_instance, globals())
|
|
|
|
# Get logging started
|
|
log = logging.getLogger(__name__)
|
|
|
|
__virtualname__ = "packet"
|
|
|
|
|
|
# Only load this module if the Packet configuration is in place.
|
|
def __virtual__():
|
|
"""
|
|
Check for Packet configs.
|
|
"""
|
|
if HAS_PACKET is False:
|
|
return False, "The packet python library is not installed"
|
|
if get_configured_provider() 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__, ("token",)
|
|
)
|
|
|
|
|
|
def avail_images(call=None):
|
|
"""
|
|
Return available Packet os images.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud --list-images packet-provider
|
|
salt-cloud -f avail_images packet-provider
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The avail_images function must be called with -f or --function."
|
|
)
|
|
|
|
ret = {}
|
|
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
ret = {}
|
|
|
|
for os_system in manager.list_operating_systems():
|
|
ret[os_system.name] = os_system.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
def avail_locations(call=None):
|
|
"""
|
|
Return available Packet datacenter locations.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud --list-locations packet-provider
|
|
salt-cloud -f avail_locations packet-provider
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The avail_locations function must be called with -f or --function."
|
|
)
|
|
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
ret = {}
|
|
|
|
for facility in manager.list_facilities():
|
|
ret[facility.name] = facility.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
def avail_sizes(call=None):
|
|
"""
|
|
Return available Packet sizes.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud --list-sizes packet-provider
|
|
salt-cloud -f avail_sizes packet-provider
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The avail_locations function must be called with -f or --function."
|
|
)
|
|
|
|
vm_ = get_configured_provider()
|
|
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
ret = {}
|
|
|
|
for plan in manager.list_plans():
|
|
ret[plan.name] = plan.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
def avail_projects(call=None):
|
|
"""
|
|
Return available Packet projects.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud -f avail_projects packet-provider
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The avail_projects function must be called with -f or --function."
|
|
)
|
|
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
ret = {}
|
|
|
|
for project in manager.list_projects():
|
|
ret[project.name] = project.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
def _wait_for_status(status_type, object_id, status=None, timeout=500, quiet=True):
|
|
"""
|
|
Wait for a certain status from Packet.
|
|
status_type
|
|
device or volume
|
|
object_id
|
|
The ID of the Packet device or volume to wait on. Required.
|
|
status
|
|
The status to wait for.
|
|
timeout
|
|
The amount of time to wait for a status to update.
|
|
quiet
|
|
Log status updates to debug logs when False. Otherwise, logs to info.
|
|
"""
|
|
if status is None:
|
|
status = "ok"
|
|
|
|
interval = 5
|
|
iterations = int(timeout / interval)
|
|
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
for i in range(0, iterations):
|
|
get_object = getattr(
|
|
manager, "get_{status_type}".format(status_type=status_type)
|
|
)
|
|
obj = get_object(object_id)
|
|
|
|
if obj.state == status:
|
|
return obj
|
|
|
|
time.sleep(interval)
|
|
log.log(
|
|
logging.INFO if not quiet else logging.DEBUG,
|
|
"Status for Packet %s is '%s', waiting for '%s'.",
|
|
object_id,
|
|
obj.state,
|
|
status,
|
|
)
|
|
|
|
return obj
|
|
|
|
|
|
def is_profile_configured(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 __virtualname__,
|
|
vm_["profile"],
|
|
vm_=vm_,
|
|
)
|
|
is False
|
|
):
|
|
return False
|
|
|
|
alias, driver = _get_active_provider_name().split(":")
|
|
|
|
profile_data = __opts__["providers"][alias][driver]["profiles"][vm_["profile"]]
|
|
|
|
if profile_data.get("storage_size") or profile_data.get("storage_tier"):
|
|
required_keys = ["storage_size", "storage_tier"]
|
|
|
|
for key in required_keys:
|
|
if profile_data.get(key) is None:
|
|
log.error(
|
|
"both storage_size and storage_tier required for "
|
|
"profile %s. Please check your profile configuration",
|
|
vm_["profile"],
|
|
)
|
|
return False
|
|
|
|
locations = avail_locations()
|
|
|
|
for location in locations.values():
|
|
if location["code"] == profile_data["location"]:
|
|
if "storage" not in location["features"]:
|
|
log.error(
|
|
"Chosen location %s for profile %s does not "
|
|
"support storage feature. Please check your "
|
|
"profile configuration",
|
|
location["code"],
|
|
vm_["profile"],
|
|
)
|
|
return False
|
|
|
|
if profile_data.get("storage_snapshot_count") or profile_data.get(
|
|
"storage_snapshot_frequency"
|
|
):
|
|
required_keys = ["storage_size", "storage_tier"]
|
|
|
|
for key in required_keys:
|
|
if profile_data.get(key) is None:
|
|
log.error(
|
|
"both storage_snapshot_count and "
|
|
"storage_snapshot_frequency required for profile "
|
|
"%s. Please check your profile configuration",
|
|
vm_["profile"],
|
|
)
|
|
return False
|
|
|
|
except AttributeError:
|
|
pass
|
|
|
|
return True
|
|
|
|
|
|
def create(vm_):
|
|
"""
|
|
Create a single Packet VM.
|
|
"""
|
|
name = vm_["name"]
|
|
|
|
if not is_profile_configured(vm_):
|
|
return False
|
|
|
|
__utils__["cloud.fire_event"](
|
|
"event",
|
|
"starting create",
|
|
"salt/cloud/{}/creating".format(name),
|
|
args=__utils__["cloud.filter_event"](
|
|
"creating", vm_, ["name", "profile", "provider", "driver"]
|
|
),
|
|
sock_dir=__opts__["sock_dir"],
|
|
transport=__opts__["transport"],
|
|
)
|
|
|
|
log.info("Creating Packet VM %s", name)
|
|
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
__utils__["cloud.fire_event"](
|
|
"event",
|
|
"requesting instance",
|
|
"salt/cloud/{}/requesting".format(vm_["name"]),
|
|
args=__utils__["cloud.filter_event"](
|
|
"requesting", vm_, ["name", "profile", "provider", "driver"]
|
|
),
|
|
sock_dir=__opts__["sock_dir"],
|
|
transport=__opts__["transport"],
|
|
)
|
|
|
|
device = manager.create_device(
|
|
project_id=vm_["project_id"],
|
|
hostname=name,
|
|
plan=vm_["size"],
|
|
facility=vm_["location"],
|
|
operating_system=vm_["image"],
|
|
)
|
|
|
|
device = _wait_for_status("device", device.id, status="active")
|
|
|
|
if device.state != "active":
|
|
log.error(
|
|
"Error creating %s on PACKET\n\nwhile waiting for initial ready status",
|
|
name,
|
|
exc_info_on_loglevel=logging.DEBUG,
|
|
)
|
|
|
|
# Define which ssh_interface to use
|
|
ssh_interface = _get_ssh_interface(vm_)
|
|
|
|
# Pass the correct IP address to the bootstrap ssh_host key
|
|
if ssh_interface == "private_ips":
|
|
for ip in device.ip_addresses:
|
|
if ip["public"] is False:
|
|
vm_["ssh_host"] = ip["address"]
|
|
break
|
|
else:
|
|
for ip in device.ip_addresses:
|
|
if ip["public"] is True:
|
|
vm_["ssh_host"] = ip["address"]
|
|
break
|
|
|
|
key_filename = config.get_cloud_config_value(
|
|
"private_key", vm_, __opts__, search_global=False, default=None
|
|
)
|
|
|
|
vm_["key_filename"] = key_filename
|
|
|
|
vm_["private_key"] = key_filename
|
|
|
|
# Bootstrap!
|
|
ret = __utils__["cloud.bootstrap"](vm_, __opts__)
|
|
|
|
ret.update({"device": device.__dict__})
|
|
|
|
if vm_.get("storage_tier") and vm_.get("storage_size"):
|
|
# create storage and attach it to device
|
|
|
|
volume = manager.create_volume(
|
|
vm_["project_id"],
|
|
"{}_storage".format(name),
|
|
vm_.get("storage_tier"),
|
|
vm_.get("storage_size"),
|
|
vm_.get("location"),
|
|
snapshot_count=vm_.get("storage_snapshot_count", 0),
|
|
snapshot_frequency=vm_.get("storage_snapshot_frequency"),
|
|
)
|
|
|
|
volume.attach(device.id)
|
|
|
|
volume = _wait_for_status("volume", volume.id, status="active")
|
|
|
|
if volume.state != "active":
|
|
log.error(
|
|
"Error creating %s on PACKET\n\nwhile waiting for initial ready status",
|
|
name,
|
|
exc_info_on_loglevel=logging.DEBUG,
|
|
)
|
|
|
|
ret.update({"volume": volume.__dict__})
|
|
|
|
log.info("Created Cloud VM '%s'", name)
|
|
|
|
log.debug("'%s' VM creation details:\n%s", name, pprint.pformat(device.__dict__))
|
|
|
|
__utils__["cloud.fire_event"](
|
|
"event",
|
|
"created instance",
|
|
"salt/cloud/{}/created".format(name),
|
|
args=__utils__["cloud.filter_event"](
|
|
"created", vm_, ["name", "profile", "provider", "driver"]
|
|
),
|
|
sock_dir=__opts__["sock_dir"],
|
|
transport=__opts__["transport"],
|
|
)
|
|
|
|
return ret
|
|
|
|
|
|
def list_nodes_full(call=None):
|
|
"""
|
|
List devices, with all available information.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud -F
|
|
salt-cloud --full-query
|
|
salt-cloud -f list_nodes_full packet-provider
|
|
|
|
..
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The list_nodes_full function must be called with -f or --function."
|
|
)
|
|
|
|
ret = {}
|
|
|
|
for device in get_devices_by_token():
|
|
ret[device.hostname] = device.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
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 packet-provider
|
|
salt-cloud --function list_nodes_min packet-provider
|
|
"""
|
|
if call == "action":
|
|
raise SaltCloudSystemExit(
|
|
"The list_nodes_min function must be called with -f or --function."
|
|
)
|
|
|
|
ret = {}
|
|
|
|
for device in get_devices_by_token():
|
|
ret[device.hostname] = {"id": device.id, "state": device.state}
|
|
|
|
return ret
|
|
|
|
|
|
def list_nodes_select(call=None):
|
|
"""
|
|
Return a list of the VMs that are on the provider, with select fields.
|
|
"""
|
|
return salt.utils.cloud.list_nodes_select(
|
|
list_nodes_full(),
|
|
__opts__["query.selection"],
|
|
call,
|
|
)
|
|
|
|
|
|
def get_devices_by_token():
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
devices = []
|
|
|
|
for profile_name in vm_["profiles"]:
|
|
profile = vm_["profiles"][profile_name]
|
|
|
|
devices.extend(manager.list_devices(profile["project_id"]))
|
|
|
|
return devices
|
|
|
|
|
|
def list_nodes(call=None):
|
|
"""
|
|
Returns a list of devices, keeping only a brief listing.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud -Q
|
|
salt-cloud --query
|
|
salt-cloud -f list_nodes packet-provider
|
|
..
|
|
"""
|
|
|
|
if call == "action":
|
|
raise SaltCloudException(
|
|
"The list_nodes function must be called with -f or --function."
|
|
)
|
|
|
|
ret = {}
|
|
|
|
for device in get_devices_by_token():
|
|
ret[device.hostname] = device.__dict__
|
|
|
|
return ret
|
|
|
|
|
|
def destroy(name, call=None):
|
|
"""
|
|
Destroys a Packet device by name.
|
|
|
|
name
|
|
The hostname of VM to be be destroyed.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-cloud -d name
|
|
"""
|
|
if call == "function":
|
|
raise SaltCloudException(
|
|
"The destroy action must be called with -d, --destroy, -a or --action."
|
|
)
|
|
|
|
__utils__["cloud.fire_event"](
|
|
"event",
|
|
"destroying instance",
|
|
"salt/cloud/{}/destroying".format(name),
|
|
args={"name": name},
|
|
sock_dir=__opts__["sock_dir"],
|
|
transport=__opts__["transport"],
|
|
)
|
|
|
|
vm_ = get_configured_provider()
|
|
manager = packet.Manager(auth_token=vm_["token"])
|
|
|
|
nodes = list_nodes_min()
|
|
|
|
node = nodes[name]
|
|
|
|
for project in manager.list_projects():
|
|
|
|
for volume in manager.list_volumes(project.id):
|
|
if volume.attached_to == node["id"]:
|
|
volume.detach()
|
|
volume.delete()
|
|
break
|
|
|
|
manager.call_api("devices/{id}".format(id=node["id"]), type="DELETE")
|
|
|
|
__utils__["cloud.fire_event"](
|
|
"event",
|
|
"destroyed instance",
|
|
"salt/cloud/{}/destroyed".format(name),
|
|
args={"name": name},
|
|
sock_dir=__opts__["sock_dir"],
|
|
transport=__opts__["transport"],
|
|
)
|
|
|
|
return {}
|
|
|
|
|
|
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
|
|
)
|