Fixing discover and provision method
[nepi.git] / src / nepi / resources / planetlab / node.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
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.
9 #
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.
14 #
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/>.
17 #
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19 #         Lucia Guevgeozian <lucia.guevgeozian_odizzio@inria.fr>
20
21 from nepi.execution.attribute import Attribute, Flags, Types
22 from nepi.execution.resource import ResourceManager, clsinit_copy, ResourceState, \
23         reschedule_delay
24 from nepi.resources.linux.node import LinuxNode
25 from nepi.resources.planetlab.plcapi import PLCAPIFactory 
26 from nepi.util.execfuncs import lexec
27
28 from random import randint
29 import time
30 import threading
31
32 @clsinit_copy
33 class PlanetlabNode(LinuxNode):
34     _rtype = "PlanetlabNode"
35
36     blacklist = list()
37     provisionlist = list()
38
39     lock_blist = threading.Lock()
40     lock_plist = threading.Lock()
41
42     lock_slice = threading.Lock()
43
44
45     @classmethod
46     def _register_attributes(cls):
47         ip = Attribute("ip", "PlanetLab host public IP address",
48                 flags = Flags.ReadOnly)
49
50         pl_url = Attribute("plcApiUrl", "URL of PlanetLab PLCAPI host \
51                     (e.g. www.planet-lab.eu or www.planet-lab.org) ",
52                     default = "www.planet-lab.eu",
53                     flags = Flags.Credential)
54
55         pl_ptn = Attribute("plcApiPattern", "PLC API service regexp pattern \
56                     (e.g. https://%(hostname)s:443/PLCAPI/ ) ",
57                     default = "https://%(hostname)s:443/PLCAPI/",
58                     flags = Flags.ExecReadOnly)
59     
60         pl_user = Attribute("pluser", "PlanetLab account user, as the one to \
61                     authenticate in the website) ",
62                     flags = Flags.Credential)
63
64         pl_password = Attribute("password", 
65                         "PlanetLab account password, as \
66                         the one to authenticate in the website) ",
67                         flags = Flags.Credential)
68
69         city = Attribute("city", "Constrain location (city) during resource \
70                 discovery. May use wildcards.",
71                 flags = Flags.Filter)
72
73         country = Attribute("country", "Constrain location (country) during \
74                     resource discovery. May use wildcards.",
75                     flags = Flags.Filter)
76
77         region = Attribute("region", "Constrain location (region) during \
78                     resource discovery. May use wildcards.",
79                     flags = Flags.Filter)
80
81         architecture = Attribute("architecture", "Constrain architecture \
82                         during resource discovery.",
83                         type = Types.Enumerate,
84                         allowed = ["x86_64", 
85                                     "i386"],
86                         flags = Flags.Filter)
87
88         operating_system = Attribute("operatingSystem", "Constrain operating \
89                             system during resource discovery.",
90                             type = Types.Enumerate,
91                             allowed =  ["f8",
92                                         "f12",
93                                         "f14",
94                                         "centos",
95                                         "other"],
96                             flags = Flags.Filter)
97
98         site = Attribute("site", "Constrain the PlanetLab site this node \
99                 should reside on.",
100                 type = Types.Enumerate,
101                 allowed = ["PLE",
102                             "PLC",
103                             "PLJ"],
104                 flags = Flags.Filter)
105
106         min_reliability = Attribute("minReliability", "Constrain reliability \
107                             while picking PlanetLab nodes. Specifies a lower \
108                             acceptable bound.",
109                             type = Types.Double,
110                             range = (1, 100),
111                             flags = Flags.Filter)
112
113         max_reliability = Attribute("maxReliability", "Constrain reliability \
114                             while picking PlanetLab nodes. Specifies an upper \
115                             acceptable bound.",
116                             type = Types.Double,
117                             range = (1, 100),
118                             flags = Flags.Filter)
119
120         min_bandwidth = Attribute("minBandwidth", "Constrain available \
121                             bandwidth while picking PlanetLab nodes. \
122                             Specifies a lower acceptable bound.",
123                             type = Types.Double,
124                             range = (0, 2**31),
125                             flags = Flags.Filter)
126
127         max_bandwidth = Attribute("maxBandwidth", "Constrain available \
128                             bandwidth while picking PlanetLab nodes. \
129                             Specifies an upper acceptable bound.",
130                             type = Types.Double,
131                             range = (0, 2**31),
132                             flags = Flags.Filter)
133
134         min_load = Attribute("minLoad", "Constrain node load average while \
135                     picking PlanetLab nodes. Specifies a lower acceptable \
136                     bound.",
137                     type = Types.Double,
138                     range = (0, 2**31),
139                     flags = Flags.Filter)
140
141         max_load = Attribute("maxLoad", "Constrain node load average while \
142                     picking PlanetLab nodes. Specifies an upper acceptable \
143                     bound.",
144                     type = Types.Double,
145                     range = (0, 2**31),
146                     flags = Flags.Filter)
147
148         min_cpu = Attribute("minCpu", "Constrain available cpu time while \
149                     picking PlanetLab nodes. Specifies a lower acceptable \
150                     bound.",
151                     type = Types.Double,
152                     range = (0, 100),
153                     flags = Flags.Filter)
154
155         max_cpu = Attribute("maxCpu", "Constrain available cpu time while \
156                     picking PlanetLab nodes. Specifies an upper acceptable \
157                     bound.",
158                     type = Types.Double,
159                     range = (0, 100),
160                     flags = Flags.Filter)
161
162         timeframe = Attribute("timeframe", "Past time period in which to check\
163                         information about the node. Values are year,month, \
164                         week, latest",
165                         default = "week",
166                         type = Types.Enumerate,
167                         allowed = ["latest",
168                                     "week",
169                                     "month",
170                                     "year"],
171                         flags = Flags.Filter)
172
173         cls._register_attribute(ip)
174         cls._register_attribute(pl_url)
175         cls._register_attribute(pl_ptn)
176         cls._register_attribute(pl_user)
177         cls._register_attribute(pl_password)
178         cls._register_attribute(site)
179         cls._register_attribute(city)
180         cls._register_attribute(country)
181         cls._register_attribute(region)
182         cls._register_attribute(architecture)
183         cls._register_attribute(operating_system)
184         cls._register_attribute(min_reliability)
185         cls._register_attribute(max_reliability)
186         cls._register_attribute(min_bandwidth)
187         cls._register_attribute(max_bandwidth)
188         cls._register_attribute(min_load)
189         cls._register_attribute(max_load)
190         cls._register_attribute(min_cpu)
191         cls._register_attribute(max_cpu)
192         cls._register_attribute(timeframe)
193         
194
195     def __init__(self, ec, guid):
196         super(PlanetlabNode, self).__init__(ec, guid)
197
198         self._plapi = None
199         self._node_to_provision = None
200     
201     @property
202     def plapi(self):
203         if not self._plapi:
204             pl_user = self.get("pluser")
205             pl_pass = self.get("password")
206             pl_url = self.get("plcApiUrl")
207             pl_ptn = self.get("plcApiPattern")
208
209             self._plapi =  PLCAPIFactory.get_api(pl_user, pl_pass, pl_url,
210                     pl_ptn)
211             
212         return self._plapi
213
214     def discoverl(self):
215         """
216         Based on the attributes defined by the user, discover the suitable nodes
217         """
218         hostname = self.get("hostname")
219         if hostname:
220             # the user specified one particular node to be provisioned
221             # check with PLCAPI if it is alvive
222             node_id = self._query_if_alive(hostname=hostname)
223             node_id = node_id.pop()
224
225             # check that the node is not blacklisted or already being provision 
226             # by other RM
227             blist = PlanetlabNode.blacklist
228             plist = PlanetlabNode.provisionlist
229             if node_id not in blist and node_id not in plist:
230                 
231                 # check that is really alive, by performing ping
232                 ping_ok = self._do_ping(node_id)
233                 if not ping_ok:
234                     self._blacklist_node(node_id)
235                     self.fail_node_not_alive(hostname)
236                 else:
237                     self._node_to_provision = node_id
238                     self._put_node_in_provision(node_id)
239                     super(PlanetlabNode, self).discover()
240                 
241             else:
242                 self.fail_node_not_available(hostname)                
243         
244         else:
245             # the user specifies constraints based on attributes, zero, one or 
246             # more nodes can match these constraints 
247             nodes = self._filter_based_on_attributes()
248             nodes_alive = self._query_if_alive(nodes)
249     
250             # nodes that are already part of user's slice have the priority to
251             # provisioned
252             nodes_inslice = self._check_if_in_slice(nodes_alive)
253             nodes_not_inslice = list(set(nodes_alive) - set(nodes_inslice))
254             
255             node_id = None
256             if nodes_inslice:
257                 node_id = self._choose_random_node(nodes_inslice)
258                 
259             if not node_id and nodes_not_inslice:
260                 # Either there were no matching nodes in the user's slice, or
261                 # the nodes in the slice  were blacklisted or being provisioned
262                 # by other RM. Note nodes_not_inslice is never empty
263                 node_id = self._choose_random_node(nodes_not_inslice)
264             if not node_id:
265                 self.fail_not_enough_nodes()
266
267             self._node_to_provision = node_id
268             super(PlanetlabNode, self).discover()
269             
270     def provisionl(self):
271         """
272         Add node to user's slice after verifing that the node is functioning
273         correctly
274         """
275         provision_ok = False
276         ssh_ok = False
277         proc_ok = False
278         timeout = 1200
279
280         while not provision_ok:
281             node = self._node_to_provision
282             self._set_hostname_attr(node)
283             self._add_node_to_slice(node)
284             
285             # check ssh connection
286             t = 0 
287             while t < timeout and not ssh_ok:
288
289                 cmd = 'echo \'GOOD NODE\''
290                 ((out, err), proc) = self.execute(cmd)
291                 if out.find("GOOD NODE") < 0:
292                     t = t + 60
293                     time.sleep(60)
294                     continue
295                 else:
296                     ssh_ok = True
297                     continue
298
299             if not ssh_ok:
300                 # the timeout was reach without establishing ssh connection
301                 # the node is blacklisted, deleted from the slice, and a new
302                 # node to provision is discovered
303                 self._blacklist_node(node)
304                 self._delete_node_from_slice(node)
305                 self.discover()
306                 continue
307             
308             # check /proc directory is mounted (ssh_ok = True)
309             else:
310                 cmd = 'mount |grep proc'
311                 ((out, err), proc) = self.execute(cmd)
312                 if out.find("/proc type proc") < 0:
313                     self._blacklist_node(node)
314                     self._delete_node_from_slice(node)
315                     self.discover()
316                     continue
317             
318                 else:
319                     provision_ok = True
320                     # set IP attribute
321                     ip = self._get_ip(node)
322                     self.set("ip", ip)
323             
324         super(PlanetlabNode, self).provision()
325
326     def _filter_based_on_attributes(self):
327         """
328         Retrive the list of nodes ids that match user's constraints 
329         """
330         # Map user's defined attributes with tagnames of PlanetLab
331         timeframe = self.get("timeframe")[0]
332         attr_to_tags = {
333             'city' : 'city',
334             'country' : 'country',
335             'region' : 'region',
336             'architecture' : 'arch',
337             'operatingSystem' : 'fcdistro',
338             #'site' : 'pldistro',
339             'minReliability' : 'reliability%s' % timeframe,
340             'maxReliability' : 'reliability%s' % timeframe,
341             'minBandwidth' : 'bw%s' % timeframe,
342             'maxBandwidth' : 'bw%s' % timeframe,
343             'minLoad' : 'load%s' % timeframe,
344             'maxLoad' : 'load%s' % timeframe,
345             'minCpu' : 'cpu%s' % timeframe,
346             'maxCpu' : 'cpu%s' % timeframe,
347         }
348         
349         nodes_id = []
350         filters = {}
351
352         for attr_name, attr_obj in self._attrs.iteritems():
353             attr_value = self.get(attr_name)
354             
355             if attr_value is not None and attr_obj.flags == 8 and \
356                 attr_name != 'timeframe':
357         
358                 attr_tag = attr_to_tags[attr_name]
359                 filters['tagname'] = attr_tag
360
361                 # filter nodes by fixed constraints e.g. operating system
362                 if not 'min' in attr_name and not 'max' in attr_name:
363                     filters['value'] = attr_value
364                     nodes_id = self._filter_by_fixed_attr(filters, nodes_id)
365
366                 # filter nodes by range constraints e.g. max bandwidth
367                 elif ('min' or 'max') in attr_name:
368                     nodes_id = self._filter_by_range_attr(attr_name, attr_value, filters, nodes_id)
369                 
370         return nodes_id
371                     
372
373     def _filter_by_fixed_attr(self, filters, nodes_id):
374         """
375         Query PLCAPI for nodes ids matching fixed attributes defined by the
376         user
377         """
378         node_tags = self.plapi.get_node_tags(filters)
379         if node_tags is not None:
380
381             if len(nodes_id) == 0:
382                 # first attribute being matched
383                 for node_tag in node_tags:
384                     nodes_id.append(node_tag['node_id'])
385             else:
386                 # remove the nodes ids that don't match the new attribute
387                 # that is being match
388
389                 nodes_id_tmp = []
390                 for node_tag in node_tags:
391                     if node_tag['node_id'] in nodes_id:
392                         nodes_id_tmp.append(node_tag['node_id'])
393
394                 if len(nodes_id_tmp):
395                     nodes_id = set(nodes_id) & set(nodes_id_tmp)
396                 else:
397                     # no node from before match the new constraint
398                     self.fail_discovery()
399         else:
400             # no nodes match the filter applied
401             self.fail_discovery()
402
403         return nodes_id
404
405     def _filter_by_range_attr(self, attr_name, attr_value, filters, nodes_id):
406         """
407         Query PLCAPI for nodes ids matching attributes defined in a certain
408         range, by the user
409         """
410         node_tags = self.plapi.get_node_tags(filters)
411         if node_tags is not None:
412             
413             if len(nodes_id) == 0:
414                 # first attribute being matched
415                 for node_tag in node_tags:
416  
417                    # check that matches the min or max restriction
418                     if 'min' in attr_name and node_tag['value'] != 'n/a' and \
419                         float(node_tag['value']) > attr_value:
420                         nodes_id.append(node_tag['node_id'])
421
422                     elif 'max' in attr_name and node_tag['value'] != 'n/a' and \
423                         float(node_tag['value']) < attr_value:
424                         nodes_id.append(node_tag['node_id'])
425             else:
426
427                 # remove the nodes ids that don't match the new attribute
428                 # that is being match
429                 nodes_id_tmp = []
430                 for node_tag in node_tags:
431
432                     # check that matches the min or max restriction and was a
433                     # matching previous filters
434                     if 'min' in attr_name and node_tag['value'] != 'n/a' and \
435                         float(node_tag['value']) > attr_value and \
436                         node_tag['node_id'] in nodes_id:
437                         nodes_id_tmp.append(node_tag['node_id'])
438
439                     elif 'max' in attr_name and node_tag['value'] != 'n/a' and \
440                         float(node_tag['value']) < attr_value and \
441                         node_tag['node_id'] in nodes_id:
442                         nodes_id_tmp.append(node_tag['node_id'])
443
444                 if len(nodes_id_tmp):
445                     nodes_id = set(nodes_id) & set(nodes_id_tmp)
446                 else:
447                     # no node from before match the new constraint
448                     self.fail_discovery()
449
450         else: #TODO CHECK
451             # no nodes match the filter applied
452             self.fail_discovery()
453
454         return nodes_id
455         
456     def _query_if_alive(self, nodes_id=None, hostname=None):
457         """
458         Query PLCAPI for nodes that register activity recently, using filters 
459         related to the state of the node, e.g. last time it was contacted
460         """
461         if nodes_id is None and hostname is None:
462             msg = "Specify nodes_id or hostname"
463             raise RuntimeError, msg
464
465         if nodes_id is not None and hostname is not None:
466             msg = "Specify either nodes_id or hostname"
467             raise RuntimeError, msg
468
469         # define PL filters to check the node is alive
470         filters = dict()
471         filters['run_level'] = 'boot'
472         filters['boot_state'] = 'boot'
473         filters['node_type'] = 'regular' 
474         filters['>last_contact'] =  int(time.time()) - 2*3600
475
476         # adding node_id or hostname to the filters to check for the particular
477         # node
478         if nodes_id:
479             filters['node_id'] = list(nodes_id)
480             alive_nodes_id = self._get_nodes_id(filters)
481         elif hostname:
482             filters['hostname'] = hostname
483             alive_nodes_id = self._get_nodes_id(filters)
484
485         if len(alive_nodes_id) == 0:
486             self.fail_discovery()
487         else:
488             nodes_id = list()
489             for node_id in alive_nodes_id:
490                 nid = node_id['node_id']
491                 nodes_id.append(nid)
492
493             return nodes_id
494
495     def _choose_random_node(self, nodes):
496         """
497         From the possible nodes for provision, choose randomly to decrese the
498         probability of different RMs choosing the same node for provision
499         """
500         blist = PlanetlabNode.blacklist
501         plist = PlanetlabNode.provisionlist
502
503         size = len(nodes)
504         while size:
505             size = size - 1
506             index = randint(0, size)
507             node_id = nodes[index]
508             nodes[index] = nodes[size]
509
510             # check the node is not blacklisted or being provision by other RM
511             # and perform ping to check that is really alive
512             if node_id not in blist and node_id not in plist:
513                 ping_ok = self._do_ping(node_id)
514                 if not ping_ok:
515                     self._blacklist_node(node_id)
516                 else:
517                     # discovered node for provision, added to provision list
518                     self._put_node_in_provision(node_id)
519                     return node_id
520
521     def _get_nodes_id(self, filters):
522         return self.plapi.get_nodes(filters, fields=['node_id'])
523
524     def _add_node_to_slice(self, node_id):
525         self.warn(" Adding node to slice ")
526         slicename = self.get("username")
527         with PlanetlabNode.lock_slice:
528             slice_nodes = self.plapi.get_slice_nodes(slicename)
529             slice_nodes.append(node_id)
530             self.plapi.add_slice_nodes(slicename, slice_nodes)
531
532     def _delete_node_from_slice(self, node):
533         self.warn(" Deleting node from slice ")
534         slicename = self.get("username")
535         self.plapi.delete_slice_node(slicename, [node])
536
537     def _set_hostname_attr(self, node):
538         """
539         Query PLCAPI for the hostname of a certain node id and sets the
540         attribute hostname, it will over write the previous value
541         """
542         hostname = self.plapi.get_nodes(node, ['hostname'])
543         self.set("hostname", hostname[0]['hostname'])
544
545     def _check_if_in_slice(self, nodes_id):
546         """
547         Query PLCAPI to find out if any node id from nodes_id is in the user's
548         slice
549         """
550         slicename = self.get("username")
551         slice_nodes = self.plapi.get_slice_nodes(slicename)
552         nodes_inslice = list(set(nodes_id) & set(slice_nodes))
553
554         return nodes_inslice
555
556     def _do_ping(self, node_id):
557         """
558         Perform ping command on node's IP matching node id
559         """
560         ping_ok = False
561         ip = self._get_ip(node_id)
562         command = "ping -c2 %s | echo \"PING OK\"" % ip
563
564         (out, err) = lexec(command)
565         if not out.find("PING OK") < 0:
566             ping_ok = True
567
568         return ping_ok 
569
570     def _blacklist_node(self, node):
571         """
572         Add node mal functioning node to blacklist
573         """
574         blist = PlanetlabNode.blacklist
575
576         self.warn(" Blacklisting malfunctioning node ")
577         with PlanetlabNode.lock_blist:
578             blist.append(node)
579
580     def _put_node_in_provision(self, node):
581         """
582         Add node to the list of nodes being provisioned, in order for other RMs
583         to not try to provision the same one again
584         """
585         plist = PlanetlabNode.provisionlist
586
587         self.warn(" Provisioning node ")
588         with PlanetlabNode.lock_plist:
589             plist.append(node)
590
591     def _get_ip(self, node_id):
592         """
593         Query PLCAPI for the IP of a node with certain node id
594         """
595         ip = self.plapi.get_interfaces({'node_id':node_id}, fields=['ip'])
596         ip = ip[0]['ip']
597         return ip
598
599     def fail_discovery(self):
600         self.fail()
601         msg = "Discovery failed. No candidates found for node"
602         self.error(msg)
603         raise RuntimeError, msg
604
605     def fail_node_not_alive(self, hostname):
606         msg = "Node %s not alive, pick another node" % hostname
607         raise RuntimeError, msg
608     
609     def fail_node_not_available(self, hostname):
610         msg = "Node %s not available for provisioning, pick another \
611                 node" % hostname
612         raise RuntimeError, msg
613
614     def fail_not_enough_nodes(self):
615         msg = "Not enough nodes available for provisioning"
616         raise RuntimeError, msg
617
618     def valid_connection(self, guid):
619         # TODO: Validate!
620         return True
621
622