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