blob: 85f77ba93e252ab083ef6246ebb462c6d4d0a342 [file] [log] [blame]
# 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.
import json
import time
from libcloud.common.exceptions import BaseHTTPError
from libcloud.common.types import LibcloudError
class UpcloudTimeoutException(LibcloudError):
pass
class UpcloudCreateNodeRequestBody(object):
"""
Body of the create_node request
Takes the create_node arguments (**kwargs) and constructs the request body
:param name: Name of the created server (required)
:type name: ``str``
:param size: The size of resources allocated to this node.
:type size: :class:`.NodeSize`
:param image: OS Image to boot on node.
: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`
:param ex_hostname: Hostname. Default is 'localhost'. (optional)
:type ex_hostname: ``str``
:param ex_username: User's username, which is created.
Default is 'root'. (optional)
:type ex_username: ``str``
"""
def __init__(
self,
name,
size,
image,
location,
auth=None,
ex_hostname="localhost",
ex_username="root",
):
self.body = {
"server": {
"title": name,
"hostname": ex_hostname,
"plan": size.id,
"zone": location.id,
"login_user": _LoginUser(ex_username, auth).to_dict(),
"storage_devices": _StorageDevice(image, size).to_dict(),
}
}
def to_json(self):
"""
Serializes the body to json
:return: JSON string
:rtype: ``str``
"""
return json.dumps(self.body)
class UpcloudNodeDestroyer(object):
"""
Helper class for destroying node.
Node must be first stopped and then it can be
destroyed
:param upcloud_node_operations: UpcloudNodeOperations instance
:type upcloud_node_operations: :class:`.UpcloudNodeOperations`
:param sleep_func: Callable function, which sleeps.
Takes int argument to sleep in seconds (optional)
:type sleep_func: ``function``
"""
WAIT_AMOUNT = 2
SLEEP_COUNT_TO_TIMEOUT = 20
def __init__(self, upcloud_node_operations, sleep_func=None):
self._operations = upcloud_node_operations
self._sleep_func = sleep_func or time.sleep
self._sleep_count = 0
def destroy_node(self, node_id):
"""
Destroys the given node.
:param node_id: Id of the Node.
:type node_id: ``int``
"""
self._stop_called = False
self._sleep_count = 0
return self._do_destroy_node(node_id)
def _do_destroy_node(self, node_id):
state = self._operations.get_node_state(node_id)
if state == "stopped":
self._operations.destroy_node(node_id)
return True
elif state == "error":
return False
elif state == "started":
if not self._stop_called:
self._operations.stop_node(node_id)
self._stop_called = True
else:
# Waiting for started state to change and
# not calling stop again
self._sleep()
return self._do_destroy_node(node_id)
elif state == "maintenance":
# Lets wait maintenace state to go away and retry destroy
self._sleep()
return self._do_destroy_node(node_id)
elif state is None: # Server not found any more
return True
def _sleep(self):
if self._sleep_count > self.SLEEP_COUNT_TO_TIMEOUT:
raise UpcloudTimeoutException("Timeout, could not destroy node")
self._sleep_count += 1
self._sleep_func(self.WAIT_AMOUNT)
class UpcloudNodeOperations(object):
"""
Helper class to start and stop node.
:param conneciton: Connection instance
:type connection: :class:`.UpcloudConnection`
"""
def __init__(self, connection):
self.connection = connection
def stop_node(self, node_id):
"""
Stops the node
:param node_id: Id of the Node
:type node_id: ``int``
"""
body = {"stop_server": {"stop_type": "hard"}}
self.connection.request(
"1.2/server/{0}/stop".format(node_id), method="POST", data=json.dumps(body)
)
def get_node_state(self, node_id):
"""
Get the state of the node.
:param node_id: Id of the Node
:type node_id: ``int``
:rtype: ``str``
"""
action = "1.2/server/{0}".format(node_id)
try:
response = self.connection.request(action)
return response.object["server"]["state"]
except BaseHTTPError as e:
if e.code == 404:
return None
raise
def destroy_node(self, node_id):
"""
Destroys the node.
:param node_id: Id of the Node
:type node_id: ``int``
"""
self.connection.request("1.2/server/{0}".format(node_id), method="DELETE")
class PlanPrice(object):
"""
Helper class to construct plan price in different zones
:param zone_prices: List of prices in different zones in UpCloud
:type zone_prices: ```list```
"""
def __init__(self, zone_prices):
self._zone_prices = zone_prices
def get_price(self, plan_name, location=None):
"""
Returns the plan's price in location. If location
is not provided returns None
:param plan_name: Name of the plan
:type plan_name: ```str```
:param location: Location, which price is returned (optional)
:type location: :class:`.NodeLocation`
rtype: ``float``
"""
if location is None:
return None
server_plan_name = "server_plan_" + plan_name
for zone_price in self._zone_prices:
if zone_price["name"] == location.id:
return zone_price.get(server_plan_name, {}).get("price")
return None
class _LoginUser(object):
def __init__(self, user_id, auth=None):
self.user_id = user_id
self.auth = auth
def to_dict(self):
login_user = {"username": self.user_id}
if self.auth is not None:
login_user["ssh_keys"] = {"ssh_key": [self.auth.pubkey]}
else:
login_user["create_password"] = "yes"
return login_user
class _StorageDevice(object):
def __init__(self, image, size):
self.image = image
self.size = size
def to_dict(self):
extra = self.image.extra
if extra["type"] == "template":
return self._storage_device_for_template_image()
elif extra["type"] == "cdrom":
return self._storage_device_for_cdrom_image()
def _storage_device_for_template_image(self):
hdd_device = {"action": "clone", "storage": self.image.id}
hdd_device.update(self._common_hdd_device())
return {"storage_device": [hdd_device]}
def _storage_device_for_cdrom_image(self):
hdd_device = {"action": "create"}
hdd_device.update(self._common_hdd_device())
storage_devices = {
"storage_device": [
hdd_device,
{"action": "attach", "storage": self.image.id, "type": "cdrom"},
]
}
return storage_devices
def _common_hdd_device(self):
return {
"title": self.image.name,
"size": self.size.disk,
"tier": self.size.extra.get("storage_tier", "maxiops"),
}