d66d1e5cb247442a08fb01c50ae98cb34fbb082c
[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
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.iotlab_api.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.iotlab_api.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.iotlab_api.GetLeases({'name': 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             if job['duration'] < self.driver.iotlab_api.GetLeaseGranularity():
134                 del requested_jobs_dict[job['start_time']]
135
136         #Requested jobs
137         for start_time in requested_jobs_dict:
138             requested_nodes_by_start_time[int(start_time)] = \
139                 requested_jobs_dict[start_time]['hostname']
140         #Check if there is any difference between the leases already
141         #registered in OAR and the requested jobs.
142         #Difference could be:
143         #-Lease deleted in the requested jobs
144         #-Added/removed nodes
145         #-Newly added lease
146
147         logger.debug("IOTLABSLICES verify_slice_leases \
148                         requested_nodes_by_start_time %s \
149                         "% (requested_nodes_by_start_time))
150         #Find all deleted leases
151         start_time_list = \
152             list(set(leases_by_start_time.keys()).\
153             difference(requested_nodes_by_start_time.keys()))
154         deleted_leases = [leases_by_start_time[start_time]['lease_id'] \
155                             for start_time in start_time_list]
156
157
158         #Find added or removed nodes in exisiting leases
159         for start_time in requested_nodes_by_start_time:
160             logger.debug("IOTLABSLICES verify_slice_leases  start_time %s \
161                          "%( start_time))
162             if start_time in current_nodes_reserved_by_start_time:
163
164                 if requested_nodes_by_start_time[start_time] == \
165                     current_nodes_reserved_by_start_time[start_time]:
166                     continue
167
168                 else:
169                     update_node_set = \
170                             set(requested_nodes_by_start_time[start_time])
171                     added_nodes = \
172                         update_node_set.difference(\
173                         current_nodes_reserved_by_start_time[start_time])
174                     shared_nodes = \
175                         update_node_set.intersection(\
176                         current_nodes_reserved_by_start_time[start_time])
177                     old_nodes_set = \
178                         set(\
179                         current_nodes_reserved_by_start_time[start_time])
180                     removed_nodes = \
181                         old_nodes_set.difference(\
182                         requested_nodes_by_start_time[start_time])
183                     logger.debug("IOTLABSLICES verify_slice_leases \
184                         shared_nodes %s  added_nodes %s removed_nodes %s"\
185                         %(shared_nodes, added_nodes,removed_nodes ))
186                     #If the lease is modified, delete it before
187                     #creating it again.
188                     #Add the deleted lease job id in the list
189                     #WARNING :rescheduling does not work if there is already
190                     # 2 running/scheduled jobs because deleting a job
191                     #takes time SA 18/10/2012
192                     if added_nodes or removed_nodes:
193                         deleted_leases.append(\
194                             leases_by_start_time[start_time]['lease_id'])
195                         #Reschedule the job
196                         if added_nodes or shared_nodes:
197                             reschedule_jobs_dict[str(start_time)] = \
198                                         requested_jobs_dict[str(start_time)]
199
200             else:
201                     #New lease
202
203                 job = requested_jobs_dict[str(start_time)]
204                 logger.debug("IOTLABSLICES \
205                               NEWLEASE slice %s  job %s"
206                              % (sfa_slice, job))
207                 self.driver.iotlab_api.AddLeases(
208                     job['hostname'],
209                     sfa_slice, int(job['start_time']),
210                     int(job['duration']))
211
212         #Deleted leases are the ones with lease id not declared in the Rspec
213         if deleted_leases:
214             self.driver.iotlab_api.DeleteLeases(deleted_leases,
215                                                 sfa_slice['user']['uid'])
216             logger.debug("IOTLABSLICES \
217                           verify_slice_leases slice %s deleted_leases %s"
218                          % (sfa_slice, deleted_leases))
219
220         if reschedule_jobs_dict:
221             for start_time in reschedule_jobs_dict:
222                 job = reschedule_jobs_dict[start_time]
223                 self.driver.iotlab_api.AddLeases(
224                     job['hostname'],
225                     sfa_slice, int(job['start_time']),
226                     int(job['duration']))
227         return leases
228
229     def verify_slice_nodes(self, sfa_slice, requested_slivers, peer):
230         """Check for wanted and unwanted nodes in the slice.
231
232         Removes nodes and associated leases that the user does not want anymore
233         by deleteing the associated job in OAR (DeleteSliceFromNodes).
234         Returns the nodes' hostnames that are going to be in the slice.
235
236         :param sfa_slice: slice record. Must contain node_ids and list_node_ids.
237
238         :param requested_slivers: list of requested nodes' hostnames.
239         :param peer: unused so far.
240
241         :type sfa_slice: dict
242         :type requested_slivers: list
243         :type peer: string
244
245         :returns: list requested nodes hostnames
246         :rtype: list
247
248         .. warning:: UNUSED SQA 24/07/13
249         .. seealso:: DeleteSliceFromNodes
250         .. todo:: check what to do with the peer? Can not remove peer nodes from
251             slice here. Anyway, in this case, the peer should have gotten the
252             remove request too.
253
254         """
255         current_slivers = []
256         deleted_nodes = []
257
258         if 'node_ids' in sfa_slice:
259             nodes = self.driver.iotlab_api.GetNodes(
260                 sfa_slice['list_node_ids'],
261                 ['hostname'])
262             current_slivers = [node['hostname'] for node in nodes]
263
264             # remove nodes not in rspec
265             deleted_nodes = list(set(current_slivers).
266                                  difference(requested_slivers))
267
268             logger.debug("IOTLABSLICES \tverify_slice_nodes slice %s\
269                                          \r\n \r\n deleted_nodes %s"
270                          % (sfa_slice, deleted_nodes))
271
272             if deleted_nodes:
273                 #Delete the entire experience
274                 self.driver.iotlab_api.DeleteSliceFromNodes(sfa_slice)
275             return nodes
276
277     def verify_slice(self, slice_hrn, slice_record, sfa_peer):
278         """Ensures slice record exists.
279
280         The slice record must exist either in Iotlab or in the other
281         federated testbed (sfa_peer). If the slice does not belong to Iotlab,
282         check if the user already exists in LDAP. In this case, adds the slice
283         to the sfa DB and associates its LDAP user.
284
285         :param slice_hrn: slice's name
286         :param slice_record: sfa record of the slice
287         :param sfa_peer: name of the peer authority if any.(not Iotlab).
288
289         :type slice_hrn: string
290         :type slice_record: dictionary
291         :type sfa_peer: string
292
293         .. seealso:: AddSlice
294
295
296         """
297
298         slicename = slice_hrn
299         # check if slice belongs to Iotlab
300         slices_list = self.driver.iotlab_api.GetSlices(
301             slice_filter=slicename, slice_filter_type='slice_hrn')
302
303         sfa_slice = None
304
305         if slices_list:
306             for sl in slices_list:
307
308                 logger.debug("SLABSLICE \t verify_slice slicename %s \
309                                 slices_list %s sl %s \r slice_record %s"
310                              % (slicename, slices_list, sl, slice_record))
311                 sfa_slice = sl
312                 sfa_slice.update(slice_record)
313
314         else:
315             #Search for user in ldap based on email SA 14/11/12
316             ldap_user = self.driver.iotlab_api.ldap.LdapFindUser(\
317                                                     slice_record['user'])
318             logger.debug(" IOTLABSLICES \tverify_slice Oups \
319                         slice_record %s sfa_peer %s ldap_user %s"
320                         % (slice_record, sfa_peer, ldap_user))
321             #User already registered in ldap, meaning user should be in SFA db
322             #and hrn = sfa_auth+ uid
323             sfa_slice = {'hrn': slicename,
324                          'node_list': [],
325                          'authority': slice_record['authority'],
326                          'gid': slice_record['gid'],
327                          'slice_id': slice_record['record_id'],
328                          'reg-researchers': slice_record['reg-researchers'],
329                          'peer_authority': str(sfa_peer)
330                          }
331
332             if ldap_user:
333                 hrn = self.driver.iotlab_api.root_auth + '.' + ldap_user['uid']
334                 user = self.driver.get_user_record(hrn)
335
336                 logger.debug(" IOTLABSLICES \tverify_slice hrn %s USER %s"
337                              % (hrn, user))
338
339                  # add the external slice to the local SFA iotlab DB
340                 if sfa_slice:
341                     self.driver.iotlab_api.AddSlice(sfa_slice, user)
342
343             logger.debug("IOTLABSLICES \tverify_slice ADDSLICE OK")
344         return sfa_slice
345
346
347     def verify_persons(self, slice_hrn, slice_record, users, options={}):
348         """Ensures the users in users list exist and are enabled in LDAP. Adds
349         person if needed.
350
351         Checking that a user exist is based on the user's email. If the user is
352         still not found in the LDAP, it means that the user comes from another
353         federated. In this case an account has to be created in LDAP
354         so as to enable the user to use the testbed, since we trust the testbed
355         he comes from. This is done by calling AddPerson.
356
357         :param slice_hrn: slice name
358         :param slice_record: record of the slice_hrn
359         :param users: users is a record list. Records can either be
360             local records or users records from known and trusted federated
361             sites.If the user is from another site that iotlab doesn't trust yet,
362             then Resolve will raise an error before getting to create_sliver.
363
364         :type slice_hrn: string
365         :type slice_record: string
366         :type users: list
367
368         .. seealso:: AddPerson
369         .. note:: Removed unused peer and sfa_peer parameters. SA 18/07/13.
370
371
372         """
373         #TODO SA 21/08/12 verify_persons Needs review
374
375         logger.debug("IOTLABSLICES \tverify_persons \tslice_hrn  %s  \
376                     \t slice_record %s\r\n users %s \t  "
377                      % (slice_hrn, slice_record, users))
378         users_by_id = {}
379
380         users_by_email = {}
381         #users_dict : dict whose keys can either be the user's hrn or its id.
382         #Values contains only id and hrn
383         users_dict = {}
384
385         #First create dicts by hrn and id for each user in the user record list:
386         for info in users:
387             if 'slice_record' in info:
388                 slice_rec = info['slice_record']
389                 user = slice_rec['user']
390
391             if 'email' in user:
392                 users_by_email[user['email']] = user
393                 users_dict[user['email']] = user
394
395         logger.debug("SLABSLICE.PY \t verify_person  \
396                         users_dict %s \r\n user_by_email %s \r\n \
397                         \tusers_by_id %s "
398                      % (users_dict, users_by_email, users_by_id))
399
400         existing_user_ids = []
401         existing_user_emails = []
402         existing_users = []
403         # Check if user is in Iotlab LDAP using its hrn.
404         # Assuming Iotlab is centralised :  one LDAP for all sites,
405         # user's record_id unknown from LDAP
406         # LDAP does not provide users id, therefore we rely on email to find the
407         # user in LDAP
408
409         if users_by_email:
410             #Construct the list of filters (list of dicts) for GetPersons
411             filter_user = [users_by_email[email] for email in users_by_email]
412             #Check user i in LDAP with GetPersons
413             #Needed because what if the user has been deleted in LDAP but
414             #is still in SFA?
415             existing_users = self.driver.iotlab_api.GetPersons(filter_user)
416             logger.debug(" \r\n SLABSLICE.PY \tverify_person  filter_user \
417                         %s existing_users %s "
418                         % (filter_user, existing_users))
419             #User is in iotlab LDAP
420             if existing_users:
421                 for user in existing_users:
422                     users_dict[user['email']].update(user)
423                     existing_user_emails.append(
424                         users_dict[user['email']]['email'])
425
426
427             # User from another known trusted federated site. Check
428             # if a iotlab account matching the email has already been created.
429             else:
430                 req = 'mail='
431                 if isinstance(users, list):
432                     req += users[0]['email']
433                 else:
434                     req += users['email']
435                 ldap_reslt = self.driver.iotlab_api.ldap.LdapSearch(req)
436
437                 if ldap_reslt:
438                     logger.debug(" SLABSLICE.PY \tverify_person users \
439                                 USER already in Iotlab \t ldap_reslt %s \
440                                 " % (ldap_reslt))
441                     existing_users.append(ldap_reslt[1])
442
443                 else:
444                     #User not existing in LDAP
445                     logger.debug("SLABSLICE.PY \tverify_person users \
446                                 not in ldap ...NEW ACCOUNT NEEDED %s \r\n \t \
447                                 ldap_reslt %s " % (users, ldap_reslt))
448
449         requested_user_emails = users_by_email.keys()
450         requested_user_hrns = \
451             [users_by_email[user]['hrn'] for user in users_by_email]
452         logger.debug("SLABSLICE.PY \tverify_person  \
453                        users_by_email  %s " % (users_by_email))
454
455         #Check that the user of the slice in the slice record
456         #matches one of the existing users
457         try:
458             if slice_record['PI'][0] in requested_user_hrns:
459                 logger.debug(" SLABSLICE  \tverify_person ['PI']\
460                                 slice_record %s" % (slice_record))
461
462         except KeyError:
463             pass
464
465         # users to be added, removed or updated
466         #One user in one iotlab slice : there should be no need
467         #to remove/ add any user from/to a slice.
468         #However a user from SFA which is not registered in Iotlab yet
469         #should be added to the LDAP.
470         added_user_emails = set(requested_user_emails).\
471                                         difference(set(existing_user_emails))
472
473
474         #self.verify_keys(existing_slice_users, updated_users_list, \
475                                                             #peer, append)
476
477         added_persons = []
478         # add new users
479         #requested_user_email is in existing_user_emails
480         if len(added_user_emails) == 0:
481             slice_record['login'] = users_dict[requested_user_emails[0]]['uid']
482             logger.debug(" SLABSLICE  \tverify_person QUICK DIRTY %s"
483                          % (slice_record))
484
485         for added_user_email in added_user_emails:
486             added_user = users_dict[added_user_email]
487             logger.debug(" IOTLABSLICES \r\n \r\n  \t  verify_person \
488                          added_user %s" % (added_user))
489             person = {}
490             person['peer_person_id'] = None
491             k_list = ['first_name', 'last_name', 'person_id']
492             for k in k_list:
493                 if k in added_user:
494                     person[k] = added_user[k]
495
496             person['pkey'] = added_user['keys'][0]
497             person['mail'] = added_user['email']
498             person['email'] = added_user['email']
499             person['key_ids'] = added_user.get('key_ids', [])
500
501             ret = self.driver.iotlab_api.AddPerson(person)
502             if 'uid' in ret:
503                 # meaning bool is True and the AddPerson was successful
504                 person['uid'] = ret['uid']
505                 slice_record['login'] = person['uid']
506             else:
507                 # error message in ret
508                 logger.debug(" IOTLABSLICES ret message %s" %(ret))
509
510             logger.debug(" SLABSLICE \r\n \r\n  \t THE SECOND verify_person\
511                            person %s" % (person))
512             #Update slice_Record with the id now known to LDAP
513
514
515             added_persons.append(person)
516         return added_persons
517
518
519     def verify_keys(self, persons, users, peer, options={}):
520         """
521         .. warning:: unused
522         """
523         # existing keys
524         key_ids = []
525         for person in persons:
526             key_ids.extend(person['key_ids'])
527         keylist = self.driver.iotlab_api.GetKeys(key_ids, ['key_id', 'key'])
528
529         keydict = {}
530         for key in keylist:
531             keydict[key['key']] = key['key_id']
532         existing_keys = keydict.keys()
533
534         persondict = {}
535         for person in persons:
536             persondict[person['email']] = person
537
538         # add new keys
539         requested_keys = []
540         updated_persons = []
541         users_by_key_string = {}
542         for user in users:
543             user_keys = user.get('keys', [])
544             updated_persons.append(user)
545             for key_string in user_keys:
546                 users_by_key_string[key_string] = user
547                 requested_keys.append(key_string)
548                 if key_string not in existing_keys:
549                     key = {'key': key_string, 'key_type': 'ssh'}
550                     #try:
551                         ##if peer:
552                             #person = persondict[user['email']]
553                             #self.driver.iotlab_api.UnBindObjectFromPeer(
554                                 # 'person',person['person_id'],
555                                 # peer['shortname'])
556                     ret = self.driver.iotlab_api.AddPersonKey(
557                         user['email'], key)
558                         #if peer:
559                             #key_index = user_keys.index(key['key'])
560                             #remote_key_id = user['key_ids'][key_index]
561                             #self.driver.iotlab_api.BindObjectToPeer('key', \
562                                             #key['key_id'], peer['shortname'], \
563                                             #remote_key_id)
564
565                     #finally:
566                         #if peer:
567                             #self.driver.iotlab_api.BindObjectToPeer('person', \
568                                     #person['person_id'], peer['shortname'], \
569                                     #user['person_id'])
570
571         # remove old keys (only if we are not appending)
572         append = options.get('append', True)
573         if append is False:
574             removed_keys = set(existing_keys).difference(requested_keys)
575             for key in removed_keys:
576                     #if peer:
577                         #self.driver.iotlab_api.UnBindObjectFromPeer('key', \
578                                         #key, peer['shortname'])
579
580                 user = users_by_key_string[key]
581                 self.driver.iotlab_api.DeleteKey(user, key)
582
583         return