iotlab driver: wip fixing Timur's bug when a user already has an account in LDAP...
[sfa.git] / sfa / iotlab / iotlabslices.py
1 """
2 This file defines the IotlabSlices class by which all the slice checkings
3 upon lease creation are done.
4 """
5 from sfa.util.xrn import get_authority, urn_to_hrn, hrn_to_urn
6 from sfa.util.sfalogging import logger
7
8 MAXINT = 2L**31-1
9
10
11 class IotlabSlices:
12     """
13     This class is responsible for checking the slice when creating a
14     lease or a sliver. Those checks include verifying that the user is valid,
15     that the slice is known from the testbed or from our peers, that the list
16     of nodes involved has not changed (in this case the lease is modified
17     accordingly).
18     """
19     rspec_to_slice_tag = {'max_rate': 'net_max_rate'}
20
21     def __init__(self, driver):
22         """
23         Get the reference to the driver here.
24         """
25         self.driver = driver
26
27     def get_peer(self, xrn):
28         """
29         Finds the authority of a resource based on its xrn.
30         If the authority is Iotlab (local) return None,
31         Otherwise, look up in the DB if Iotlab is federated with this site
32         authority and returns its DB record if it is the case.
33
34         :param xrn: resource's xrn
35         :type xrn: string
36         :returns: peer record
37         :rtype: dict
38
39         """
40         hrn, hrn_type = urn_to_hrn(xrn)
41         #Does this slice belong to a local site or a peer iotlab site?
42         peer = None
43
44         # get this slice's authority (site)
45         slice_authority = get_authority(hrn)
46         #Iotlab stuff
47         #This slice belongs to the current site
48         if slice_authority == self.driver.testbed_shell.root_auth:
49             site_authority = slice_authority
50             return None
51
52         site_authority = get_authority(slice_authority).lower()
53         # get this site's authority (sfa root authority or sub authority)
54
55         logger.debug("IOTLABSLICES \t get_peer slice_authority  %s \
56                     site_authority %s hrn %s"
57                      % (slice_authority, site_authority, hrn))
58
59         # check if we are already peered with this site_authority
60         #if so find the peer record
61         peers = self.driver.GetPeers(peer_filter=site_authority)
62         for peer_record in peers:
63             if site_authority == peer_record.hrn:
64                 peer = peer_record
65         logger.debug(" IOTLABSLICES \tget_peer peer  %s " % (peer))
66         return peer
67
68     def get_sfa_peer(self, xrn):
69         """Returns the authority name for the xrn or None if the local site
70         is the authority.
71
72         :param xrn: the xrn of the resource we are looking the authority for.
73         :type xrn: string
74         :returns: the resources's authority name.
75         :rtype: string
76
77         """
78         hrn, hrn_type = urn_to_hrn(xrn)
79
80         # return the authority for this hrn or None if we are the authority
81         sfa_peer = None
82         slice_authority = get_authority(hrn)
83         site_authority = get_authority(slice_authority)
84
85         if site_authority != self.driver.hrn:
86             sfa_peer = site_authority
87
88         return sfa_peer
89
90     def verify_slice_leases(self, sfa_slice, requested_jobs_dict, peer):
91         """
92         Compare requested leases with the leases already scheduled/
93         running in OAR. If necessary, delete and recreate modified leases,
94         and delete no longer requested ones.
95
96         :param sfa_slice: sfa slice record
97         :param requested_jobs_dict: dictionary of requested leases
98         :param peer: sfa peer record
99
100         :type sfa_slice: dict
101         :type requested_jobs_dict: dict
102         :type peer: dict
103         :returns: leases list of dictionary
104         :rtype: list
105
106         """
107
108         logger.debug("IOTLABSLICES verify_slice_leases sfa_slice %s "
109                      % (sfa_slice))
110         #First get the list of current leases from OAR
111         leases = self.driver.GetLeases({'slice_hrn': sfa_slice['hrn']})
112         logger.debug("IOTLABSLICES verify_slice_leases requested_jobs_dict %s \
113                         leases %s " % (requested_jobs_dict, leases))
114
115         current_nodes_reserved_by_start_time = {}
116         requested_nodes_by_start_time = {}
117         leases_by_start_time = {}
118         reschedule_jobs_dict = {}
119
120         #Create reduced dictionary with key start_time and value
121         # the list of nodes
122         #-for the leases already registered by OAR first
123         # then for the new leases requested by the user
124
125         #Leases already scheduled/running in OAR
126         for lease in leases:
127             current_nodes_reserved_by_start_time[lease['t_from']] = \
128                     lease['reserved_nodes']
129             leases_by_start_time[lease['t_from']] = lease
130
131         #First remove job whose duration is too short
132         for job in requested_jobs_dict.values():
133             job['duration'] = \
134                 str(int(job['duration']) \
135                 * self.driver.testbed_shell.GetLeaseGranularity())
136             if job['duration'] < \
137                     self.driver.testbed_shell.GetLeaseGranularity():
138                 del requested_jobs_dict[job['start_time']]
139
140         #Requested jobs
141         for start_time in requested_jobs_dict:
142             requested_nodes_by_start_time[int(start_time)] = \
143                 requested_jobs_dict[start_time]['hostname']
144         #Check if there is any difference between the leases already
145         #registered in OAR and the requested jobs.
146         #Difference could be:
147         #-Lease deleted in the requested jobs
148         #-Added/removed nodes
149         #-Newly added lease
150
151         logger.debug("IOTLABSLICES verify_slice_leases \
152                         requested_nodes_by_start_time %s \
153                         "% (requested_nodes_by_start_time))
154         #Find all deleted leases
155         start_time_list = \
156             list(set(leases_by_start_time.keys()).\
157             difference(requested_nodes_by_start_time.keys()))
158         deleted_leases = [leases_by_start_time[start_time]['lease_id'] \
159                             for start_time in start_time_list]
160
161
162         #Find added or removed nodes in exisiting leases
163         for start_time in requested_nodes_by_start_time:
164             logger.debug("IOTLABSLICES verify_slice_leases  start_time %s \
165                          "%( start_time))
166             if start_time in current_nodes_reserved_by_start_time:
167
168                 # JORDAN : if we request the same nodes: do nothing
169                 if requested_nodes_by_start_time[start_time] == \
170                     current_nodes_reserved_by_start_time[start_time]:
171                     continue
172
173                 else:
174                     update_node_set = \
175                             set(requested_nodes_by_start_time[start_time])
176                     added_nodes = \
177                         update_node_set.difference(\
178                         current_nodes_reserved_by_start_time[start_time])
179                     shared_nodes = \
180                         update_node_set.intersection(\
181                         current_nodes_reserved_by_start_time[start_time])
182                     old_nodes_set = \
183                         set(\
184                         current_nodes_reserved_by_start_time[start_time])
185                     removed_nodes = \
186                         old_nodes_set.difference(\
187                         requested_nodes_by_start_time[start_time])
188                     logger.debug("IOTLABSLICES verify_slice_leases \
189                         shared_nodes %s  added_nodes %s removed_nodes %s"\
190                         %(shared_nodes, added_nodes,removed_nodes ))
191                     #If the lease is modified, delete it before
192                     #creating it again.
193                     #Add the deleted lease job id in the list
194                     #WARNING :rescheduling does not work if there is already
195                     # 2 running/scheduled jobs because deleting a job
196                     #takes time SA 18/10/2012
197                     if added_nodes or removed_nodes:
198                         deleted_leases.append(\
199                             leases_by_start_time[start_time]['lease_id'])
200                         #Reschedule the job
201                         if added_nodes or shared_nodes:
202                             reschedule_jobs_dict[str(start_time)] = \
203                                         requested_jobs_dict[str(start_time)]
204
205             else:
206                     #New lease
207
208                 job = requested_jobs_dict[str(start_time)]
209                 logger.debug("IOTLABSLICES \
210                               NEWLEASE slice %s  job %s"
211                              % (sfa_slice, job))
212                 job_id = self.driver.AddLeases(
213                     job['hostname'],
214                     sfa_slice, int(job['start_time']),
215                     int(job['duration']))
216
217                 # Removed by jordan
218                 #if job_id is not None:
219                 #    new_leases = self.driver.GetLeases(login=
220                 #        sfa_slice['login'])
221                 #    for new_lease in new_leases:
222                 #        leases.append(new_lease)
223
224         #Deleted leases are the ones with lease id not declared in the Rspec
225         if deleted_leases:
226             self.driver.testbed_shell.DeleteLeases(deleted_leases,
227                                                 sfa_slice['login'])
228             #self.driver.testbed_shell.DeleteLeases(deleted_leases,
229             #                                    sfa_slice['user']['uid'])
230             logger.debug("IOTLABSLICES \
231                           verify_slice_leases slice %s deleted_leases %s"
232                          % (sfa_slice, deleted_leases))
233
234         if reschedule_jobs_dict:
235             for start_time in reschedule_jobs_dict:
236                 job = reschedule_jobs_dict[start_time]
237                 self.driver.AddLeases(
238                     job['hostname'],
239                     sfa_slice, int(job['start_time']),
240                     int(job['duration']))
241
242         # Added by Jordan: until we find a better solution, always update the list of leases
243         return self.driver.GetLeases(login= sfa_slice['login'])
244         #return leases
245
246     def verify_slice_nodes(self, sfa_slice, requested_slivers, peer):
247         """Check for wanted and unwanted nodes in the slice.
248
249         Removes nodes and associated leases that the user does not want anymore
250         by deleteing the associated job in OAR (DeleteSliceFromNodes).
251         Returns the nodes' hostnames that are going to be in the slice.
252
253         :param sfa_slice: slice record. Must contain node_ids and list_node_ids.
254
255         :param requested_slivers: list of requested nodes' hostnames.
256         :param peer: unused so far.
257
258         :type sfa_slice: dict
259         :type requested_slivers: list
260         :type peer: string
261
262         :returns: list requested nodes hostnames
263         :rtype: list
264
265         .. warning:: UNUSED SQA 24/07/13
266         .. seealso:: DeleteSliceFromNodes
267         .. todo:: check what to do with the peer? Can not remove peer nodes from
268             slice here. Anyway, in this case, the peer should have gotten the
269             remove request too.
270
271         """
272         current_slivers = []
273         deleted_nodes = []
274
275         if 'node_ids' in sfa_slice:
276             nodes = self.driver.testbed_shell.GetNodes(
277                 sfa_slice['list_node_ids'],
278                 ['hostname'])
279             current_slivers = [node['hostname'] for node in nodes]
280
281             # remove nodes not in rspec
282             deleted_nodes = list(set(current_slivers).
283                                  difference(requested_slivers))
284
285             logger.debug("IOTLABSLICES \tverify_slice_nodes slice %s\
286                                          \r\n \r\n deleted_nodes %s"
287                          % (sfa_slice, deleted_nodes))
288
289             if deleted_nodes:
290                 #Delete the entire experience
291                 self.driver.testbed_shell.DeleteSliceFromNodes(sfa_slice)
292             return nodes
293
294     def verify_slice(self, slice_hrn, slice_record, sfa_peer):
295         """Ensures slice record exists.
296
297         The slice record must exist either in Iotlab or in the other
298         federated testbed (sfa_peer). If the slice does not belong to Iotlab,
299         check if the user already exists in LDAP. In this case, adds the slice
300         to the sfa DB and associates its LDAP user.
301
302         :param slice_hrn: slice's name
303         :param slice_record: sfa record of the slice
304         :param sfa_peer: name of the peer authority if any.(not Iotlab).
305
306         :type slice_hrn: string
307         :type slice_record: dictionary
308         :type sfa_peer: string
309
310         .. seealso:: AddSlice
311
312
313         """
314
315         slicename = slice_hrn
316         sfa_slice = None
317
318         # check if slice belongs to Iotlab
319         if slicename.startswith("iotlab"):
320             slices_list = self.driver.GetSlices(slice_filter=slicename,
321                                                 slice_filter_type='slice_hrn')
322     
323             if slices_list:
324                 for sl in slices_list:
325     
326                     logger.debug("IOTLABSLICES \t verify_slice slicename %s \
327                                     slices_list %s sl %s \r slice_record %s"
328                                  % (slicename, slices_list, sl, slice_record))
329                     sfa_slice = sl
330                     sfa_slice.update(slice_record)
331
332         else:
333             #Search for user in ldap based on email SA 14/11/12
334             ldap_user = self.driver.testbed_shell.ldap.LdapFindUser(\
335                                                     slice_record['user'])
336             logger.debug(" IOTLABSLICES \tverify_slice Oups \
337                         slice_record %s sfa_peer %s ldap_user %s"
338                         % (slice_record, sfa_peer, ldap_user))
339             #User already registered in ldap, meaning user should be in SFA db
340             #and hrn = sfa_auth+ uid
341             sfa_slice = {'hrn': slicename,
342                          'node_list': [],
343                          'authority': slice_record['authority'],
344                          'gid': slice_record['gid'],
345                          #'slice_id': slice_record['record_id'],
346                          'reg-researchers': slice_record['reg-researchers'],
347                          'urn': hrn_to_urn(slicename,'slice'),
348                          #'peer_authority': str(sfa_peer)
349                          }
350
351             if ldap_user:
352                 hrn = self.driver.testbed_shell.root_auth + '.' \
353                                                 + ldap_user['uid']
354                 user = self.driver.get_user_record(hrn)
355
356                 logger.debug(" IOTLABSLICES \tverify_slice hrn %s USER %s"
357                              % (hrn, user))
358
359                  # add the external slice to the local SFA iotlab DB
360                 if sfa_slice:
361                     self.driver.AddSlice(sfa_slice, user)
362
363             logger.debug("IOTLABSLICES \tverify_slice ADDSLICE OK")
364         return sfa_slice
365
366
367     def verify_persons(self, slice_hrn, slice_record, users, options=None):
368         """Ensures the users in users list exist and are enabled in LDAP. Adds
369         person if needed (AddPerson).
370
371         Checking that a user exist is based on the user's email. If the user is
372         still not found in the LDAP, it means that the user comes from another
373         federated testbed. In this case an account has to be created in LDAP
374         so as to enable the user to use the testbed, since we trust the testbed
375         he comes from. This is done by calling AddPerson.
376
377         :param slice_hrn: slice name
378         :param slice_record: record of the slice_hrn
379         :param users: users is a record list. Records can either be
380             local records or users records from known and trusted federated
381             sites.If the user is from another site that iotlab doesn't trust
382             yet, then Resolve will raise an error before getting to allocate.
383
384         :type slice_hrn: string
385         :type slice_record: string
386         :type users: list
387
388         .. seealso:: AddPerson
389         .. note:: Removed unused peer and sfa_peer parameters. SA 18/07/13.
390
391
392         """
393         slice_user = slice_record['user']['hrn']
394
395         if options is None: options={}
396         logger.debug("IOTLABSLICES \tverify_persons \tslice_hrn  %s  \
397                     \t slice_record %s\r\n users %s \t  "
398                      % (slice_hrn, slice_record, users))
399
400         users_by_email = {}
401         #users_dict : dict whose keys can either be the user's hrn or its id.
402         #Values contains only id and hrn
403         users_dict = {}
404         
405         # XXX LOIC !!! Fix: Only 1 user per slice in iotlab
406         users = [slice_record['user']]
407         #First create dicts by hrn and id for each user in the user record list:
408         for info in users:
409             # if 'slice_record' in info:
410             #     slice_rec = info['slice_record']
411                 # if 'user' in slice_rec :
412                 #     user = slice_rec['user']
413
414             if 'email' in info:
415                 users_by_email[info['email']] = info
416                 users_dict[info['email']] = info
417
418         #logger.debug("IOTLABSLICES.PY \t verify_person  \
419         #                users_dict %s \r\n user_by_email %s \r\n  "
420         #             % (users_dict, users_by_email))
421
422         existing_user_ids = []
423         existing_user_emails = []
424         existing_users = []
425         # Check if user is in Iotlab LDAP using its hrn.
426         # Assuming Iotlab is centralised :  one LDAP for all sites,
427         # user's record_id unknown from LDAP
428         # LDAP does not provide users id, therefore we rely on email to find the
429         # user in LDAP
430
431         if users_by_email:
432             #Construct the list of filters (list of dicts) for GetPersons
433             filter_user = [users_by_email[email] for email in users_by_email]
434             #Check user i in LDAP with GetPersons
435             #Needed because what if the user has been deleted in LDAP but
436             #is still in SFA?
437             # GetPersons -> LdapFindUser -> _process_ldap_info_for_one_user
438             # XXX LOIC Fix in _process_ldap_info_for_one_user not to update user with hrn=None
439             existing_users = self.driver.testbed_shell.GetPersons(filter_user)
440             logger.debug(" \r\n IOTLABSLICES.PY \tverify_person  filter_user %s\
441                        existing_users %s  "
442                         % (filter_user, existing_users))
443             #User is in iotlab LDAP
444             if existing_users:
445                 for user in existing_users:
446                     user['login'] = user['uid']
447                     # XXX LOIC Fix we already have all informations comming from Allocate
448                     #users_dict[user['email']].update(user)
449                     #existing_user_emails.append(
450                     #    users_dict[user['email']]['email'])
451                 logger.debug("User is in iotlab LDAP slice_record[user] = %s" % slice_user)
452
453             # User from another known trusted federated site. Check
454             # if a iotlab account matching the email has already been created.
455             else:
456                 req = 'mail='
457                 if isinstance(users, list):
458                     req += users[0]['email']
459                 else:
460                     req += users['email']
461                 ldap_reslt = self.driver.testbed_shell.ldap.LdapSearch(req)
462                 logger.debug("LdapSearch slice_record[user] = %s" % slice_user)
463                 if ldap_reslt:
464                     logger.debug(" IOTLABSLICES.PY \tverify_person users \
465                                 USER already in Iotlab \t ldap_reslt %s \
466                                 " % (ldap_reslt))
467                     existing_users.append(ldap_reslt[1])
468                     logger.debug("ldap_reslt slice_record[user] = %s" % slice_user)
469                 else:
470                     #User not existing in LDAP
471                     logger.debug("IOTLABSLICES.PY \tverify_person users \
472                                 not in ldap ...NEW ACCOUNT NEEDED %s \r\n \t \
473                                 ldap_reslt %s " % (users, ldap_reslt))
474
475         requested_user_emails = users_by_email.keys()
476         # requested_user_hrns = \
477         #     [users_by_email[user]['hrn'] for user in users_by_email]
478         # logger.debug("IOTLABSLICES.PY \tverify_person  \
479         #                users_by_email  %s " % (users_by_email))
480
481         # #Check that the user of the slice in the slice record
482         # #matches one of the existing users
483         # try:
484         #     if slice_record['reg-researchers'][0] in requested_user_hrns:
485         #         logger.debug(" IOTLABSLICES  \tverify_person ['PI']\
486         #                         slice_record %s" % (slice_record))
487
488         # except KeyError:
489         #     pass
490
491         # users to be added, removed or updated
492         #One user in one iotlab slice : there should be no need
493         #to remove/ add any user from/to a slice.
494         #However a user from SFA which is not registered in Iotlab yet
495         #should be added to the LDAP.
496         added_user_emails = set(requested_user_emails).\
497                                         difference(set(existing_user_emails))
498
499
500         #self.verify_keys(existing_slice_users, updated_users_list, \
501                                                             #peer, append)
502
503         # XXX JORDAN the uid of the user is put in slice_record['login']
504         added_persons = []
505         # add new users
506         #requested_user_email is in existing_user_emails
507         if len(added_user_emails) == 0:
508             slice_record['login'] = existing_users[0]['uid']
509             #slice_record['login'] = users_dict[requested_user_emails[0]]['uid']
510             logger.debug(" IOTLABSLICES  \tverify_person QUICK DIRTY %s"
511                          % (slice_record))
512             # XXX JORDAN uid == 'register'
513
514         # XXX JORDAN i have no added_user_emails
515         for added_user_email in added_user_emails:
516             added_user = users_dict[added_user_email]
517             logger.debug(" IOTLABSLICES \r\n \r\n  \t  verify_person \
518                          added_user %s" % (added_user))
519             person = {}
520             person['peer_person_id'] = None
521             k_list = ['first_name', 'last_name', 'person_id']
522             for k in k_list:
523                 if k in added_user:
524                     person[k] = added_user[k]
525             # bug user without key
526             if added_user['keys']:
527                 person['pkey'] = added_user['keys'][0]
528             person['mail'] = added_user['email']
529             person['email'] = added_user['email']
530             person['key_ids'] = added_user.get('key_ids', [])
531
532             ret = self.driver.AddPerson(person)
533             if 'uid' in ret:
534                 # meaning bool is True and the AddPerson was successful
535                 person['uid'] = ret['uid']
536                 slice_record['login'] = person['uid']
537             else:
538                 # error message in ret
539                 logger.debug(" IOTLABSLICES ret message %s" %(ret))
540
541             logger.debug(" IOTLABSLICES \r\n \r\n  \t THE SECOND verify_person\
542                            person %s" % (person))
543             #Update slice_Record with the id now known to LDAP
544
545
546             added_persons.append(person)
547         return added_persons
548
549
550     def verify_keys(self, persons, users, peer, options=None):
551         """
552         .. warning:: unused
553         """
554         if options is None: options={}
555         # existing keys
556         key_ids = []
557         for person in persons:
558             key_ids.extend(person['key_ids'])
559         keylist = self.driver.GetKeys(key_ids, ['key_id', 'key'])
560
561         keydict = {}
562         for key in keylist:
563             keydict[key['key']] = key['key_id']
564         existing_keys = keydict.keys()
565
566         persondict = {}
567         for person in persons:
568             persondict[person['email']] = person
569
570         # add new keys
571         requested_keys = []
572         updated_persons = []
573         users_by_key_string = {}
574         for user in users:
575             user_keys = user.get('keys', [])
576             updated_persons.append(user)
577             for key_string in user_keys:
578                 users_by_key_string[key_string] = user
579                 requested_keys.append(key_string)
580                 if key_string not in existing_keys:
581                     key = {'key': key_string, 'key_type': 'ssh'}
582                     #try:
583                         ##if peer:
584                             #person = persondict[user['email']]
585                             #self.driver.testbed_shell.UnBindObjectFromPeer(
586                                 # 'person',person['person_id'],
587                                 # peer['shortname'])
588                     ret = self.driver.testbed_shell.AddPersonKey(
589                         user['email'], key)
590                         #if peer:
591                             #key_index = user_keys.index(key['key'])
592                             #remote_key_id = user['key_ids'][key_index]
593                             #self.driver.testbed_shell.BindObjectToPeer('key', \
594                                             #key['key_id'], peer['shortname'], \
595                                             #remote_key_id)
596
597         # remove old keys (only if we are not appending)
598         append = options.get('append', True)
599         if append is False:
600             removed_keys = set(existing_keys).difference(requested_keys)
601             for key in removed_keys:
602                     #if peer:
603                         #self.driver.testbed_shell.UnBindObjectFromPeer('key', \
604                                         #key, peer['shortname'])
605
606                 user = users_by_key_string[key]
607                 self.driver.testbed_shell.DeleteKey(user, key)
608
609         return