| # 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"), |
| } |