2 # NEPI, a framework to manage network experiments
3 # Copyright (C) 2013 INRIA
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19 # Lucia Guevgeozian <lucia.guevgeozian_odizzio@inria.fr>
21 from nepi.execution.attribute import Attribute, Flags, Types
22 from nepi.execution.resource import ResourceManager, clsinit_copy, ResourceState, \
24 from nepi.resources.linux.node import LinuxNode
25 from nepi.resources.planetlab.plcapi import PLCAPIFactory
26 from nepi.util.execfuncs import lexec
27 from nepi.util import sshfuncs
29 from random import randint
34 class PlanetlabNode(LinuxNode):
35 _rtype = "PlanetlabNode"
36 _help = "Controls a PlanetLab host accessible using a SSH key " \
37 "associated to a PlanetLab user account"
38 _backend = "planetlab"
40 lock = threading.Lock()
43 def _register_attributes(cls):
44 ip = Attribute("ip", "PlanetLab host public IP address",
45 flags = Flags.ReadOnly)
47 pl_url = Attribute("plcApiUrl", "URL of PlanetLab PLCAPI host \
48 (e.g. www.planet-lab.eu or www.planet-lab.org) ",
49 default = "www.planet-lab.eu",
50 flags = Flags.Credential)
52 pl_ptn = Attribute("plcApiPattern", "PLC API service regexp pattern \
53 (e.g. https://%(hostname)s:443/PLCAPI/ ) ",
54 default = "https://%(hostname)s:443/PLCAPI/",
55 flags = Flags.ExecReadOnly)
57 pl_user = Attribute("pluser", "PlanetLab account user, as the one to \
58 authenticate in the website) ",
59 flags = Flags.Credential)
61 pl_password = Attribute("plpassword",
62 "PlanetLab account password, as \
63 the one to authenticate in the website) ",
64 flags = Flags.Credential)
66 city = Attribute("city", "Constrain location (city) during resource \
67 discovery. May use wildcards.",
70 country = Attribute("country", "Constrain location (country) during \
71 resource discovery. May use wildcards.",
74 region = Attribute("region", "Constrain location (region) during \
75 resource discovery. May use wildcards.",
78 architecture = Attribute("architecture", "Constrain architecture \
79 during resource discovery.",
80 type = Types.Enumerate,
85 operating_system = Attribute("operatingSystem", "Constrain operating \
86 system during resource discovery.",
87 type = Types.Enumerate,
95 #site = Attribute("site", "Constrain the PlanetLab site this node \
97 # type = Types.Enumerate,
101 # flags = Flags.Filter)
103 min_reliability = Attribute("minReliability", "Constrain reliability \
104 while picking PlanetLab nodes. Specifies a lower \
108 flags = Flags.Filter)
110 max_reliability = Attribute("maxReliability", "Constrain reliability \
111 while picking PlanetLab nodes. Specifies an upper \
115 flags = Flags.Filter)
117 min_bandwidth = Attribute("minBandwidth", "Constrain available \
118 bandwidth while picking PlanetLab nodes. \
119 Specifies a lower acceptable bound.",
122 flags = Flags.Filter)
124 max_bandwidth = Attribute("maxBandwidth", "Constrain available \
125 bandwidth while picking PlanetLab nodes. \
126 Specifies an upper acceptable bound.",
129 flags = Flags.Filter)
131 min_load = Attribute("minLoad", "Constrain node load average while \
132 picking PlanetLab nodes. Specifies a lower acceptable \
136 flags = Flags.Filter)
138 max_load = Attribute("maxLoad", "Constrain node load average while \
139 picking PlanetLab nodes. Specifies an upper acceptable \
143 flags = Flags.Filter)
145 min_cpu = Attribute("minCpu", "Constrain available cpu time while \
146 picking PlanetLab nodes. Specifies a lower acceptable \
150 flags = Flags.Filter)
152 max_cpu = Attribute("maxCpu", "Constrain available cpu time while \
153 picking PlanetLab nodes. Specifies an upper acceptable \
157 flags = Flags.Filter)
159 timeframe = Attribute("timeframe", "Past time period in which to check\
160 information about the node. Values are year,month, \
163 type = Types.Enumerate,
168 flags = Flags.Filter)
170 cls._register_attribute(ip)
171 cls._register_attribute(pl_url)
172 cls._register_attribute(pl_ptn)
173 cls._register_attribute(pl_user)
174 cls._register_attribute(pl_password)
175 #cls._register_attribute(site)
176 cls._register_attribute(city)
177 cls._register_attribute(country)
178 cls._register_attribute(region)
179 cls._register_attribute(architecture)
180 cls._register_attribute(operating_system)
181 cls._register_attribute(min_reliability)
182 cls._register_attribute(max_reliability)
183 cls._register_attribute(min_bandwidth)
184 cls._register_attribute(max_bandwidth)
185 cls._register_attribute(min_load)
186 cls._register_attribute(max_load)
187 cls._register_attribute(min_cpu)
188 cls._register_attribute(max_cpu)
189 cls._register_attribute(timeframe)
192 def __init__(self, ec, guid):
193 super(PlanetlabNode, self).__init__(ec, guid)
196 self._node_to_provision = None
201 pl_user = self.get("pluser")
202 pl_pass = self.get("plpassword")
203 pl_url = self.get("plcApiUrl")
204 pl_ptn = self.get("plcApiPattern")
206 self._plapi = PLCAPIFactory.get_api(pl_user, pl_pass, pl_url,
213 Based on the attributes defined by the user, discover the suitable nodes
215 hostname = self._get_hostname()
216 print self.guid, hostname
218 # the user specified one particular node to be provisioned
219 # check with PLCAPI if it is alvive
220 node_id = self._query_if_alive(hostname=hostname)
221 node_id = node_id.pop()
222 print self.guid, node_id
224 # check that the node is not blacklisted or being provisioned
226 with PlanetlabNode.lock:
227 plist = self.plapi.reserved()
228 blist = self.plapi.blacklisted()
229 print self.guid,plist
230 print self.guid,blist
231 if node_id not in blist and node_id not in plist:
233 # check that is really alive, by performing ping
234 ping_ok = self._do_ping(node_id)
236 self._blacklist_node(node_id)
237 self.fail_node_not_alive(hostname)
239 self._put_node_in_provision(node_id)
240 self._node_to_provision = node_id
241 super(PlanetlabNode, self).discover()
244 self.fail_node_not_available(hostname)
247 # the user specifies constraints based on attributes, zero, one or
248 # more nodes can match these constraints
249 nodes = self._filter_based_on_attributes()
250 nodes_alive = self._query_if_alive(nodes)
252 # nodes that are already part of user's slice have the priority to
254 nodes_inslice = self._check_if_in_slice(nodes_alive)
255 nodes_not_inslice = list(set(nodes_alive) - set(nodes_inslice))
259 node_id = self._choose_random_node(nodes_inslice)
262 # Either there were no matching nodes in the user's slice, or
263 # the nodes in the slice were blacklisted or being provisioned
264 # by other RM. Note nodes_not_inslice is never empty
265 node_id = self._choose_random_node(nodes_not_inslice)
268 self._node_to_provision = node_id
269 super(PlanetlabNode, self).discover()
271 self.fail_not_enough_nodes()
275 Add node to user's slice after verifing that the node is functioning
283 while not provision_ok:
284 node = self._node_to_provision
285 # Adding try catch to set hostname because sometimes MyPLC fails
286 # when trying to retrive node's hostname
288 self._set_hostname_attr(node)
290 with PlanetlabNode.lock:
291 self._blacklist_node(node)
295 self._add_node_to_slice(node)
297 # check ssh connection
299 while t < timeout and not ssh_ok:
301 cmd = 'echo \'GOOD NODE\''
302 ((out, err), proc) = self.execute(cmd)
303 if out.find("GOOD NODE") < 0:
312 # the timeout was reach without establishing ssh connection
313 # the node is blacklisted, deleted from the slice, and a new
314 # node to provision is discovered
315 with PlanetlabNode.lock:
316 self._blacklist_node(node)
317 self._delete_node_from_slice(node)
318 self.set('hostname', None)
322 # check /proc directory is mounted (ssh_ok = True)
324 cmd = 'mount |grep proc'
325 ((out, err), proc) = self.execute(cmd)
326 if out.find("/proc type proc") < 0:
327 with PlanetlabNode.lock:
328 self._blacklist_node(node)
329 self._delete_node_from_slice(node)
330 self.set('hostname', None)
337 ip = self._get_ip(node)
340 super(PlanetlabNode, self).provision()
342 def _filter_based_on_attributes(self):
344 Retrive the list of nodes ids that match user's constraints
346 # Map user's defined attributes with tagnames of PlanetLab
347 timeframe = self.get("timeframe")[0]
350 'country' : 'country',
352 'architecture' : 'arch',
353 'operatingSystem' : 'fcdistro',
354 #'site' : 'pldistro',
355 'minReliability' : 'reliability%s' % timeframe,
356 'maxReliability' : 'reliability%s' % timeframe,
357 'minBandwidth' : 'bw%s' % timeframe,
358 'maxBandwidth' : 'bw%s' % timeframe,
359 'minLoad' : 'load%s' % timeframe,
360 'maxLoad' : 'load%s' % timeframe,
361 'minCpu' : 'cpu%s' % timeframe,
362 'maxCpu' : 'cpu%s' % timeframe,
368 for attr_name, attr_obj in self._attrs.iteritems():
369 attr_value = self.get(attr_name)
371 if attr_value is not None and attr_obj.flags == 8 and \
372 attr_name != 'timeframe':
374 attr_tag = attr_to_tags[attr_name]
375 filters['tagname'] = attr_tag
377 # filter nodes by fixed constraints e.g. operating system
378 if not 'min' in attr_name and not 'max' in attr_name:
379 filters['value'] = attr_value
380 nodes_id = self._filter_by_fixed_attr(filters, nodes_id)
382 # filter nodes by range constraints e.g. max bandwidth
383 elif ('min' or 'max') in attr_name:
384 nodes_id = self._filter_by_range_attr(attr_name, attr_value, filters, nodes_id)
387 nodes = self.plapi.get_nodes()
389 nodes_id.append(node['node_id'])
394 def _filter_by_fixed_attr(self, filters, nodes_id):
396 Query PLCAPI for nodes ids matching fixed attributes defined by the
399 node_tags = self.plapi.get_node_tags(filters)
400 if node_tags is not None:
402 if len(nodes_id) == 0:
403 # first attribute being matched
404 for node_tag in node_tags:
405 nodes_id.append(node_tag['node_id'])
407 # remove the nodes ids that don't match the new attribute
408 # that is being match
411 for node_tag in node_tags:
412 if node_tag['node_id'] in nodes_id:
413 nodes_id_tmp.append(node_tag['node_id'])
415 if len(nodes_id_tmp):
416 nodes_id = set(nodes_id) & set(nodes_id_tmp)
418 # no node from before match the new constraint
419 self.fail_discovery()
421 # no nodes match the filter applied
422 self.fail_discovery()
426 def _filter_by_range_attr(self, attr_name, attr_value, filters, nodes_id):
428 Query PLCAPI for nodes ids matching attributes defined in a certain
431 node_tags = self.plapi.get_node_tags(filters)
432 if node_tags is not None:
434 if len(nodes_id) == 0:
435 # first attribute being matched
436 for node_tag in node_tags:
438 # check that matches the min or max restriction
439 if 'min' in attr_name and node_tag['value'] != 'n/a' and \
440 float(node_tag['value']) > attr_value:
441 nodes_id.append(node_tag['node_id'])
443 elif 'max' in attr_name and node_tag['value'] != 'n/a' and \
444 float(node_tag['value']) < attr_value:
445 nodes_id.append(node_tag['node_id'])
448 # remove the nodes ids that don't match the new attribute
449 # that is being match
451 for node_tag in node_tags:
453 # check that matches the min or max restriction and was a
454 # matching previous filters
455 if 'min' in attr_name and node_tag['value'] != 'n/a' and \
456 float(node_tag['value']) > attr_value and \
457 node_tag['node_id'] in nodes_id:
458 nodes_id_tmp.append(node_tag['node_id'])
460 elif 'max' in attr_name and node_tag['value'] != 'n/a' and \
461 float(node_tag['value']) < attr_value and \
462 node_tag['node_id'] in nodes_id:
463 nodes_id_tmp.append(node_tag['node_id'])
465 if len(nodes_id_tmp):
466 nodes_id = set(nodes_id) & set(nodes_id_tmp)
468 # no node from before match the new constraint
469 self.fail_discovery()
472 # no nodes match the filter applied
473 self.fail_discovery()
477 def _query_if_alive(self, nodes_id=None, hostname=None):
479 Query PLCAPI for nodes that register activity recently, using filters
480 related to the state of the node, e.g. last time it was contacted
482 if nodes_id is None and hostname is None:
483 msg = "Specify nodes_id or hostname"
484 raise RuntimeError, msg
486 if nodes_id is not None and hostname is not None:
487 msg = "Specify either nodes_id or hostname"
488 raise RuntimeError, msg
490 # define PL filters to check the node is alive
492 filters['run_level'] = 'boot'
493 filters['boot_state'] = 'boot'
494 filters['node_type'] = 'regular'
495 #filters['>last_contact'] = int(time.time()) - 2*3600
497 # adding node_id or hostname to the filters to check for the particular
500 filters['node_id'] = list(nodes_id)
501 alive_nodes_id = self._get_nodes_id(filters)
503 filters['hostname'] = hostname
504 alive_nodes_id = self._get_nodes_id(filters)
506 if len(alive_nodes_id) == 0:
507 self.fail_node_not_alive(self, hostname)
510 for node_id in alive_nodes_id:
511 nid = node_id['node_id']
516 def _choose_random_node(self, nodes):
518 From the possible nodes for provision, choose randomly to decrese the
519 probability of different RMs choosing the same node for provision
524 index = randint(0, size)
525 node_id = nodes[index]
526 nodes[index] = nodes[size]
528 # check the node is not blacklisted or being provision by other RM
529 # and perform ping to check that is really alive
530 with PlanetlabNode.lock:
532 blist = self.plapi.blacklisted()
533 plist = self.plapi.reserved()
534 if node_id not in blist and node_id not in plist:
535 ping_ok = self._do_ping(node_id)
536 print " ### ping_ok #### %s guid %s" % (ping_ok, self.guid)
538 self._blacklist_node(node_id)
540 # discovered node for provision, added to provision list
541 self._put_node_in_provision(node_id)
542 print "node_id %s , guid %s" % (node_id, self.guid)
545 def _get_nodes_id(self, filters):
546 return self.plapi.get_nodes(filters, fields=['node_id'])
548 def _add_node_to_slice(self, node_id):
549 self.info(" Selected node to provision ")
550 slicename = self.get("username")
551 with PlanetlabNode.lock:
552 slice_nodes = self.plapi.get_slice_nodes(slicename)
553 slice_nodes.append(node_id)
554 self.plapi.add_slice_nodes(slicename, slice_nodes)
556 def _delete_node_from_slice(self, node):
557 self.warn(" Deleting node from slice ")
558 slicename = self.get("username")
559 self.plapi.delete_slice_node(slicename, [node])
561 def _get_hostname(self):
562 hostname = self.get("hostname")
567 hostname = sshfuncs.gethostbyname(ip)
572 def _set_hostname_attr(self, node):
574 Query PLCAPI for the hostname of a certain node id and sets the
575 attribute hostname, it will over write the previous value
577 hostname = self.plapi.get_nodes(node, ['hostname'])
578 self.set("hostname", hostname[0]['hostname'])
580 def _check_if_in_slice(self, nodes_id):
582 Query PLCAPI to find out if any node id from nodes_id is in the user's
585 slicename = self.get("username")
586 slice_nodes = self.plapi.get_slice_nodes(slicename)
587 nodes_inslice = list(set(nodes_id) & set(slice_nodes))
590 def _do_ping(self, node_id):
592 Perform ping command on node's IP matching node id
595 ip = self._get_ip(node_id)
596 print "ip de do_ping %s, guid %s" % (ip, self.guid)
597 if not ip: return ping_ok
599 command = "ping -c2 %s" % ip
601 (out, err) = lexec(command)
602 print "out de do_ping %s, guid %s" % (out, self.guid)
603 if not out.find("2 received") < 0:
606 print "ping_ok de do_ping %s, guid %s" % (ping_ok, self.guid)
609 def _blacklist_node(self, node):
611 Add node mal functioning node to blacklist
613 self.warn(" Blacklisting malfunctioning node ")
614 self._plapi.blacklist_host(node)
616 def _put_node_in_provision(self, node):
618 Add node to the list of nodes being provisioned, in order for other RMs
619 to not try to provision the same one again
621 self._plapi.reserve_host(node)
623 def _get_ip(self, node_id):
625 Query PLCAPI for the IP of a node with certain node id
627 hostname = self.plapi.get_nodes(node_id, ['hostname'])[0]
628 print "#### HOSTNAME ##### %s ### guid %s " % (hostname['hostname'], self.guid)
629 ip = sshfuncs.gethostbyname(hostname['hostname'])
631 # Fail while trying to find the IP
635 def fail_discovery(self):
637 msg = "Discovery failed. No candidates found for node"
639 raise RuntimeError, msg
641 def fail_node_not_alive(self, hostname=None):
643 msg = "Node %s not alive" % hostname
644 raise RuntimeError, msg
646 def fail_node_not_available(self, hostname):
648 msg = "Node %s not available for provisioning" % hostname
649 raise RuntimeError, msg
651 def fail_not_enough_nodes(self):
653 msg = "Not enough nodes available for provisioning"
654 raise RuntimeError, msg
656 def valid_connection(self, guid):