blob: 4c4a0b7734abb23f2e86be157d3da9761157882d [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.
"""
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
class G8ProvisionError(Exception):
pass
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)
def destroy(self):
self.driver.ex_delete_portforward(self)
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
def list_nodes(self):
return self.driver.list_nodes(self)
def destroy(self):
return self.driver.ex_destroy_network(self)
def list_portforwards(self):
return self.driver.ex_list_portforwards(self)
def create_portforward(self, node, publicport, privateport, protocol="tcp"):
return self.driver.ex_create_portforward(self, node, publicport, privateport, protocol)
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
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: Descripton 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
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
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 assing 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)
def ex_destroy_network(self, network):
# type (G8Network) -> bool
self._api_request("/cloudspaces/delete", {"cloudspaceId": int(network.id)})
return True
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
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
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)
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
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
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
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
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
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
def destroy_node(self, node):
# type (Node) -> bool
"""
Destroy node
"""
self._api_request("/machines/delete", {"machineId": int(node.id)})
return True
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
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
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: Descripton 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)
def destroy_volume(self, volume):
# type (StorageVolume) -> bool
self._api_request("/disks/delete", {"diskId": int(volume.id)})
return True
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
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()