Skip to content

Commit

Permalink
feat: Add new OpenNebula XMLRPC libcloud provider
Browse files Browse the repository at this point in the history
  • Loading branch information
sfayer committed Oct 17, 2023
1 parent 55a0293 commit 52e4911
Showing 1 changed file with 261 additions and 0 deletions.
261 changes: 261 additions & 0 deletions src/DIRAC/Resources/Computing/CloudProviders/OpenNebula.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
""" OpenNebula XML-RPC (Version 6) Driver
To use this in DIRAC, on the CE Resource set:
- CEType = Cloud
- CloudType = DIRAC.Resources.Computing.CloudProviders.OpenNebula.OpenNebula_6_0_NodeDriver
- Driver_host = <hostname of your cloud provider>
- Driver_port = 2633
- Driver_secure = True (for SSL)
- Instance_Image = name:<name of image to use>
- Instance_Flavor = name:<name of template to use>
"""

from libcloud.compute.drivers.opennebula import OpenNebulaNodeDriver
from libcloud.compute.base import NodeState
from libcloud.compute.base import NodeImage, NodeSize, StorageVolume
from libcloud.common.base import ConnectionUserAndKey, XmlResponse
from libcloud.common.xmlrpc import XMLRPCConnection, XMLRPCResponse


class OpenNebulaXMLRPCResponse(XMLRPCResponse):
"""
Class for protocol responses in the OpenNebula XML-RPC Protocol.
"""

def parse_body(self):
""" Decode the return body to extract the response status.
In error cases raise a LibCloudError with the inner message.
For successful requests, return either a base type or an XML
ElementTree of the response data.
"""
res = super(OpenNebulaXMLRPCResponse, self).parse_body()
success, value = res[0:2]
if not success:
# Non protocol error at server
# Value contains the error string
raise LibcloudError(value, driver=self)
# Value is either an XML string of a base object
# i.e. an int in the case of just an ID being returned
if isinstance(value, str):
return ET.fromstring(value)
else:
return value


class OpenNebulaXMLRPCConnection(XMLRPCConnection, ConnectionUserAndKey):
"""
Connection class for new OpenNebula XML-RPC protocol with basic
(username/password) auth.
"""
responseCls = OpenNebulaXMLRPCResponse
endpoint = '/RPC2'

def request(self, method, *args, **kwargs):
""" Call XML-RPC method on OpenNebula server using the standard
username/password authentication.
The method is called with "username:password" as the first
argument; other arguments are sent after this.
"""
# First parmaeter is the username/password auth string
auth_str = "%s:%s" % (self.user_id, self.key)
real_args = (method, auth_str) + args
return super(OpenNebulaXMLRPCConnection, self).request(*real_args,
**kwargs)


class OpenNebula_6_0_NodeDriver(OpenNebulaNodeDriver):
"""
OpenNebula.org node driver for OpenNebula.org v6.0.
"""

name = 'OpenNebula (v6.0)'
connectionCls = OpenNebulaXMLRPCConnection

# List function suppport filtering
# These are the parameters to get all entries
# (Filter -2 = ALL, Start = 0, End -1 = ALL
REQ_FILTER_ALL = (-2, 0, -1)
# Filter for VM state
# Unstopped is all nodes in a non-terminated state
REQ_FILTER_UNSTOPPED = -1
# Numeric State ID value mappings, used for node states
STATE_ID_MAP = {
# 0: Init
1: NodeState.PENDING,
2: NodeState.PAUSED, # Hold
3: NodeState.RUNNING, # Active
4: NodeState.STOPPED,
5: NodeState.SUSPENDED,
6: NodeState.TERMINATED, # Done
7: NodeState.ERROR, # Failed
8: NodeState.STOPPED, # Power Off
# 9: Undeployed
# 10: Cloning
# 11: Cloning Failure
}

def create_node(self, name, size, image=None, network=None, context=None,
ex_onhold=False, ex_tmpl_network=True):
tmpl_id = None
if isinstance(size, int):
tmpl_id = size
elif isinstance(size, NodeSize):
tmpl_id = size.id

if tmpl_id is not None:
return self._create_vm_template(name, tmpl_id, context, ex_onhold,
ex_tmpl_network)
else:
return self._create_vm_direct(name, size, image, network, context,
ex_onhold)

def _create_vm_template(self, name, tmpl_id, context, ex_onhold,
ex_tmpl_network):
extra_str = self._gen_context(context, ex_tmpl_network)
res = self.connection.request("one.template.instantiate",
tmpl_id,
name,
ex_onhold,
extra_str)
return self.ex_get_node(res.object)

