1927 lines
64 KiB
Python
1927 lines
64 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
|
# (the "License"); you may not use this file except in compliance with
|
|
# the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
Provides base classes for working with drivers
|
|
"""
|
|
|
|
from __future__ import with_statement
|
|
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Tuple
|
|
from typing import Type
|
|
from typing import Optional
|
|
from typing import Any
|
|
from typing import Union
|
|
from typing import Callable
|
|
from typing import TYPE_CHECKING
|
|
|
|
import time
|
|
import hashlib
|
|
import os
|
|
import re
|
|
import socket
|
|
import random
|
|
import binascii
|
|
import datetime
|
|
import traceback
|
|
import atexit
|
|
|
|
from libcloud.utils.py3 import b
|
|
|
|
import libcloud.compute.ssh
|
|
from libcloud.pricing import get_size_price
|
|
from libcloud.compute.types import NodeState, StorageVolumeState,\
|
|
DeploymentError
|
|
if TYPE_CHECKING:
|
|
from libcloud.compute.deployment import Deployment
|
|
from libcloud.compute.types import Provider
|
|
from libcloud.compute.types import NodeImageMemberState
|
|
from libcloud.compute.ssh import SSHClient
|
|
from libcloud.compute.ssh import BaseSSHClient
|
|
from libcloud.common.base import Connection
|
|
from libcloud.common.base import ConnectionKey
|
|
from libcloud.common.base import BaseDriver
|
|
from libcloud.common.types import LibcloudError
|
|
from libcloud.compute.ssh import have_paramiko
|
|
from libcloud.compute.ssh import SSHCommandTimeoutError
|
|
|
|
from libcloud.utils.networking import is_private_subnet
|
|
from libcloud.utils.networking import is_valid_ip_address
|
|
|
|
if have_paramiko:
|
|
from paramiko.ssh_exception import SSHException
|
|
from paramiko.ssh_exception import AuthenticationException
|
|
|
|
SSH_TIMEOUT_EXCEPTION_CLASSES = (AuthenticationException, SSHException,
|
|
IOError, socket.gaierror, socket.error)
|
|
else:
|
|
SSH_TIMEOUT_EXCEPTION_CLASSES = (IOError, socket.gaierror, # type: ignore
|
|
socket.error) # type: ignore
|
|
|
|
T_Auth = Union['NodeAuthSSHKey', 'NodeAuthPassword']
|
|
|
|
T_Ssh_key = Union[List[str], str]
|
|
|
|
# How long to wait for the node to come online after creating it
|
|
NODE_ONLINE_WAIT_TIMEOUT = 10 * 60
|
|
|
|
# How long to try connecting to a remote SSH server when running a deployment
|
|
# script.
|
|
SSH_CONNECT_TIMEOUT = 5 * 60
|
|
|
|
# Error message which should be considered fatal for deploy_node() method and
|
|
# on which we should abort retrying and immediately propagate the error
|
|
SSH_FATAL_ERROR_MSGS = [
|
|
# Propagate (key) file doesn't exist errors
|
|
# NOTE: Paramiko only supports PEM private key format
|
|
# See https://github.com/paramiko/paramiko/issues/1313
|
|
# for details
|
|
'no such file or directory',
|
|
'invalid key',
|
|
'not a valid ',
|
|
'invalid or unsupported key type',
|
|
'private file is encrypted',
|
|
'private key file is encrypted',
|
|
'private key file checkints do not match',
|
|
'invalid password provided'
|
|
]
|
|
|
|
__all__ = [
|
|
'Node',
|
|
'NodeState',
|
|
'NodeSize',
|
|
'NodeImage',
|
|
'NodeImageMember',
|
|
'NodeLocation',
|
|
'NodeAuthSSHKey',
|
|
'NodeAuthPassword',
|
|
'NodeDriver',
|
|
|
|
'StorageVolume',
|
|
'StorageVolumeState',
|
|
'VolumeSnapshot',
|
|
|
|
# Deprecated, moved to libcloud.utils.networking
|
|
'is_private_subnet',
|
|
'is_valid_ip_address'
|
|
]
|
|
|
|
|
|
class UuidMixin(object):
|
|
"""
|
|
Mixin class for get_uuid function.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._uuid = None # type: str
|
|
|
|
def get_uuid(self):
|
|
"""
|
|
Unique hash for a node, node image, or node size
|
|
|
|
The hash is a function of an SHA1 hash of the node, node image,
|
|
or node size's ID and its driver which means that it should be
|
|
unique between all objects of its type.
|
|
In some subclasses (e.g. GoGridNode) there is no ID
|
|
available so the public IP address is used. This means that,
|
|
unlike a properly done system UUID, the same UUID may mean a
|
|
different system install at a different time
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> node = driver.create_node()
|
|
>>> node.get_uuid()
|
|
'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f'
|
|
|
|
Note, for example, that this example will always produce the
|
|
same UUID!
|
|
|
|
:rtype: ``str``
|
|
"""
|
|
if not self._uuid:
|
|
self._uuid = hashlib.sha1(b('%s:%s' %
|
|
(self.id, self.driver.type))).hexdigest()
|
|
|
|
return self._uuid
|
|
|
|
@property
|
|
def uuid(self):
|
|
# type: () -> str
|
|
return self.get_uuid()
|
|
|
|
|
|
class Node(UuidMixin):
|
|
"""
|
|
Provide a common interface for handling nodes of all types.
|
|
|
|
The Node object provides the interface in libcloud through which
|
|
we can manipulate nodes in different cloud providers in the same
|
|
way. Node objects don't actually do much directly themselves,
|
|
instead the node driver handles the connection to the node.
|
|
|
|
You don't normally create a node object yourself; instead you use
|
|
a driver and then have that create the node for you.
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> node = driver.create_node()
|
|
>>> node.public_ips[0]
|
|
'127.0.0.3'
|
|
>>> node.name
|
|
'dummy-3'
|
|
|
|
You can also get nodes from the driver's list_node function.
|
|
|
|
>>> node = driver.list_nodes()[0]
|
|
>>> node.name
|
|
'dummy-1'
|
|
|
|
The node keeps a reference to its own driver which means that we
|
|
can work on nodes from different providers without having to know
|
|
which is which.
|
|
|
|
>>> driver = DummyNodeDriver(72)
|
|
>>> node2 = driver.create_node()
|
|
>>> node.driver.creds
|
|
0
|
|
>>> node2.driver.creds
|
|
72
|
|
|
|
Although Node objects can be subclassed, this isn't normally
|
|
done. Instead, any driver specific information is stored in the
|
|
"extra" attribute of the node.
|
|
|
|
>>> node.extra
|
|
{'foo': 'bar'}
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
name, # type: str
|
|
state, # type: NodeState
|
|
public_ips, # type: List[str]
|
|
private_ips, # type: List[str]
|
|
driver,
|
|
size=None, # type: NodeSize
|
|
image=None, # type: NodeImage
|
|
extra=None, # type: dict
|
|
created_at=None # type: datetime.datetime
|
|
):
|
|
"""
|
|
:param id: Node ID.
|
|
:type id: ``str``
|
|
|
|
:param name: Node name.
|
|
:type name: ``str``
|
|
|
|
:param state: Node state.
|
|
:type state: :class:`libcloud.compute.types.NodeState`
|
|
|
|
|
|
:param public_ips: Public IP addresses associated with this node.
|
|
:type public_ips: ``list``
|
|
|
|
:param private_ips: Private IP addresses associated with this node.
|
|
:type private_ips: ``list``
|
|
|
|
:param driver: Driver this node belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param size: Size of this node. (optional)
|
|
:type size: :class:`.NodeSize`
|
|
|
|
:param image: Image of this node. (optional)
|
|
:type image: :class:`.NodeImage`
|
|
|
|
:param created_at: The datetime this node was created (optional)
|
|
:type created_at: :class: `datetime.datetime`
|
|
|
|
:param extra: Optional provider specific attributes associated with
|
|
this node.
|
|
:type extra: ``dict``
|
|
|
|
"""
|
|
self.id = str(id) if id else None
|
|
self.name = name
|
|
self.state = state
|
|
self.public_ips = public_ips if public_ips else []
|
|
self.private_ips = private_ips if private_ips else []
|
|
self.driver = driver
|
|
self.size = size
|
|
self.created_at = created_at
|
|
self.image = image
|
|
self.extra = extra or {}
|
|
UuidMixin.__init__(self)
|
|
|
|
def reboot(self):
|
|
# type: () -> bool
|
|
"""
|
|
Reboot this node
|
|
|
|
:return: ``bool``
|
|
|
|
This calls the node's driver and reboots the node
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> node = driver.create_node()
|
|
>>> node.state == NodeState.RUNNING
|
|
True
|
|
>>> node.state == NodeState.REBOOTING
|
|
False
|
|
>>> node.reboot()
|
|
True
|
|
>>> node.state == NodeState.REBOOTING
|
|
True
|
|
"""
|
|
return self.driver.reboot_node(self)
|
|
|
|
def start(self):
|
|
# type: () -> bool
|
|
"""
|
|
Start this node.
|
|
|
|
:return: ``bool``
|
|
"""
|
|
return self.driver.start_node(self)
|
|
|
|
def stop_node(self):
|
|
# type: () -> bool
|
|
"""
|
|
Stop (shutdown) this node.
|
|
|
|
:return: ``bool``
|
|
"""
|
|
return self.driver.stop_node(self)
|
|
|
|
def destroy(self):
|
|
# type: () -> bool
|
|
"""
|
|
Destroy this node
|
|
|
|
:return: ``bool``
|
|
|
|
This calls the node's driver and destroys the node
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> from libcloud.compute.types import NodeState
|
|
>>> node = driver.create_node()
|
|
>>> node.state == NodeState.RUNNING
|
|
True
|
|
>>> node.destroy()
|
|
True
|
|
>>> node.state == NodeState.RUNNING
|
|
False
|
|
|
|
"""
|
|
return self.driver.destroy_node(self)
|
|
|
|
def __repr__(self):
|
|
state = NodeState.tostring(self.state)
|
|
|
|
return (('<Node: uuid=%s, name=%s, state=%s, public_ips=%s, '
|
|
'private_ips=%s, provider=%s ...>')
|
|
% (self.uuid, self.name, state, self.public_ips,
|
|
self.private_ips, self.driver.name))
|
|
|
|
|
|
class NodeSize(UuidMixin):
|
|
"""
|
|
A Base NodeSize class to derive from.
|
|
|
|
NodeSizes are objects which are typically returned a driver's
|
|
list_sizes function. They contain a number of different
|
|
parameters which define how big an image is.
|
|
|
|
The exact parameters available depends on the provider.
|
|
|
|
N.B. Where a parameter is "unlimited" (for example bandwidth in
|
|
Amazon) this will be given as 0.
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> size = driver.list_sizes()[0]
|
|
>>> size.ram
|
|
128
|
|
>>> size.bandwidth
|
|
500
|
|
>>> size.price
|
|
4
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
name, # type: str
|
|
ram, # type: int
|
|
disk, # type: int
|
|
bandwidth, # type: Optional[int]
|
|
price, # type: float
|
|
driver, # type: NodeDriver
|
|
extra=None # type: Optional[dict]
|
|
):
|
|
"""
|
|
:param id: Size ID.
|
|
:type id: ``str``
|
|
|
|
:param name: Size name.
|
|
:type name: ``str``
|
|
|
|
:param ram: Amount of memory (in MB) provided by this size.
|
|
:type ram: ``int``
|
|
|
|
:param disk: Amount of disk storage (in GB) provided by this image.
|
|
:type disk: ``int``
|
|
|
|
:param bandwidth: Amount of bandiwdth included with this size.
|
|
:type bandwidth: ``int``
|
|
|
|
:param price: Price (in US dollars) of running this node for an hour.
|
|
:type price: ``float``
|
|
|
|
:param driver: Driver this size belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param extra: Optional provider specific attributes associated with
|
|
this size.
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = str(id)
|
|
self.name = name
|
|
self.ram = ram
|
|
self.disk = disk
|
|
self.bandwidth = bandwidth
|
|
self.price = price
|
|
self.driver = driver
|
|
self.extra = extra or {}
|
|
UuidMixin.__init__(self)
|
|
|
|
def __repr__(self):
|
|
return (('<NodeSize: id=%s, name=%s, ram=%s disk=%s bandwidth=%s '
|
|
'price=%s driver=%s ...>')
|
|
% (self.id, self.name, self.ram, self.disk, self.bandwidth,
|
|
self.price, self.driver.name))
|
|
|
|
|
|
class NodeImage(UuidMixin):
|
|
"""
|
|
An operating system image.
|
|
|
|
NodeImage objects are typically returned by the driver for the
|
|
cloud provider in response to the list_images function
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> image = driver.list_images()[0]
|
|
>>> image.name
|
|
'Ubuntu 9.10'
|
|
|
|
Apart from name and id, there is no further standard information;
|
|
other parameters are stored in a driver specific "extra" variable
|
|
|
|
When creating a node, a node image should be given as an argument
|
|
to the create_node function to decide which OS image to use.
|
|
|
|
>>> node = driver.create_node(image=image)
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
name, # type: str
|
|
driver, # type: NodeDriver
|
|
extra=None # type: Optional[dict]
|
|
):
|
|
"""
|
|
:param id: Image ID.
|
|
:type id: ``str``
|
|
|
|
:param name: Image name.
|
|
:type name: ``str``
|
|
|
|
:param driver: Driver this image belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param extra: Optional provided specific attributes associated with
|
|
this image.
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = str(id)
|
|
self.name = name
|
|
self.driver = driver
|
|
self.extra = extra or {}
|
|
UuidMixin.__init__(self)
|
|
|
|
def __repr__(self):
|
|
return (('<NodeImage: id=%s, name=%s, driver=%s ...>')
|
|
% (self.id, self.name, self.driver.name))
|
|
|
|
|
|
class NodeImageMember(UuidMixin):
|
|
"""
|
|
A member of an image. At some cloud providers there is a mechanism
|
|
to share images. Once an image is shared with another account that
|
|
user will be a 'member' of the image.
|
|
|
|
For example, see the image members schema in the OpenStack Image
|
|
Service API v2 documentation. https://developer.openstack.org/
|
|
api-ref/image/v2/index.html#image-members-schema
|
|
|
|
NodeImageMember objects are typically returned by the driver for the
|
|
cloud provider in response to the list_image_members method
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
image_id, # type: str
|
|
state, # type: NodeImageMemberState
|
|
driver, # type: NodeDriver
|
|
created=None, # type: datetime.datetime
|
|
extra=None # type: Optional[dict]
|
|
):
|
|
"""
|
|
:param id: Image member ID.
|
|
:type id: ``str``
|
|
|
|
:param id: The associated image ID.
|
|
:type id: ``str``
|
|
|
|
:param state: State of the NodeImageMember. If not
|
|
provided, will default to UNKNOWN.
|
|
:type state: :class:`.NodeImageMemberState`
|
|
|
|
:param driver: Driver this image belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param created: A datetime object that represents when the
|
|
image member was created
|
|
:type created: ``datetime.datetime``
|
|
|
|
:param extra: Optional provided specific attributes associated with
|
|
this image.
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = str(id)
|
|
self.image_id = str(image_id)
|
|
self.state = state
|
|
self.driver = driver
|
|
self.created = created
|
|
self.extra = extra or {}
|
|
UuidMixin.__init__(self)
|
|
|
|
def __repr__(self):
|
|
return (('<NodeImageMember: id=%s, image_id=%s, '
|
|
'state=%s, driver=%s ...>')
|
|
% (self.id, self.image_id, self.state, self.driver.name))
|
|
|
|
|
|
class NodeLocation(object):
|
|
"""
|
|
A physical location where nodes can be.
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> location = driver.list_locations()[0]
|
|
>>> location.country
|
|
'US'
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
name, # type: str
|
|
country, # type: str
|
|
driver, # type: NodeDriver
|
|
extra=None # type: Optional[dict]
|
|
):
|
|
"""
|
|
:param id: Location ID.
|
|
:type id: ``str``
|
|
|
|
:param name: Location name.
|
|
:type name: ``str``
|
|
|
|
:param country: Location country.
|
|
:type country: ``str``
|
|
|
|
:param driver: Driver this location belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param extra: Optional provided specific attributes associated with
|
|
this location.
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = str(id)
|
|
self.name = name
|
|
self.country = country
|
|
self.driver = driver
|
|
self.extra = extra or {}
|
|
|
|
def __repr__(self):
|
|
return (('<NodeLocation: id=%s, name=%s, country=%s, driver=%s>')
|
|
% (self.id, self.name, self.country, self.driver.name))
|
|
|
|
|
|
class NodeAuthSSHKey(object):
|
|
"""
|
|
An SSH key to be installed for authentication to a node.
|
|
|
|
This is the actual contents of the users ssh public key which will
|
|
normally be installed as root's public key on the node.
|
|
|
|
>>> pubkey = '...' # read from file
|
|
>>> from libcloud.compute.base import NodeAuthSSHKey
|
|
>>> k = NodeAuthSSHKey(pubkey)
|
|
>>> k
|
|
<NodeAuthSSHKey>
|
|
"""
|
|
|
|
def __init__(self, pubkey):
|
|
# type: (str) -> None
|
|
"""
|
|
:param pubkey: Public key material.
|
|
:type pubkey: ``str``
|
|
"""
|
|
self.pubkey = pubkey
|
|
|
|
def __repr__(self):
|
|
return '<NodeAuthSSHKey>'
|
|
|
|
|
|
class NodeAuthPassword(object):
|
|
"""
|
|
A password to be used for authentication to a node.
|
|
"""
|
|
def __init__(self, password, generated=False):
|
|
# type: (str, bool) -> None
|
|
"""
|
|
:param password: Password.
|
|
:type password: ``str``
|
|
|
|
:type generated: ``True`` if this password was automatically generated,
|
|
``False`` otherwise.
|
|
"""
|
|
self.password = password
|
|
self.generated = generated
|
|
|
|
def __repr__(self):
|
|
return '<NodeAuthPassword>'
|
|
|
|
|
|
class StorageVolume(UuidMixin):
|
|
"""
|
|
A base StorageVolume class to derive from.
|
|
"""
|
|
|
|
def __init__(self,
|
|
id, # type: str
|
|
name, # type: str
|
|
size, # type: int
|
|
driver, # type: NodeDriver
|
|
state=None, # type: Optional[StorageVolumeState]
|
|
extra=None # type: Optional[Dict]
|
|
):
|
|
# type: (...) -> None
|
|
"""
|
|
:param id: Storage volume ID.
|
|
:type id: ``str``
|
|
|
|
:param name: Storage volume name.
|
|
:type name: ``str``
|
|
|
|
:param size: Size of this volume (in GB).
|
|
:type size: ``int``
|
|
|
|
:param driver: Driver this image belongs to.
|
|
:type driver: :class:`.NodeDriver`
|
|
|
|
:param state: Optional state of the StorageVolume. If not
|
|
provided, will default to UNKNOWN.
|
|
:type state: :class:`.StorageVolumeState`
|
|
|
|
:param extra: Optional provider specific attributes.
|
|
:type extra: ``dict``
|
|
"""
|
|
self.id = id
|
|
self.name = name
|
|
self.size = size
|
|
self.driver = driver
|
|
self.extra = extra
|
|
self.state = state
|
|
UuidMixin.__init__(self)
|
|
|
|
def list_snapshots(self):
|
|
# type: () -> List[VolumeSnapshot]
|
|
"""
|
|
:rtype: ``list`` of ``VolumeSnapshot``
|
|
"""
|
|
return self.driver.list_volume_snapshots(volume=self)
|
|
|
|
def attach(self, node, device=None):
|
|
# type: (Node, Optional[str]) -> bool
|
|
"""
|
|
Attach this volume to a node.
|
|
|
|
:param node: Node to attach volume to
|
|
:type node: :class:`.Node`
|
|
|
|
:param device: Where the device is exposed,
|
|
e.g. '/dev/sdb (optional)
|
|
:type device: ``str``
|
|
|
|
:return: ``True`` if attach was successful, ``False`` otherwise.
|
|
:rtype: ``bool``
|
|
"""
|
|
return self.driver.attach_volume(node=node, volume=self, device=device)
|
|
|
|
def detach(self):
|
|
# type: () -> bool
|
|
"""
|
|
Detach this volume from its node
|
|
|
|
:return: ``True`` if detach was successful, ``False`` otherwise.
|
|
:rtype: ``bool``
|
|
"""
|
|
return self.driver.detach_volume(volume=self)
|
|
|
|
def snapshot(self, name):
|
|
# type: (str) -> VolumeSnapshot
|
|
"""
|
|
Creates a snapshot of this volume.
|
|
|
|
:return: Created snapshot.
|
|
:rtype: ``VolumeSnapshot``
|
|
"""
|
|
return self.driver.create_volume_snapshot(volume=self, name=name)
|
|
|
|
def destroy(self):
|
|
# type: () -> bool
|
|
"""
|
|
Destroy this storage volume.
|
|
|
|
:return: ``True`` if destroy was successful, ``False`` otherwise.
|
|
:rtype: ``bool``
|
|
"""
|
|
|
|
return self.driver.destroy_volume(volume=self)
|
|
|
|
def __repr__(self):
|
|
return '<StorageVolume id=%s size=%s driver=%s>' % (
|
|
self.id, self.size, self.driver.name)
|
|
|
|
|
|
class VolumeSnapshot(object):
|
|
"""
|
|
A base VolumeSnapshot class to derive from.
|
|
"""
|
|
def __init__(self,
|
|
id, # type: str
|
|
driver, # type: NodeDriver
|
|
size=None, # type: int
|
|
extra=None, # type: Optional[Dict]
|
|
created=None, # type: Optional[datetime.datetime]
|
|
state=None, # type: StorageVolumeState
|
|
name=None # type: Optional[str]
|
|
):
|
|
# type: (...) -> None
|
|
"""
|
|
VolumeSnapshot constructor.
|
|
|
|
:param id: Snapshot ID.
|
|
:type id: ``str``
|
|
|
|
:param driver: The driver that represents a connection to the
|
|
provider
|
|
:type driver: `NodeDriver`
|
|
|
|
:param size: A snapshot size in GB.
|
|
:type size: ``int``
|
|
|
|
:param extra: Provider depends parameters for snapshot.
|
|
:type extra: ``dict``
|
|
|
|
:param created: A datetime object that represents when the
|
|
snapshot was created
|
|
:type created: ``datetime.datetime``
|
|
|
|
:param state: A string representing the state the snapshot is
|
|
in. See `libcloud.compute.types.StorageVolumeState`.
|
|
:type state: ``StorageVolumeState``
|
|
|
|
:param name: A string representing the name of the snapshot
|
|
:type name: ``str``
|
|
"""
|
|
self.id = id
|
|
self.driver = driver
|
|
self.size = size
|
|
self.extra = extra or {}
|
|
self.created = created
|
|
self.state = state
|
|
self.name = name
|
|
|
|
def destroy(self):
|
|
# type: () -> bool
|
|
"""
|
|
Destroys this snapshot.
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
return self.driver.destroy_volume_snapshot(snapshot=self)
|
|
|
|
def __repr__(self):
|
|
return ('<VolumeSnapshot "%s" id=%s size=%s driver=%s state=%s>' %
|
|
(self.name, self.id, self.size, self.driver.name, self.state))
|
|
|
|
|
|
class KeyPair(object):
|
|
"""
|
|
Represents a SSH key pair.
|
|
"""
|
|
|
|
def __init__(self,
|
|
name, # type: str
|
|
public_key, # type: str
|
|
fingerprint, # type: str
|
|
driver, # type: NodeDriver
|
|
private_key=None, # type: Optional[str]
|
|
extra=None # type: Optional[Dict]
|
|
):
|
|
# type: (...) -> None
|
|
"""
|
|
Constructor.
|
|
|
|
:keyword name: Name of the key pair object.
|
|
:type name: ``str``
|
|
|
|
:keyword fingerprint: Key fingerprint.
|
|
:type fingerprint: ``str``
|
|
|
|
:keyword public_key: Public key in OpenSSH format.
|
|
:type public_key: ``str``
|
|
|
|
:keyword private_key: Private key in PEM format.
|
|
:type private_key: ``str``
|
|
|
|
:keyword extra: Provider specific attributes associated with this
|
|
key pair. (optional)
|
|
:type extra: ``dict``
|
|
"""
|
|
self.name = name
|
|
self.fingerprint = fingerprint
|
|
self.public_key = public_key
|
|
self.private_key = private_key
|
|
self.driver = driver
|
|
self.extra = extra or {}
|
|
|
|
def __repr__(self):
|
|
return ('<KeyPair name=%s fingerprint=%s driver=%s>' %
|
|
(self.name, self.fingerprint, self.driver.name))
|
|
|
|
|
|
class NodeDriver(BaseDriver):
|
|
"""
|
|
A base NodeDriver class to derive from
|
|
|
|
This class is always subclassed by a specific driver. For
|
|
examples of base behavior of most functions (except deploy node)
|
|
see the dummy driver.
|
|
"""
|
|
|
|
connectionCls = ConnectionKey # type: Type[Connection]
|
|
name = None # type: str
|
|
api_name = None # type: str
|
|
website = None # type: str
|
|
type = None # type: Union[Provider,str]
|
|
port = None # type: int
|
|
features = {'create_node': []} # type: Dict[str, List[str]]
|
|
|
|
"""
|
|
List of available features for a driver.
|
|
- :meth:`libcloud.compute.base.NodeDriver.create_node`
|
|
- ssh_key: Supports :class:`.NodeAuthSSHKey` as an authentication
|
|
method for nodes.
|
|
- password: Supports :class:`.NodeAuthPassword` as an
|
|
authentication
|
|
method for nodes.
|
|
- generates_password: Returns a password attribute on the Node
|
|
object returned from creation.
|
|
"""
|
|
|
|
NODE_STATE_MAP = {} # type: Dict[str, NodeState]
|
|
|
|
def list_nodes(self, *args, **kwargs):
|
|
# type: (Any, Any) -> List[Node]
|
|
"""
|
|
List all nodes.
|
|
|
|
:return: list of node objects
|
|
:rtype: ``list`` of :class:`.Node`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_nodes not implemented for this driver')
|
|
|
|
def list_sizes(self, location=None):
|
|
# type: (Optional[NodeLocation]) -> List[NodeSize]
|
|
"""
|
|
List sizes on a provider
|
|
|
|
:param location: The location at which to list sizes
|
|
:type location: :class:`.NodeLocation`
|
|
|
|
:return: list of node size objects
|
|
:rtype: ``list`` of :class:`.NodeSize`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_sizes not implemented for this driver')
|
|
|
|
def list_locations(self):
|
|
# type: () -> List[NodeLocation]
|
|
"""
|
|
List data centers for a provider
|
|
|
|
:return: list of node location objects
|
|
:rtype: ``list`` of :class:`.NodeLocation`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_locations not implemented for this driver')
|
|
|
|
def create_node(self,
|
|
name, # type: str
|
|
size, # type: NodeSize
|
|
image, # type: NodeImage
|
|
location=None, # type: Optional[NodeLocation]
|
|
auth=None # type: T_Auth
|
|
):
|
|
# type: (...) -> Node
|
|
"""
|
|
Create a new node instance. This instance will be started
|
|
automatically.
|
|
|
|
Not all hosting API's are created equal and to allow libcloud to
|
|
support as many as possible there are some standard supported
|
|
variations of ``create_node``. These are declared using a
|
|
``features`` API.
|
|
You can inspect ``driver.features['create_node']`` to see what
|
|
variation of the API you are dealing with:
|
|
|
|
``ssh_key``
|
|
You can inject a public key into a new node allows key based SSH
|
|
authentication.
|
|
``password``
|
|
You can inject a password into a new node for SSH authentication.
|
|
If no password is provided libcloud will generated a password.
|
|
The password will be available as
|
|
``return_value.extra['password']``.
|
|
``generates_password``
|
|
The hosting provider will generate a password. It will be returned
|
|
to you via ``return_value.extra['password']``.
|
|
|
|
Some drivers allow you to set how you will authenticate with the
|
|
instance that is created. You can inject this initial authentication
|
|
information via the ``auth`` parameter.
|
|
|
|
If a driver supports the ``ssh_key`` feature flag for ``created_node``
|
|
you can upload a public key into the new instance::
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> auth = NodeAuthSSHKey('pubkey data here')
|
|
>>> node = driver.create_node("test_node", auth=auth)
|
|
|
|
If a driver supports the ``password`` feature flag for ``create_node``
|
|
you can set a password::
|
|
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> auth = NodeAuthPassword('mysecretpassword')
|
|
>>> node = driver.create_node("test_node", auth=auth)
|
|
|
|
If a driver supports the ``password`` feature and you don't provide the
|
|
``auth`` argument libcloud will assign a password::
|
|
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> node = driver.create_node("test_node")
|
|
>>> password = node.extra['password']
|
|
|
|
A password will also be returned in this way for drivers that declare
|
|
the ``generates_password`` feature, though in that case the password is
|
|
actually provided to the driver API by the hosting provider rather than
|
|
generated by libcloud.
|
|
|
|
You can only pass a :class:`.NodeAuthPassword` or
|
|
:class:`.NodeAuthSSHKey` to ``create_node`` via the auth parameter if
|
|
has the corresponding feature flag.
|
|
|
|
:param name: String with a name for this new node (required)
|
|
:type name: ``str``
|
|
|
|
:param size: The size of resources allocated to this node.
|
|
(required)
|
|
:type size: :class:`.NodeSize`
|
|
|
|
:param image: OS Image to boot on node. (required)
|
|
:type image: :class:`.NodeImage`
|
|
|
|
:param location: Which data center to create a node in. If empty,
|
|
undefined behavior will be selected. (optional)
|
|
:type location: :class:`.NodeLocation`
|
|
|
|
:param auth: Initial authentication information for the node
|
|
(optional)
|
|
:type auth: :class:`.NodeAuthSSHKey` or :class:`NodeAuthPassword`
|
|
|
|
:return: The newly created node.
|
|
:rtype: :class:`.Node`
|
|
"""
|
|
raise NotImplementedError(
|
|
'create_node not implemented for this driver')
|
|
|
|
def deploy_node(self,
|
|
deploy, # type: Deployment
|
|
ssh_username='root', # type: str
|
|
ssh_alternate_usernames=None, # type: Optional[List[str]]
|
|
ssh_port=22, # type: int
|
|
ssh_timeout=10, # type: int
|
|
ssh_key=None, # type: Optional[T_Ssh_key]
|
|
ssh_key_password=None, # type: Optional[str]
|
|
auth=None, # type: T_Auth
|
|
timeout=SSH_CONNECT_TIMEOUT, # type: int
|
|
max_tries=3, # type: int
|
|
ssh_interface='public_ips', # type: str
|
|
at_exit_func=None, # type: Callable
|
|
wait_period=5, # type: int
|
|
**create_node_kwargs):
|
|
# type: (...) -> Node
|
|
"""
|
|
Create a new node, and start deployment.
|
|
|
|
In order to be able to SSH into a created node access credentials are
|
|
required.
|
|
|
|
A user can pass either a :class:`.NodeAuthPassword` or
|
|
:class:`.NodeAuthSSHKey` to the ``auth`` argument. If the
|
|
``create_node`` implementation supports that kind if credential (as
|
|
declared in ``self.features['create_node']``) then it is passed on to
|
|
``create_node``. Otherwise it is not passed on to ``create_node`` and
|
|
it is only used for authentication.
|
|
|
|
If the ``auth`` parameter is not supplied but the driver declares it
|
|
supports ``generates_password`` then the password returned by
|
|
``create_node`` will be used to SSH into the server.
|
|
|
|
Finally, if the ``ssh_key_file`` is supplied that key will be used to
|
|
SSH into the server.
|
|
|
|
This function may raise a :class:`DeploymentException`, if a
|
|
create_node call was successful, but there is a later error (like SSH
|
|
failing or timing out). This exception includes a Node object which
|
|
you may want to destroy if incomplete deployments are not desirable.
|
|
|
|
>>> from libcloud.compute.drivers.dummy import DummyNodeDriver
|
|
>>> from libcloud.compute.deployment import ScriptDeployment
|
|
>>> from libcloud.compute.deployment import MultiStepDeployment
|
|
>>> from libcloud.compute.base import NodeAuthSSHKey
|
|
>>> driver = DummyNodeDriver(0)
|
|
>>> key = NodeAuthSSHKey('...') # read from file
|
|
>>> script = ScriptDeployment("yum -y install emacs strace tcpdump")
|
|
>>> msd = MultiStepDeployment([key, script])
|
|
>>> def d():
|
|
... try:
|
|
... driver.deploy_node(deploy=msd)
|
|
... except NotImplementedError:
|
|
... print ("not implemented for dummy driver")
|
|
>>> d()
|
|
not implemented for dummy driver
|
|
|
|
Deploy node is typically not overridden in subclasses. The
|
|
existing implementation should be able to handle most such.
|
|
|
|
:param deploy: Deployment to run once machine is online and
|
|
available to SSH.
|
|
:type deploy: :class:`Deployment`
|
|
|
|
:param ssh_username: Optional name of the account which is used
|
|
when connecting to
|
|
SSH server (default is root)
|
|
:type ssh_username: ``str``
|
|
|
|
:param ssh_alternate_usernames: Optional list of ssh usernames to
|
|
try to connect with if using the
|
|
default one fails
|
|
:type ssh_alternate_usernames: ``list``
|
|
|
|
:param ssh_port: Optional SSH server port (default is 22)
|
|
:type ssh_port: ``int``
|
|
|
|
:param ssh_timeout: Optional SSH connection timeout in seconds
|
|
(default is 10)
|
|
:type ssh_timeout: ``float``
|
|
|
|
:param auth: Initial authentication information for the node
|
|
(optional)
|
|
:type auth: :class:`.NodeAuthSSHKey` or :class:`NodeAuthPassword`
|
|
|
|
:param ssh_key: A path (or paths) to an SSH private key with which
|
|
to attempt to authenticate. (optional)
|
|
:type ssh_key: ``str`` or ``list`` of ``str``
|
|
|
|
:param ssh_key_password: Optional password used for encrypted keys.
|
|
:type ssh_key_password: ``str``
|
|
|
|
:param timeout: How many seconds to wait before timing out.
|
|
(default is 600)
|
|
:type timeout: ``int``
|
|
|
|
:param max_tries: How many times to retry if a deployment fails
|
|
before giving up (default is 3)
|
|
:type max_tries: ``int``
|
|
|
|
:param ssh_interface: The interface to wait for. Default is
|
|
'public_ips', other option is 'private_ips'.
|
|
:type ssh_interface: ``str``
|
|
|
|
:param at_exit_func: Optional atexit handler function which will be
|
|
registered and called with created node if user
|
|
cancels the deploy process (e.g. CTRL+C), after
|
|
the node has been created, but before the deploy
|
|
process has finished.
|
|
|
|
This method gets passed in two keyword arguments:
|
|
|
|
- driver -> node driver in question
|
|
- node -> created Node object
|
|
|
|
Keep in mind that this function will only be
|
|
called in such scenario. In case the method
|
|
finishes (this includes throwing an exception),
|
|
at exit handler function won't be called.
|
|
:type at_exit_func: ``func``
|
|
|
|
:param wait_period: How many seconds to wait between each iteration
|
|
while waiting for node to transition into
|
|
running state and have IP assigned. (default is 5)
|
|
:type wait_period: ``int``
|
|
|
|
"""
|
|
if not libcloud.compute.ssh.have_paramiko:
|
|
raise RuntimeError('paramiko is not installed. You can install ' +
|
|
'it using pip: pip install paramiko')
|
|
|
|
if auth:
|
|
if not isinstance(auth, (NodeAuthSSHKey, NodeAuthPassword)):
|
|
raise NotImplementedError(
|
|
'If providing auth, only NodeAuthSSHKey or'
|
|
'NodeAuthPassword is supported')
|
|
elif ssh_key:
|
|
# If an ssh_key is provided we can try deploy_node
|
|
pass
|
|
elif 'create_node' in self.features:
|
|
f = self.features['create_node']
|
|
if 'generates_password' not in f and "password" not in f:
|
|
raise NotImplementedError(
|
|
'deploy_node not implemented for this driver')
|
|
else:
|
|
raise NotImplementedError(
|
|
'deploy_node not implemented for this driver')
|
|
|
|
# NOTE 1: This is a workaround for legacy code. Sadly a lot of legacy
|
|
# code uses **kwargs in "create_node()" method and simply ignores
|
|
# "deploy_node()" arguments which are passed to it.
|
|
# That's obviously far from idea that's why we first try to pass only
|
|
# non-deploy node arguments to the "create_node()" methods and if it
|
|
# that doesn't work, fall back to the old approach and simply pass in
|
|
# all the arguments
|
|
# NOTE 2: Some drivers which use password based SSH authentication
|
|
# rely on password being stored on the "auth" argument and that's why
|
|
# we also propagate that argument to "create_node()" method.
|
|
try:
|
|
# NOTE: We only pass auth to the method if auth argument is
|
|
# provided
|
|
if auth:
|
|
node = self.create_node(auth=auth, **create_node_kwargs)
|
|
else:
|
|
node = self.create_node(**create_node_kwargs)
|
|
except TypeError as e:
|
|
msg_1_re = (r'create_node\(\) missing \d+ required '
|
|
'positional arguments.*')
|
|
msg_2_re = r'create_node\(\) takes at least \d+ arguments.*'
|
|
if re.match(msg_1_re, str(e)) or re.match(msg_2_re, str(e)):
|
|
# pylint: disable=unexpected-keyword-arg
|
|
node = self.create_node( # type: ignore
|
|
deploy=deploy,
|
|
ssh_username=ssh_username,
|
|
ssh_alternate_usernames=ssh_alternate_usernames,
|
|
ssh_port=ssh_port,
|
|
ssh_timeout=ssh_timeout,
|
|
ssh_key=ssh_key,
|
|
auth=auth,
|
|
timeout=timeout,
|
|
max_tries=max_tries,
|
|
ssh_interface=ssh_interface,
|
|
**create_node_kwargs)
|
|
# pylint: enable=unexpected-keyword-arg
|
|
else:
|
|
raise e
|
|
|
|
if at_exit_func:
|
|
atexit.register(at_exit_func, driver=self, node=node)
|
|
|
|
password = None
|
|
if auth:
|
|
if isinstance(auth, NodeAuthPassword):
|
|
password = auth.password
|
|
elif 'password' in node.extra:
|
|
password = node.extra['password']
|
|
|
|
wait_timeout = timeout or NODE_ONLINE_WAIT_TIMEOUT
|
|
|
|
# Wait until node is up and running and has IP assigned
|
|
try:
|
|
node, ip_addresses = self.wait_until_running(
|
|
nodes=[node],
|
|
wait_period=wait_period,
|
|
timeout=wait_timeout,
|
|
ssh_interface=ssh_interface)[0]
|
|
except Exception as e:
|
|
if at_exit_func:
|
|
atexit.unregister(at_exit_func)
|
|
|
|
raise DeploymentError(node=node, original_exception=e, driver=self)
|
|
|
|
ssh_alternate_usernames = ssh_alternate_usernames or []
|
|
deploy_timeout = timeout or SSH_CONNECT_TIMEOUT
|
|
|
|
deploy_error = None
|
|
|
|
for username in ([ssh_username] + ssh_alternate_usernames):
|
|
try:
|
|
self._connect_and_run_deployment_script(
|
|
task=deploy, node=node,
|
|
ssh_hostname=ip_addresses[0], ssh_port=ssh_port,
|
|
ssh_username=username, ssh_password=password,
|
|
ssh_key_file=ssh_key, ssh_key_password=ssh_key_password,
|
|
ssh_timeout=ssh_timeout,
|
|
timeout=deploy_timeout, max_tries=max_tries)
|
|
except Exception as e:
|
|
# Try alternate username
|
|
# Todo: Need to fix paramiko so we can catch a more specific
|
|
# exception
|
|
deploy_error = e
|
|
else:
|
|
# Script successfully executed, don't try alternate username
|
|
deploy_error = None
|
|
break
|
|
|
|
if deploy_error is not None:
|
|
if at_exit_func:
|
|
atexit.unregister(at_exit_func)
|
|
|
|
raise DeploymentError(node=node, original_exception=deploy_error,
|
|
driver=self)
|
|
|
|
if at_exit_func:
|
|
atexit.unregister(at_exit_func)
|
|
|
|
return node
|
|
|
|
def reboot_node(self, node):
|
|
# type: (Node) -> bool
|
|
"""
|
|
Reboot a node.
|
|
|
|
:param node: The node to be rebooted
|
|
:type node: :class:`.Node`
|
|
|
|
:return: True if the reboot was successful, otherwise False
|
|
:rtype: ``bool``
|
|
"""
|
|
raise NotImplementedError(
|
|
'reboot_node not implemented for this driver')
|
|
|
|
def start_node(self, node):
|
|
# type: (Node) -> bool
|
|
"""
|
|
Start a node.
|
|
|
|
:param node: The node to be started
|
|
:type node: :class:`.Node`
|
|
|
|
:return: True if the start was successful, otherwise False
|
|
:rtype: ``bool``
|
|
"""
|
|
raise NotImplementedError(
|
|
'start_node not implemented for this driver')
|
|
|
|
def stop_node(self, node):
|
|
# type: (Node) -> bool
|
|
"""
|
|
Stop a node
|
|
|
|
:param node: The node to be stopped.
|
|
:type node: :class:`.Node`
|
|
|
|
:return: True if the stop was successful, otherwise False
|
|
:rtype: ``bool``
|
|
"""
|
|
raise NotImplementedError(
|
|
'stop_node not implemented for this driver')
|
|
|
|
def destroy_node(self, node):
|
|
# type: (Node) -> bool
|
|
"""
|
|
Destroy a node.
|
|
|
|
Depending upon the provider, this may destroy all data associated with
|
|
the node, including backups.
|
|
|
|
:param node: The node to be destroyed
|
|
:type node: :class:`.Node`
|
|
|
|
:return: True if the destroy was successful, False otherwise.
|
|
:rtype: ``bool``
|
|
"""
|
|
raise NotImplementedError(
|
|
'destroy_node not implemented for this driver')
|
|
|
|
##
|
|
# Volume and snapshot management methods
|
|
##
|
|
|
|
def list_volumes(self):
|
|
# type: () -> List[StorageVolume]
|
|
"""
|
|
List storage volumes.
|
|
|
|
:rtype: ``list`` of :class:`.StorageVolume`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_volumes not implemented for this driver')
|
|
|
|
def list_volume_snapshots(self, volume):
|
|
# type: (StorageVolume) -> List[VolumeSnapshot]
|
|
"""
|
|
List snapshots for a storage volume.
|
|
|
|
:rtype: ``list`` of :class:`VolumeSnapshot`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_volume_snapshots not implemented for this driver')
|
|
|
|
def create_volume(self,
|
|
size, # type: int
|
|
name, # type: str
|
|
location=None, # Optional[NodeLocation]
|
|
snapshot=None # Optional[VolumeSnapshot]
|
|
):
|
|
# type: (...) -> StorageVolume
|
|
"""
|
|
Create a new volume.
|
|
|
|
:param size: Size of volume in gigabytes (required)
|
|
:type size: ``int``
|
|
|
|
:param name: Name of the volume to be created
|
|
:type name: ``str``
|
|
|
|
:param location: Which data center to create a volume in. If
|
|
empty, undefined behavior will be selected.
|
|
(optional)
|
|
:type location: :class:`.NodeLocation`
|
|
|
|
:param snapshot: Snapshot from which to create the new
|
|
volume. (optional)
|
|
:type snapshot: :class:`.VolumeSnapshot`
|
|
|
|
:return: The newly created volume.
|
|
:rtype: :class:`StorageVolume`
|
|
"""
|
|
raise NotImplementedError(
|
|
'create_volume not implemented for this driver')
|
|
|
|
def create_volume_snapshot(self, volume, name=None):
|
|
# type: (StorageVolume, Optional[str]) -> VolumeSnapshot
|
|
"""
|
|
Creates a snapshot of the storage volume.
|
|
|
|
:param volume: The StorageVolume to create a VolumeSnapshot from
|
|
:type volume: :class:`.StorageVolume`
|
|
|
|
:param name: Name of created snapshot (optional)
|
|
:type name: `str`
|
|
|
|
:rtype: :class:`VolumeSnapshot`
|
|
"""
|
|
raise NotImplementedError(
|
|
'create_volume_snapshot not implemented for this driver')
|
|
|
|
def attach_volume(self, node, volume, device=None):
|
|
# type: (Node, StorageVolume, Optional[str]) -> bool
|
|
"""
|
|
Attaches volume to node.
|
|
|
|
:param node: Node to attach volume to.
|
|
:type node: :class:`.Node`
|
|
|
|
:param volume: Volume to attach.
|
|
:type volume: :class:`.StorageVolume`
|
|
|
|
:param device: Where the device is exposed, e.g. '/dev/sdb'
|
|
:type device: ``str``
|
|
|
|
:rytpe: ``bool``
|
|
"""
|
|
raise NotImplementedError('attach not implemented for this driver')
|
|
|
|
def detach_volume(self, volume):
|
|
# type: (StorageVolume) -> bool
|
|
"""
|
|
Detaches a volume from a node.
|
|
|
|
:param volume: Volume to be detached
|
|
:type volume: :class:`.StorageVolume`
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
|
|
raise NotImplementedError('detach not implemented for this driver')
|
|
|
|
def destroy_volume(self, volume):
|
|
# type: (StorageVolume) -> bool
|
|
"""
|
|
Destroys a storage volume.
|
|
|
|
:param volume: Volume to be destroyed
|
|
:type volume: :class:`StorageVolume`
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
|
|
raise NotImplementedError(
|
|
'destroy_volume not implemented for this driver')
|
|
|
|
def destroy_volume_snapshot(self, snapshot):
|
|
# type: (VolumeSnapshot) -> bool
|
|
"""
|
|
Destroys a snapshot.
|
|
|
|
:param snapshot: The snapshot to delete
|
|
:type snapshot: :class:`VolumeSnapshot`
|
|
|
|
:rtype: :class:`bool`
|
|
"""
|
|
raise NotImplementedError(
|
|
'destroy_volume_snapshot not implemented for this driver')
|
|
|
|
##
|
|
# Image management methods
|
|
##
|
|
|
|
def list_images(self, location=None):
|
|
# type: (Optional[NodeLocation]) -> List[NodeImage]
|
|
"""
|
|
List images on a provider.
|
|
|
|
:param location: The location at which to list images.
|
|
:type location: :class:`.NodeLocation`
|
|
|
|
:return: list of node image objects.
|
|
:rtype: ``list`` of :class:`.NodeImage`
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_images not implemented for this driver')
|
|
|
|
def create_image(self, node, name, description=None):
|
|
# type: (Node, str, Optional[str]) -> List[NodeImage]
|
|
"""
|
|
Creates an image from a node object.
|
|
|
|
:param node: Node to run the task on.
|
|
:type node: :class:`.Node`
|
|
|
|
:param name: name for new image.
|
|
:type name: ``str``
|
|
|
|
:param description: description for new image.
|
|
:type name: ``description``
|
|
|
|
:rtype: :class:`.NodeImage`:
|
|
:return: NodeImage instance on success.
|
|
|
|
"""
|
|
raise NotImplementedError(
|
|
'create_image not implemented for this driver')
|
|
|
|
def delete_image(self, node_image):
|
|
# type: (NodeImage) -> bool
|
|
"""
|
|
Deletes a node image from a provider.
|
|
|
|
:param node_image: Node image object.
|
|
:type node_image: :class:`.NodeImage`
|
|
|
|
:return: ``True`` if delete_image was successful, ``False`` otherwise.
|
|
:rtype: ``bool``
|
|
"""
|
|
|
|
raise NotImplementedError(
|
|
'delete_image not implemented for this driver')
|
|
|
|
def get_image(self, image_id):
|
|
# type: (str) -> NodeImage
|
|
"""
|
|
Returns a single node image from a provider.
|
|
|
|
:param image_id: Node to run the task on.
|
|
:type image_id: ``str``
|
|
|
|
:rtype :class:`.NodeImage`:
|
|
:return: NodeImage instance on success.
|
|
"""
|
|
raise NotImplementedError(
|
|
'get_image not implemented for this driver')
|
|
|
|
def copy_image(self, source_region, node_image, name, description=None):
|
|
# type: (str, NodeImage, str, Optional[str]) -> NodeImage
|
|
"""
|
|
Copies an image from a source region to the current region.
|
|
|
|
:param source_region: Region to copy the node from.
|
|
:type source_region: ``str``
|
|
|
|
:param node_image: NodeImage to copy.
|
|
:type node_image: :class:`.NodeImage`:
|
|
|
|
:param name: name for new image.
|
|
:type name: ``str``
|
|
|
|
:param description: description for new image.
|
|
:type name: ``str``
|
|
|
|
:rtype: :class:`.NodeImage`:
|
|
:return: NodeImage instance on success.
|
|
"""
|
|
raise NotImplementedError(
|
|
'copy_image not implemented for this driver')
|
|
|
|
##
|
|
# SSH key pair management methods
|
|
##
|
|
|
|
def list_key_pairs(self):
|
|
# type: () -> List[KeyPair]
|
|
"""
|
|
List all the available key pair objects.
|
|
|
|
:rtype: ``list`` of :class:`.KeyPair` objects
|
|
"""
|
|
raise NotImplementedError(
|
|
'list_key_pairs not implemented for this driver')
|
|
|
|
def get_key_pair(self, name):
|
|
# type: (str) -> KeyPair
|
|
"""
|
|
Retrieve a single key pair.
|
|
|
|
:param name: Name of the key pair to retrieve.
|
|
:type name: ``str``
|
|
|
|
:rtype: :class:`.KeyPair`
|
|
"""
|
|
raise NotImplementedError(
|
|
'get_key_pair not implemented for this driver')
|
|
|
|
def create_key_pair(self, name):
|
|
# type: (str) -> KeyPair
|
|
"""
|
|
Create a new key pair object.
|
|
|
|
:param name: Key pair name.
|
|
:type name: ``str``
|
|
|
|
:rtype: :class:`.KeyPair` object
|
|
"""
|
|
raise NotImplementedError(
|
|
'create_key_pair not implemented for this driver')
|
|
|
|
def import_key_pair_from_string(self, name, key_material):
|
|
# type: (str, str) -> KeyPair
|
|
"""
|
|
Import a new public key from string.
|
|
|
|
:param name: Key pair name.
|
|
:type name: ``str``
|
|
|
|
:param key_material: Public key material.
|
|
:type key_material: ``str``
|
|
|
|
:rtype: :class:`.KeyPair` object
|
|
"""
|
|
raise NotImplementedError(
|
|
'import_key_pair_from_string not implemented for this driver')
|
|
|
|
def import_key_pair_from_file(self, name, key_file_path):
|
|
# type: (str, str) -> KeyPair
|
|
"""
|
|
Import a new public key from string.
|
|
|
|
:param name: Key pair name.
|
|
:type name: ``str``
|
|
|
|
:param key_file_path: Path to the public key file.
|
|
:type key_file_path: ``str``
|
|
|
|
:rtype: :class:`.KeyPair` object
|
|
"""
|
|
key_file_path = os.path.expanduser(key_file_path)
|
|
|
|
with open(key_file_path, 'r') as fp:
|
|
key_material = fp.read().strip()
|
|
|
|
return self.import_key_pair_from_string(name=name,
|
|
key_material=key_material)
|
|
|
|
def delete_key_pair(self, key_pair):
|
|
# type: (KeyPair) -> bool
|
|
"""
|
|
Delete an existing key pair.
|
|
|
|
:param key_pair: Key pair object.
|
|
:type key_pair: :class:`.KeyPair`
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
raise NotImplementedError(
|
|
'delete_key_pair not implemented for this driver')
|
|
|
|
def wait_until_running(self,
|
|
nodes, # type: List[Node]
|
|
wait_period=5, # type: float
|
|
timeout=600, # type: int
|
|
ssh_interface='public_ips', # type: str
|
|
force_ipv4=True, # type: bool
|
|
ex_list_nodes_kwargs=None # type: Optional[Dict]
|
|
):
|
|
# type: (...) -> List[Tuple[Node, List[str]]]
|
|
"""
|
|
Block until the provided nodes are considered running.
|
|
|
|
Node is considered running when it's state is "running" and when it has
|
|
at least one IP address assigned.
|
|
|
|
:param nodes: List of nodes to wait for.
|
|
:type nodes: ``list`` of :class:`.Node`
|
|
|
|
:param wait_period: How many seconds to wait between each loop
|
|
iteration. (default is 3)
|
|
:type wait_period: ``int``
|
|
|
|
:param timeout: How many seconds to wait before giving up.
|
|
(default is 600)
|
|
:type timeout: ``int``
|
|
|
|
:param ssh_interface: Which attribute on the node to use to obtain
|
|
an IP address. Valid options: public_ips,
|
|
private_ips. Default is public_ips.
|
|
:type ssh_interface: ``str``
|
|
|
|
:param force_ipv4: Ignore IPv6 addresses (default is True).
|
|
:type force_ipv4: ``bool``
|
|
|
|
:param ex_list_nodes_kwargs: Optional driver-specific keyword arguments
|
|
which are passed to the ``list_nodes``
|
|
method.
|
|
:type ex_list_nodes_kwargs: ``dict``
|
|
|
|
:return: ``[(Node, ip_addresses)]`` list of tuple of Node instance and
|
|
list of ip_address on success.
|
|
:rtype: ``list`` of ``tuple``
|
|
"""
|
|
ex_list_nodes_kwargs = ex_list_nodes_kwargs or {}
|
|
|
|
def is_supported(address):
|
|
# type: (str) -> bool
|
|
"""
|
|
Return True for supported address.
|
|
"""
|
|
if force_ipv4 and not is_valid_ip_address(address=address,
|
|
family=socket.AF_INET):
|
|
return False
|
|
return True
|
|
|
|
def filter_addresses(addresses):
|
|
# type: (List[str]) -> List[str]
|
|
"""
|
|
Return list of supported addresses.
|
|
"""
|
|
return [address for address in addresses if is_supported(address)]
|
|
|
|
if ssh_interface not in ['public_ips', 'private_ips']:
|
|
raise ValueError('ssh_interface argument must either be ' +
|
|
'public_ips or private_ips')
|
|
|
|
start = time.time()
|
|
end = start + timeout
|
|
|
|
uuids = set([node.uuid for node in nodes])
|
|
|
|
while time.time() < end:
|
|
all_nodes = self.list_nodes(**ex_list_nodes_kwargs)
|
|
matching_nodes = list([node for node in all_nodes
|
|
if node.uuid in uuids])
|
|
|
|
if len(matching_nodes) > len(uuids):
|
|
found_uuids = [node.uuid for node in matching_nodes]
|
|
msg = ('Unable to match specified uuids ' +
|
|
'(%s) with existing nodes. Found ' % (uuids) +
|
|
'multiple nodes with same uuid: (%s)' % (found_uuids))
|
|
raise LibcloudError(value=msg, driver=self)
|
|
|
|
running_nodes = [node for node in matching_nodes
|
|
if node.state == NodeState.RUNNING]
|
|
addresses = []
|
|
for node in running_nodes:
|
|
node_addresses = filter_addresses(getattr(node, ssh_interface))
|
|
if len(node_addresses) >= 1:
|
|
addresses.append(node_addresses)
|
|
|
|
if len(running_nodes) == len(uuids) == len(addresses):
|
|
return list(zip(running_nodes, addresses))
|
|
else:
|
|
time.sleep(wait_period)
|
|
continue
|
|
|
|
raise LibcloudError(value='Timed out after %s seconds' % (timeout),
|
|
driver=self)
|
|
|
|
def _get_and_check_auth(self, auth):
|
|
# type: (T_Auth) -> T_Auth
|
|
"""
|
|
Helper function for providers supporting :class:`.NodeAuthPassword` or
|
|
:class:`.NodeAuthSSHKey`
|
|
|
|
Validates that only a supported object type is passed to the auth
|
|
parameter and raises an exception if it is not.
|
|
|
|
If no :class:`.NodeAuthPassword` object is provided but one is expected
|
|
then a password is automatically generated.
|
|
"""
|
|
|
|
if isinstance(auth, NodeAuthPassword):
|
|
if 'password' in self.features['create_node']:
|
|
return auth
|
|
raise LibcloudError(
|
|
'Password provided as authentication information, but password'
|
|
'not supported', driver=self)
|
|
|
|
if isinstance(auth, NodeAuthSSHKey):
|
|
if 'ssh_key' in self.features['create_node']:
|
|
return auth
|
|
raise LibcloudError(
|
|
'SSH Key provided as authentication information, but SSH Key'
|
|
'not supported', driver=self)
|
|
|
|
if 'password' in self.features['create_node']:
|
|
value = os.urandom(16)
|
|
value = binascii.hexlify(value).decode('ascii')
|
|
|
|
# Some providers require password to also include uppercase
|
|
# characters so convert some characters to uppercase
|
|
password = ''
|
|
for char in value:
|
|
if not char.isdigit() and char.islower():
|
|
if random.randint(0, 1) == 1:
|
|
char = char.upper()
|
|
|
|
password += char
|
|
|
|
return NodeAuthPassword(password, generated=True)
|
|
|
|
if auth:
|
|
raise LibcloudError(
|
|
'"auth" argument provided, but it was not a NodeAuthPassword'
|
|
'or NodeAuthSSHKey object', driver=self)
|
|
|
|
def _wait_until_running(self, node, wait_period=3, timeout=600,
|
|
ssh_interface='public_ips', force_ipv4=True):
|
|
# type: (Node, float, int, str, bool) -> List[Tuple[Node, List[str]]]
|
|
# This is here for backward compatibility and will be removed in the
|
|
# next major release
|
|
return self.wait_until_running(nodes=[node], wait_period=wait_period,
|
|
timeout=timeout,
|
|
ssh_interface=ssh_interface,
|
|
force_ipv4=force_ipv4)
|
|
|
|
def _ssh_client_connect(self, ssh_client, wait_period=1.5, timeout=300):
|
|
# type: (BaseSSHClient, float, int) -> BaseSSHClient
|
|
"""
|
|
Try to connect to the remote SSH server. If a connection times out or
|
|
is refused it is retried up to timeout number of seconds.
|
|
|
|
:param ssh_client: A configured SSHClient instance
|
|
:type ssh_client: ``SSHClient``
|
|
|
|
:param wait_period: How many seconds to wait between each loop
|
|
iteration. (default is 1.5)
|
|
:type wait_period: ``int``
|
|
|
|
:param timeout: How many seconds to wait before giving up.
|
|
(default is 300)
|
|
:type timeout: ``int``
|
|
|
|
:return: ``SSHClient`` on success
|
|
"""
|
|
start = time.time()
|
|
end = start + timeout
|
|
|
|
while time.time() < end:
|
|
try:
|
|
ssh_client.connect()
|
|
except SSH_TIMEOUT_EXCEPTION_CLASSES as e:
|
|
# Errors which represent fatal invalid key files which should
|
|
# be propagated to the user without us retrying
|
|
message = str(e).lower()
|
|
|
|
for fatal_msg in SSH_FATAL_ERROR_MSGS:
|
|
if fatal_msg in message:
|
|
raise e
|
|
|
|
# Retry if a connection is refused, timeout occurred,
|
|
# or the connection fails due to failed authentication.
|
|
try:
|
|
ssh_client.close()
|
|
except Exception:
|
|
# Exception on close() should not be fatal since client
|
|
# socket might already be closed
|
|
pass
|
|
|
|
time.sleep(wait_period)
|
|
continue
|
|
else:
|
|
return ssh_client
|
|
|
|
raise LibcloudError(value='Could not connect to the remote SSH ' +
|
|
'server. Giving up.', driver=self)
|
|
|
|
def _connect_and_run_deployment_script(
|
|
self,
|
|
task, # type: Deployment
|
|
node, # type: Node
|
|
ssh_hostname, # type: str
|
|
ssh_port, # type: int
|
|
ssh_username, # type: str
|
|
ssh_password, # type: Optional[str]
|
|
ssh_key_file, # type: Optional[T_Ssh_key]
|
|
ssh_key_password, # type: Optional[str]
|
|
ssh_timeout, # type: int
|
|
timeout, # type: int
|
|
max_tries # type: int
|
|
):
|
|
"""
|
|
Establish an SSH connection to the node and run the provided deployment
|
|
task.
|
|
|
|
:rtype: :class:`.Node`:
|
|
:return: Node instance on success.
|
|
"""
|
|
ssh_client = SSHClient(hostname=ssh_hostname,
|
|
port=ssh_port, username=ssh_username,
|
|
password=ssh_key_password or ssh_password,
|
|
key_files=ssh_key_file,
|
|
timeout=ssh_timeout)
|
|
|
|
ssh_client = self._ssh_client_connect(ssh_client=ssh_client,
|
|
timeout=timeout)
|
|
|
|
# Execute the deployment task
|
|
node = self._run_deployment_script(task=task, node=node,
|
|
ssh_client=ssh_client,
|
|
max_tries=max_tries)
|
|
return node
|
|
|
|
def _run_deployment_script(self, task, node, ssh_client, max_tries=3):
|
|
# type: (Deployment, Node, BaseSSHClient, int) -> Node
|
|
"""
|
|
Run the deployment script on the provided node. At this point it is
|
|
assumed that SSH connection has already been established.
|
|
|
|
:param task: Deployment task to run.
|
|
:type task: :class:`Deployment`
|
|
|
|
:param node: Node to run the task on.
|
|
:type node: ``Node``
|
|
|
|
:param ssh_client: A configured and connected SSHClient instance.
|
|
:type ssh_client: :class:`SSHClient`
|
|
|
|
:param max_tries: How many times to retry if a deployment fails
|
|
before giving up. (default is 3)
|
|
:type max_tries: ``int``
|
|
|
|
:rtype: :class:`.Node`
|
|
:return: ``Node`` Node instance on success.
|
|
"""
|
|
tries = 0
|
|
|
|
while tries < max_tries:
|
|
try:
|
|
node = task.run(node, ssh_client)
|
|
except SSHCommandTimeoutError as e:
|
|
# Command timeout exception is fatal so we don't retry it.
|
|
raise e
|
|
except Exception as e:
|
|
tries += 1
|
|
|
|
if "ssh session not active" in str(e).lower():
|
|
# Sometimes connection gets closed or disconnected half
|
|
# way through for wahtever reason.
|
|
# If this happens, we try to re-connect before
|
|
# re-attempting to run the step.
|
|
try:
|
|
ssh_client.close()
|
|
except Exception:
|
|
# Non fatal since connection is most likely already
|
|
# closed at this point
|
|
pass
|
|
|
|
timeout = (int(ssh_client.timeout) if ssh_client.timeout
|
|
else 10)
|
|
ssh_client = self._ssh_client_connect(
|
|
ssh_client=ssh_client,
|
|
timeout=timeout)
|
|
|
|
if tries >= max_tries:
|
|
tb = traceback.format_exc()
|
|
raise LibcloudError(value='Failed after %d tries: %s.\n%s'
|
|
% (max_tries, str(e), tb), driver=self)
|
|
else:
|
|
# Deployment succeeded
|
|
ssh_client.close()
|
|
return node
|
|
|
|
return node
|
|
|
|
def _get_size_price(self, size_id):
|
|
# type: (str) -> Optional[float]
|
|
"""
|
|
Return pricing information for the provided size id.
|
|
"""
|
|
return get_size_price(driver_type='compute',
|
|
driver_name=self.api_name,
|
|
size_id=size_id)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
doctest.testmod()
|