Skip to content

Commit f0bb2f7

Browse files
authored
Merge pull request #7248 from ic-hep/cloudce_ext_provider
[8.0] Add OpenNebula XMLRPC libcloud driver + support in CloudCE
2 parents 60b50ec + ae1ef52 commit f0bb2f7

File tree

2 files changed

+292
-8
lines changed

2 files changed

+292
-8
lines changed

src/DIRAC/Resources/Computing/CloudComputingElement.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
CloudType:
8080
(Required) This should match the libcloud driver name for the Cloud you're
8181
trying to access. e.g. For OpenStack this should be "OPENSTACK".
82+
You can also specify a fully qualified class name to register and use as
83+
a driver: For example if your class is "MyNodeDriver" in
84+
"MyPkg/Prov/Driver.py", use "MyPkg.Prov.Driver.MyNodeDriver" here.
8285
8386
CloudAuth:
8487
(Optional) This sets the path to the authentication ini file as described
@@ -151,7 +154,7 @@
151154
import configparser
152155
import datetime
153156
from libcloud.compute.types import Provider, NodeState
154-
from libcloud.compute.providers import get_driver
157+
from libcloud.compute.providers import get_driver, set_driver
155158
from email.mime.text import MIMEText
156159
from email.mime.multipart import MIMEMultipart
157160

@@ -234,13 +237,25 @@ def _getDriver(self, refresh=False):
234237
if self._cloudDriver and not refresh:
235238
return self._cloudDriver
236239