def _create_vm_direct(self, name, size, image, network, context, ex_onhold):
tmpl_str = self._gen_template(name, size, image, network, context)
print(tmpl_str)
res = self.connection.request("one.vm.allocate", tmpl_str, ex_onhold)
return self.ex_get_node(res.object)

def destroy_node(self, node, ex_hard=False):
action = 'terminate'
if ex_hard:
action = 'terminate-hard'
return self.ex_node_action(action, node)

def reboot_node(self, node, ex_hard=False):
action = 'reboot'
if ex_hard:
action = 'reboot-hard'
return self.ex_node_action(action, node)

def list_images(self):
res = self.connection.request("one.imagepool.info",
*self.REQ_FILTER_ALL)
return self._to_images(res.object)

def list_nodes(self):
res = self.connection.request("one.vmpool.info",
*self.REQ_FILTER_ALL,
self.REQ_FILTER_UNSTOPPED)
return self._to_nodes(res.object)

def list_sizes(self, location=None):
res = self.connection.request("one.templatepool.info",
*self.REQ_FILTER_ALL)
return self._to_sizes(res.object)

def list_networks(self):
res = self.connection.request("one.vnpool.info",
*self.REQ_FILTER_ALL)
return self._to_networks(res.object)

def ex_get_node(self, node_id):
res = self.connection.request("one.vm.info", node_id)
return self._to_node(res.object)

def ex_node_action(self, action, node):
node_id = None
if isinstance(node, int):
node_id = node
else:
node_id = node.id
self.connection.request("one.vm.action", action, node_id)
# Action only returns ID, exception thrown on error
return None

def _to_images(self, images_obj):
images = []
for element in images_obj.findall('IMAGE'):
images.append(NodeImage(id=int(element.findtext('ID')),
name=element.findtext('NAME'),
driver=self))
return images

def _to_node(self, node_elem):
# Work out state
state_id = int(node_elem.findtext('STATE'))
state = NodeState.UNKNOWN
if state_id in self.STATE_ID_MAP:
state = self.STATE_ID_MAP[state_id]
# Find network IPs
# We can't distinguish between public/private
# So just store them all in private list
private_ips = []
template = node_elem.find('TEMPLATE')
if template:
for nic in template.findall('NIC'):
ip_addr = nic.findtext('IP')
if ip_addr:
private_ips.append(ip_addr)
return Node(id=node_elem.findtext('ID'),
name=node_elem.findtext('NAME'),
state=state,
public_ips=[],
private_ips=private_ips,
driver=self)

def _to_nodes(self, nodes_obj):
nodes = []
for element in nodes_obj.findall('VM'):
nodes.append(self._to_node(element))
return nodes

def _to_sizes(self, sizes_obj):
sizes = []
for element in sizes_obj.findall('VMTEMPLATE'):
template = element.find('TEMPLATE')
obj = OpenNebulaNodeSize(id=int(element.findtext('ID')),
name=element.findtext('NAME'),
ram=int(template.findtext('MEMORY')),
cpu=int(template.findtext('CPU')),
disk=None,
bandwidth=None,
price=None,
driver=self)
sizes.append(obj)
return sizes

def _to_networks(self, networks_obj):
networks = []
for element in networks_obj.findall('VNET'):
networks.append(OpenNebulaNetwork(id=int(element.findtext('ID')),
name=element.findtext('NAME'),
size=None,
address=None,
driver=self))
return networks

def _gen_context(self, context, en_network=True):
extra = []
if context is not None:
userdata = b64encode(bytes(context, 'utf-8')).decode('utf-8')
extra.append('USERDATA = "%s"' % userdata)
extra.append('USERDATA_ENCODING = "base64"')
if en_network:
extra.append('NETWORK = "YES"')
extra_str = ""
if extra:
extra_str = "CONTEXT = [\n" + ",\n".join(extra) + "\n]\n"
return extra_str

def _gen_template(self, name, size, image, network=None, context=None):
template = []
template.append('NAME = "%s"' % name)
template.append('CPU="%s"' % size.cpu)
template.append('MEMORY="%s"' % size.ram)
template.append('DISK = [ IMAGE_ID="%s" ]' % image.id)
en_network = network is not None
if en_network:
template.append('NIC = [ NETWORK = "%s" ]' % network.name)
if context is not None:
template.append(self._gen_context(context, en_network))
return "\n".join(template)

0 comments on commit 52e4911

Please sign in to comment.