297 lines
8.7 KiB
Python
297 lines
8.7 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.
|
|
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')
|
|
}
|