Source code for libcloud.compute.drivers.gig_g8

# 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.
"""
GiG G8 Driver

"""
import json

from libcloud.compute.base import (
    Node,
    NodeSize,
    NodeImage,
    UuidMixin,
    NodeDriver,
    StorageVolume,
    NodeAuthSSHKey,
)
from libcloud.common.gig_g8 import G8Connection
from libcloud.compute.types import Provider, NodeState
from libcloud.common.exceptions import BaseHTTPError


[docs]class G8ProvisionError(Exception): pass
[docs]class G8PortForward(UuidMixin): def __init__(self, network, node_id, publicport, privateport, protocol, driver): self.node_id = node_id self.network = network self.publicport = int(publicport) self.privateport = int(privateport) self.protocol = protocol self.driver = driver UuidMixin.__init__(self)
[docs] def destroy(self): self.driver.ex_delete_portforward(self)
[docs]class G8Network(UuidMixin): """ G8 Network object class. This class maps to a cloudspace """ def __init__(self, id, name, cidr, publicipaddress, driver, extra=None): self.id = id self.name = name self._cidr = cidr self.driver = driver self.publicipaddress = publicipaddress self.extra = extra UuidMixin.__init__(self) @property def cidr(self): """ Cidr is not part of the list result we will lazily fetch it with a get request """ if self._cidr is None: networkdata = self.driver._api_request("/cloudspaces/get", {"cloudspaceId": self.id}) self._cidr = networkdata["privatenetwork"] return self._cidr
[docs] def list_nodes(self): return self.driver.list_nodes(self)
[docs] def destroy(self): return self.driver.ex_destroy_network(self)
[docs] def list_portforwards(self): return self.driver.ex_list_portforwards(self)
[docs] def create_portforward(self, node, publicport, privateport, protocol="tcp"): return self.driver.ex_create_portforward(self, node, publicport, privateport, protocol)
[docs]class G8NodeDriver(NodeDriver): """ GiG G8 node driver """ NODE_STATE_MAP = { "VIRTUAL": NodeState.PENDING, "HALTED": NodeState.STOPPED, "RUNNING": NodeState.RUNNING, "DESTROYED": NodeState.TERMINATED, "DELETED": NodeState.TERMINATED, "PAUSED": NodeState.PAUSED, "ERROR": NodeState.ERROR, # transition states "DEPLOYING": NodeState.PENDING, "STOPPING": NodeState.STOPPING, "MOVING": NodeState.MIGRATING, "RESTORING": NodeState.PENDING, "STARTING": NodeState.STARTING, "PAUSING": NodeState.PENDING, "RESUMING": NodeState.PENDING, "RESETTING": NodeState.REBOOTING, "DELETING": NodeState.TERMINATED, "DESTROYING": NodeState.TERMINATED, "ADDING_DISK": NodeState.RECONFIGURING, "ATTACHING_DISK": NodeState.RECONFIGURING, "DETACHING_DISK": NodeState.RECONFIGURING, "ATTACHING_NIC": NodeState.RECONFIGURING, "DETTACHING_NIC": NodeState.RECONFIGURING, "DELETING_DISK": NodeState.RECONFIGURING, "CHANGING_DISK_LIMITS": NodeState.RECONFIGURING, "CLONING": NodeState.PENDING, "RESIZING": NodeState.RECONFIGURING, "CREATING_TEMPLATE": NodeState.PENDING, } name = "GiG G8 Node Provider" website = "https://gig.tech" type = Provider.GIG_G8 connectionCls = G8Connection def __init__(self, user_id, key, api_url): # type (int, str, str) -> None """ :param key: Token to use for api (jwt) :type key: ``str`` :param user_id: Id of the account to connect to (accountId) :type user_id: ``int`` :param api_url: G8 api url :type api_url: ``str`` :rtype: ``None`` """ self._apiurl = api_url.rstrip("/") super().__init__(key=key) self._account_id = user_id self._location_data = None def _ex_connection_class_kwargs(self): return {"url": self._apiurl} def _api_request(self, endpoint, params=None): return self.connection.request( endpoint.lstrip("/"), data=json.dumps(params), method="POST" ).object @property def _location(self): if self._location_data is None: self._location_data = self._api_request("/locations/list")[0] return self._location_data
[docs] def create_node( self, name, image, ex_network, ex_description, size=None, auth=None, ex_create_attr=None, ex_expose_ssh=False, ): # type (str, Image, G8Network, str, Size, # Optional[NodeAuthSSHKey], Optional[Dict], bool) -> Node """ Create a node. The `ex_create_attr` parameter can include the following dictionary key and value pairs: * `memory`: ``int`` Memory in MiB (only used if size is None and vcpus is passed * `vcpus`: ``int`` Amount of vcpus (only used if size is None and memory is passed) * `disk_size`: ``int`` Size of bootdisk defaults to minimumsize of the image * `user_data`: ``str`` for cloud-config data * `private_ip`: ``str`` Private Ip inside network * `data_disks`: ``list(int)`` Extra data disks to assign to vm list of disk sizes in GiB :param name: the name to assign the vm :type name: ``str`` :param size: the plan size to create mutual exclusive with `memory` `vcpus` :type size: :class:`NodeSize` :param image: which distribution to deploy on the vm :type image: :class:`NodeImage` :param network: G8 Network to place vm in :type size: :class:`G8Network` :param ex_description: Description of vm :type size: : ``str`` :param auth: an SSH key :type auth: :class:`NodeAuthSSHKey` :param ex_create_attr: A dictionary of optional attributes for vm creation :type ex_create_attr: ``dict`` :param ex_expose_ssh: Create portforward for ssh port :type ex_expose_ssh: int :return: The newly created node. :rtype: :class:`Node` """ params = { "name": name, "imageId": int(image.id), "cloudspaceId": int(ex_network.id), "description": ex_description, } ex_create_attr = ex_create_attr or {} if size: params["sizeId"] = int(size.id) else: params["memory"] = ex_create_attr["memory"] params["vcpus"] = ex_create_attr["vcpus"] if "user_data" in ex_create_attr: params["userdata"] = ex_create_attr["user_data"] if "data_disks" in ex_create_attr: params["datadisks"] = ex_create_attr["data_disks"] if "private_ip" in ex_create_attr: params["privateIp"] = ex_create_attr["private_ip"] if "disk_size" in ex_create_attr: params["disksize"] = ex_create_attr["disk_size"] else: params["disksize"] = image.extra["min_disk_size"] if auth and isinstance(auth, NodeAuthSSHKey): userdata = params.setdefault("userdata", {}) users = userdata.setdefault("users", []) root = None for user in users: if user["name"] == "root": root = user break else: root = {"name": "root", "shell": "/bin/bash"} users.append(root) keys = root.setdefault("ssh-authorized-keys", []) keys.append(auth.pubkey) elif auth: error = "Auth type {} is not implemented".format(type(auth)) raise NotImplementedError(error) machineId = self._api_request("/machines/create", params) machine = self._api_request("/machines/get", params={"machineId": machineId}) node = self._to_node(machine, ex_network) if ex_expose_ssh: port = self.ex_expose_ssh_node(node) node.extra["ssh_port"] = port node.extra["ssh_ip"] = ex_network.publicipaddress return node
def _find_ssh_ports(self, ex_network, node): forwards = ex_network.list_portforwards() usedports = [] result = {"node": None, "network": usedports} for forward in forwards: usedports.append(forward.publicport) if forward.node_id == node.id and forward.privateport == 22: result["node"] = forward.privateport return result
[docs] def ex_expose_ssh_node(self, node): """ Create portforward for ssh purposed :param node: Node to expose ssh for :type node: ``Node`` :rtype: ``int`` """ network = node.extra["network"] ports = self._find_ssh_ports(network, node) if ports["node"]: return ports["node"] usedports = ports["network"] sshport = 2200 endport = 3000 while sshport < endport: while sshport in usedports: sshport += 1 try: network.create_portforward(node, sshport, 22) node.extra["ssh_port"] = sshport node.extra["ssh_ip"] = network.publicipaddress break except BaseHTTPError as e: if e.code == 409: # port already used maybe raise let's try next usedports.append(sshport) raise else: raise G8ProvisionError("Failed to create portforward") return sshport
[docs] def ex_create_network(self, name, private_network="192.168.103.0/24", type="vgw"): # type (str, str, str) -> G8Network """ Create network also known as cloudspace :param name: the name to assign to the network :type name: ``str`` :param private_network: subnet used as private network :type private_network: ``str`` :param type: type of the gateway vgw or routeros :type type: ``str`` """ userinfo = self._api_request("../system/usermanager/whoami") params = { "accountId": self._account_id, "privatenetwork": private_network, "access": userinfo["name"], "name": name, "location": self._location["locationCode"], "type": type, } networkid = self._api_request("/cloudspaces/create", params) network = self._api_request("/cloudspaces/get", {"cloudspaceId": networkid}) return self._to_network(network)
[docs] def ex_destroy_network(self, network): # type (G8Network) -> bool self._api_request("/cloudspaces/delete", {"cloudspaceId": int(network.id)}) return True
[docs] def stop_node(self, node): # type (Node) -> bool """ Stop virtual machine """ node.state = NodeState.STOPPING self._api_request("/machines/stop", {"machineId": int(node.id)}) node.state = NodeState.STOPPED return True
[docs] def ex_list_portforwards(self, network): # type (G8Network) -> List[G8PortForward] data = self._api_request("/portforwarding/list", {"cloudspaceId": int(network.id)}) forwards = [] for forward in data: forwards.append(self._to_port_forward(forward, network)) return forwards
[docs] def ex_create_portforward(self, network, node, publicport, privateport, protocol="tcp"): # type (G8Network, Node, int, int, str) -> G8PortForward params = { "cloudspaceId": int(network.id), "machineId": int(node.id), "localPort": privateport, "publicPort": publicport, "publicIp": network.publicipaddress, "protocol": protocol, } self._api_request("/portforwarding/create", params) return self._to_port_forward(params, network)
[docs] def ex_delete_portforward(self, portforward): # type (G8PortForward) -> bool params = { "cloudspaceId": int(portforward.network.id), "publicIp": portforward.network.publicipaddress, "publicPort": portforward.publicport, "proto": portforward.protocol, } self._api_request("/portforwarding/deleteByPort", params) return True
[docs] def start_node(self, node): # type (Node) -> bool """ Start virtual machine """ node.state = NodeState.STARTING self._api_request("/machines/start", {"machineId": int(node.id)}) node.state = NodeState.RUNNING return True
[docs] def ex_list_networks(self): # type () -> List[G8Network] """ Return the list of networks. :return: A list of network objects. :rtype: ``list`` of :class:`G8Network` """ networks = [] for network in self._api_request("/cloudspaces/list"): if network["accountId"] == self._account_id: networks.append(self._to_network(network)) return networks
[docs] def list_sizes(self): # type () -> List[Size] """ Returns a list of node sizes as a cloud provider might have """ location = self._location["locationCode"] sizes = [] for size in self._api_request("/sizes/list", {"location": location}): sizes.extend(self._to_size(size)) return sizes
[docs] def list_nodes(self, ex_network=None): # type (Optional[G8Network]) -> List[Node] """ List the nodes known to a particular driver; There are two default nodes created at the beginning """ def _get_ssh_port(forwards, node): for forward in forwards: if forward.node_id == node.id and forward.privateport == 22: return forward if ex_network: networks = [ex_network] else: networks = self.ex_list_networks() nodes = [] for network in networks: nodes_list = self._api_request("/machines/list", params={"cloudspaceId": network.id}) forwards = network.list_portforwards() for nodedata in nodes_list: node = self._to_node(nodedata, network) sshforward = _get_ssh_port(forwards, node) if sshforward: node.extra["ssh_port"] = sshforward.publicport node.extra["ssh_ip"] = network.publicipaddress nodes.append(node) return nodes
[docs] def reboot_node(self, node): # type (Node) -> bool """ Reboot node returns True as if the reboot had been successful. """ node.state = NodeState.REBOOTING self._api_request("/machines/reboot", {"machineId": int(node.id)}) node.state = NodeState.RUNNING return True
[docs] def destroy_node(self, node): # type (Node) -> bool """ Destroy node """ self._api_request("/machines/delete", {"machineId": int(node.id)}) return True
[docs] def list_images(self): # type () -> List[Image] """ Returns a list of images as a cloud provider might have @inherits: :class:`NodeDriver.list_images` """ images = [] for image in self._api_request("/images/list", {"accountId": self._account_id}): images.append(self._to_image(image)) return images
[docs] def list_volumes(self): # type () -> List[StorageVolume] volumes = [] for disk in self._api_request("/disks/list", {"accountId": self._account_id}): if disk["status"] not in ["ASSIGNED", "CREATED"]: continue volumes.append(self._to_volume(disk)) return volumes
[docs] def create_volume(self, size, name, ex_description, ex_disk_type="D"): # type (int, str, str, Optional[str]) -> StorageVolume """ Create volume :param size: Size of the volume to create in GiB :type size: ``int`` :param name: Name of the volume :type name: ``str`` :param description: Description of the volume :type description: ``str`` :param disk_type: Type of the disk depending on the G8 D for datadisk is always available :type disk_type: ``str`` :rtype: class:`StorageVolume` """ params = { "size": size, "name": name, "type": ex_disk_type, "description": ex_description, "gid": self._location["gid"], "accountId": self._account_id, } diskId = self._api_request("/disks/create", params) disk = self._api_request("/disks/get", {"diskId": diskId}) return self._to_volume(disk)
[docs] def destroy_volume(self, volume): # type (StorageVolume) -> bool self._api_request("/disks/delete", {"diskId": int(volume.id)}) return True
[docs] def attach_volume(self, node, volume): # type (Node, StorageVolume) -> bool params = {"machineId": int(node.id), "diskId": int(volume.id)} self._api_request("/machines/attachDisk", params) return True
[docs] def detach_volume(self, node, volume): # type (Node, StorageVolume) -> bool params = {"machineId": int(node.id), "diskId": int(volume.id)} self._api_request("/machines/detachDisk", params) return True
def _to_volume(self, data): # type (dict) -> StorageVolume extra = {"type": data["type"], "node_id": data.get("machineId")} return StorageVolume( id=str(data["id"]), size=data["sizeMax"], name=data["name"], driver=self, extra=extra, ) def _to_node(self, nodedata, ex_network): # type (dict) -> Node state = self.NODE_STATE_MAP.get(nodedata["status"], NodeState.UNKNOWN) public_ips = [] private_ips = [] nics = nodedata.get("nics", []) if not nics: nics = nodedata.get("interfaces", []) for nic in nics: if nic["type"] == "PUBLIC": public_ips.append(nic["ipAddress"].split("/")[0]) else: private_ips.append(nic["ipAddress"]) extra = {"network": ex_network} for account in nodedata.get("accounts", []): extra["password"] = account["password"] extra["username"] = account["login"] return Node( id=str(nodedata["id"]), name=nodedata["name"], driver=self, public_ips=public_ips, private_ips=private_ips, state=state, extra=extra, ) def _to_network(self, network): # type (dict) -> G8Network return G8Network( str(network["id"]), network["name"], None, network["externalnetworkip"], self, ) def _to_image(self, image): # type (dict) -> Image extra = { "min_disk_size": image["bootDiskSize"], "min_memory": image["memory"], } return NodeImage(id=str(image["id"]), name=image["name"], driver=self, extra=extra) def _to_size(self, size): # type (dict) -> Size sizes = [] for disk in size["disks"]: sizes.append( NodeSize( id=str(size["id"]), name=size["name"], ram=size["memory"], disk=disk, driver=self, extra={"vcpus": size["vcpus"]}, bandwidth=0, price=0, ) ) return sizes def _to_port_forward(self, data, ex_network): # type (dict, G8Network) -> G8PortForward return G8PortForward( ex_network, str(data["machineId"]), data["publicPort"], data["localPort"], data["protocol"], self, )
if __name__ == "__main__": import doctest doctest.testmod()