237-
provName = self.ceParameters.get(OPT_PROVIDER, "").upper()
238-
# check if provider (type of cloud) exists
239-
if not provName or not hasattr(Provider, provName):
240-
self.log.error(f"Provider '{provName}' not found in libcloud for CE {self.ceName}.")
241-
raise RuntimeError(f"Provider '{provName}' not found in libcloud for CE {self.ceName}.")
242-
provIntName = getattr(Provider, provName)
243-
provCls = get_driver(provIntName)
240+
provName = self.ceParameters.get(OPT_PROVIDER, "")
241+
if "." in provName:
242+
# Custom driver class: register the class with libcloud if it isn't already there
243+
try:
244+
provCls = get_driver(provName)
245+
except AttributeError:
246+
# Driver not registered yet
247+
provModule, provClass = provName.rsplit(".", 1)
248+
set_driver(provName, provModule, provClass)
249+
provCls = get_driver(provName)
250+
else:
251+
# Standard driver class: use the in-build libcloud provider library
252+
# check if provider (type of cloud) exists
253+
provName = provName.upper()
254+
if not provName or not hasattr(Provider, provName):
255+
self.log.error(f"Provider '{provName}' not found in libcloud for CE {self.ceName}.")
256+
raise RuntimeError(f"Provider '{provName}' not found in libcloud for CE {self.ceName}.")
257+
provIntName = getattr(Provider, provName)
258+
provCls = get_driver(provIntName)
244259
driverOpts = self._getDriverOptions()
245260
driverKey, driverOpts["secret"] = self._getDriverAuth()
246261
self._cloudDriver = provCls(driverKey, **driverOpts)
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
""" OpenNebula XML-RPC (Version 6) Driver
2+
3+
To use this in DIRAC, on the CE Resource set:
4+
- CEType = Cloud
5+
- CloudType = DIRAC.Resources.Computing.CloudProviders.OpenNebula.OpenNebula_6_0_NodeDriver
6+
- Driver_host = <hostname of your cloud provider>
7+
- Driver_port = 2633
8+
- Driver_secure = True (for SSL)
9+
- Instance_Image = name:<name of image to use>
10+
- Instance_Flavor = name:<name of template to use>
11+
12+
(Key and Secret should be set to your username & password in cloud.auth)
13+
"""
14+
15+
from base64 import b64encode
16+
from libcloud.utils.py3 import ET
17+
from libcloud.compute.drivers.opennebula import OpenNebulaNodeDriver, OpenNebulaNodeSize, OpenNebulaNetwork
18+
from libcloud.compute.base import NodeDriver, NodeState, Node
19+
from libcloud.compute.base import NodeImage, NodeSize, StorageVolume
20+
from libcloud.common.base import ConnectionUserAndKey, XmlResponse
21+
from libcloud.common.types import LibcloudError
22+
from libcloud.common.xmlrpc import XMLRPCConnection, XMLRPCResponse
23+
24+
25+
class OpenNebulaXMLRPCResponse(XMLRPCResponse):
26+
"""
27+
Class for protocol responses in the OpenNebula XML-RPC Protocol.
28+
"""
29+
30+
def parse_body(self):
31+
"""Decode the return body to extract the response status.
32+
In error cases raise a LibCloudError with the inner message.
33+
For successful requests, return either a base type or an XML
34+
ElementTree of the response data.
35+
"""
36+
res = super().parse_body()
37+
success, value = res[0:2]
38+
if not success:
39+
# Non protocol error at server
40+
# Value contains the error string
41+
raise LibcloudError(value, driver=self)
42+
# Value is either an XML string of a base object
43+
# i.e. an int in the case of just an ID being returned
44+
if isinstance(value, str):
45+
return ET.fromstring(value)
46+
else:
47+
return value
48+
49+
50+
class OpenNebulaXMLRPCConnection(XMLRPCConnection, ConnectionUserAndKey):
51+
"""
52+
Connection class for new OpenNebula XML-RPC protocol with basic
53+
(username/password) auth.
54+
"""
55+
56+
responseCls = OpenNebulaXMLRPCResponse
57+
endpoint = "/RPC2"
58+
59+
def request(self, method, *args, **kwargs):
60+
"""Call XML-RPC method on OpenNebula server using the standard
61+
username/password authentication.
62+
The method is called with "username:password" as the first
63+
argument; other arguments are sent after this.
64+
"""
65+
# First parmaeter is the username/password auth string
66+
auth_str = f"{self.user_id}:{self.key}"
67+
real_args = (method, auth_str) + args
68+
return super().request(*real_args, **kwargs)
69+
70+
71+
class OpenNebula_6_0_NodeDriver(OpenNebulaNodeDriver):
72+
"""
73+
OpenNebula.org node driver for OpenNebula.org v6.0.
74+
"""
75+
76+
name = "OpenNebula (v6.0)"
77+
connectionCls = OpenNebulaXMLRPCConnection
78+
79+
# List function suppport filtering
80+
# These are the parameters to get all entries
81+
# (Filter -2 = ALL, Start = 0, End -1 = ALL
82+
REQ_FILTER_ALL = (-2, 0, -1)
83+
# Filter for VM state
84+
# Unstopped is all nodes in a non-terminated state
85+
REQ_FILTER_UNSTOPPED = -1
86+
# Numeric State ID value mappings, used for node states
87+
STATE_ID_MAP = {
88+
# 0: Init
89+
1: NodeState.PENDING,
90+
2: NodeState.PAUSED, # Hold
91+
3: NodeState.RUNNING, # Active
92+
4: NodeState.STOPPED,
93+
5: NodeState.SUSPENDED,
94+
6: NodeState.TERMINATED, # Done
95+
7: NodeState.ERROR, # Failed
96+
8: NodeState.STOPPED, # Power Off
97+
# 9: Undeployed
98+
# 10: Cloning
99+
# 11: Cloning Failure
100+
}
101+
102+
def __new__(cls, *args, **kwargs):
103+
return super(NodeDriver, cls).__new__(cls)
104+
105+
def create_node(
106+
self, name, size, image=None, network=None, context=None, ex_onhold=False, ex_tmpl_network=True, **kwargs
107+
):
108+
tmpl_id = None
109+
if isinstance(size, int):
110+
tmpl_id = size
111+
elif isinstance(size, NodeSize):
112+
tmpl_id = int(size.id)
113+
114+
# Allow use of ex_userdata in place of context
115+
if context is None and "ex_userdata" in kwargs:
116+
context = kwargs["ex_userdata"]
117+
118+
if tmpl_id is not None:
119+
return self._create_vm_template(name, tmpl_id, context, ex_onhold, ex_tmpl_network)
120+
else:
121+
return self._create_vm_direct(name, size, image, network, context, ex_onhold)
122+
123+
def _create_vm_template(self, name, tmpl_id, context, ex_onhold, ex_tmpl_network):
124+
extra_str = self._gen_context(context, ex_tmpl_network)
125+
res = self.connection.request("one.template.instantiate", tmpl_id, name, ex_onhold, extra_str)
126+
return self.ex_get_node_details(res.object)
127+
128+
def _create_vm_direct(self, name, size, image, network, context, ex_onhold):
129+
tmpl_str = self._gen_template(name, size, image, network, context)
130+
res = self.connection.request("one.vm.allocate", tmpl_str, ex_onhold)
131+
return self.ex_get_node_details(res.object)
132+
133+
def destroy_node(self, node, ex_hard=False):
134+
action = "terminate"
135+
if ex_hard:
136+
action = "terminate-hard"
137+
return self.ex_node_action(action, node)
138+
139+
def reboot_node(self, node, ex_hard=False):
140+
action = "reboot"
141+
if ex_hard:
142+
action = "reboot-hard"
143+
return self.ex_node_action(action, node)
144+
145+
def list_images(self):
146+
res = self.connection.request("one.imagepool.info", *self.REQ_FILTER_ALL)
147+
return self._to_images(res.object)
148+
149+
def list_nodes(self):
150+
res = self.connection.request("one.vmpool.info", *self.REQ_FILTER_ALL, self.REQ_FILTER_UNSTOPPED)
151+
return self._to_nodes(res.object)
152+
153+
def list_sizes(self, location=None):
154+
res = self.connection.request("one.templatepool.info", *self.REQ_FILTER_ALL)
155+
return self._to_sizes(res.object)
156+
157+
def list_networks(self):
158+
res = self.connection.request("one.vnpool.info", *self.REQ_FILTER_ALL)
159+
return self._to_networks(res.object)
160+
161+
def ex_get_node_details(self, node_id):
162+
res = self.connection.request("one.vm.info", node_id)
163+
return self._to_node(res.object)
164+
165+
def ex_node_action(self, action, node):
166+
node_id = None
167+
if isinstance(node, int):
168+
node_id = node
169+
else:
170+
node_id = node.id
171+
self.connection.request("one.vm.action", action, node_id)
172+
# Action only returns ID, exception thrown on error
173+
return None
174+
175+
def _to_images(self, images_obj):
176+
images = []
177+
for element in images_obj.findall("IMAGE"):
178+
images.append(NodeImage(id=int(element.findtext("ID")), name=element.findtext("NAME"), driver=self))
179+
return images
180+
181+
def _to_node(self, node_elem):
182+
# Work out state
183+
state_id = int(node_elem.findtext("STATE"))
184+
state = NodeState.UNKNOWN
185+
if state_id in self.STATE_ID_MAP:
186+
state = self.STATE_ID_MAP[state_id]
187+
# Find network IPs
188+
# We can't distinguish between public/private
189+
# So just store them all in private list
190+
private_ips = []
191+
template = node_elem.find("TEMPLATE")
192+
if template:
193+
for nic in template.findall("NIC"):
194+
ip_addr = nic.findtext("IP")
195+
if ip_addr:
196+
private_ips.append(ip_addr)
197+
return Node(
198+
id=node_elem.findtext("ID"),
199+
name=node_elem.findtext("NAME"),
200+
state=state,
201+
public_ips=[],
202+
private_ips=private_ips,
203+
driver=self,
204+
)
205+
206+
def _to_nodes(self, nodes_obj):
207+
nodes = []
208+
for element in nodes_obj.findall("VM"):
209+
nodes.append(self._to_node(element))
210+
return nodes
211+
212+
def _to_sizes(self, sizes_obj):
213+
sizes = []
214+
for element in sizes_obj.findall("VMTEMPLATE"):
215+
template = element.find("TEMPLATE")
216+
size_ram = template.findtext("MEMORY")
217+
if size_ram is not None:
218+
size_ram = int(size_ram)
219+
size_cpu = template.findtext("CPU")
220+
if size_cpu is not None:
221+
size_cpu = int(size_cpu)
222+
obj = OpenNebulaNodeSize(
223+
id=int(element.findtext("ID")),
224+
name=element.findtext("NAME"),
225+
ram=size_ram,
226+
cpu=size_cpu,
227+
disk=None,
228+
bandwidth=None,
229+
price=None,
230+
driver=self,
231+
)
232+
sizes.append(obj)
233+
return sizes
234+
235+
def _to_networks(self, networks_obj):
236+
networks = []
237+
for element in networks_obj.findall("VNET"):
238+
networks.append(
239+
OpenNebulaNetwork(
240+
id=int(element.findtext("ID")), name=element.findtext("NAME"), size=None, address=None, driver=self
241+
)
242+
)
243+
return networks
244+
245+
def _gen_context(self, context, en_network=True):
246+
extra = []
247+
if context is not None:
248+
userdata = b64encode(bytes(context, "utf-8")).decode("utf-8")
249+
extra.append(f'USERDATA = "{userdata}"')
250+
extra.append('USERDATA_ENCODING = "base64"')
251+
if en_network:
252+
extra.append('NETWORK = "YES"')
253+
extra_str = ""
254+
if extra:
255+
extra_str = "CONTEXT = [\n" + ",\n".join(extra) + "\n]\n"
256+
return extra_str
257+
258+
def _gen_template(self, name, size, image, network=None, context=None):
259+
template = []
260+
template.append(f'NAME = "{name}"')
261+
template.append(f'CPU="{size.cpu}"')
262+
template.append(f'MEMORY="{size.ram}"')
263+
template.append(f'DISK = [ IMAGE_ID="{image.id}" ]')
264+
en_network = network is not None
265+
if en_network:
266+
template.append(f'NIC = [ NETWORK = "{network.name}" ]')
267+
if context is not None:
268+
template.append(self._gen_context(context, en_network))
269+
return "\n".join(template)

0 commit comments

Comments
 (0)