f97cd3aca287f0a240077b7e99d57fd87afa1879
[sfa.git] / sfa / importer / iotlabimporter.py
1 from sfa.util.config import Config
2 from sfa.util.xrn import Xrn, get_authority, hrn_to_urn
3
4 from sfa.iotlab.iotlabdriver import IotlabDriver
5
6 from sfa.trust.certificate import Keypair, convert_public_key
7 from sfa.trust.gid import create_uuid
8
9 from sfa.storage.alchemy import dbsession
10 from sfa.storage.model import RegRecord, RegAuthority, RegSlice, RegNode, \
11                                                     RegUser, RegKey
12
13
14 from sqlalchemy.exc import SQLAlchemyError
15
16
17
18 class IotlabImporter:
19     """
20     IotlabImporter class, generic importer_class. Used to populate the SFA DB
21     with iotlab resources' records.
22     Used to update records when new resources, users or nodes, are added
23     or deleted.
24     """
25
26     def __init__ (self, auth_hierarchy, loc_logger):
27         """
28         Sets and defines import logger and the authority name. Gathers all the
29         records already registerd in the SFA DB, broke them into 3 dicts,
30         by type and hrn, by email and by type and pointer.
31
32         :param auth_hierarchy: authority name
33         :type auth_hierarchy: string
34         :param loc_logger: local logger
35         :type loc_logger: _SfaLogger
36
37         """
38         self.auth_hierarchy = auth_hierarchy
39         self.logger = loc_logger
40         self.logger.setLevelDebug()
41         #retrieve all existing SFA objects
42         self.all_records = dbsession.query(RegRecord).all()
43
44         # initialize record.stale to True by default,
45         # then mark stale=False on the ones that are in use
46         for record in self.all_records:
47             record.stale = True
48         #create hash by (type,hrn)
49         #used  to know if a given record is already known to SFA
50         self.records_by_type_hrn = \
51             dict([( (record.type,record.hrn), record) \
52                                         for record in self.all_records])
53
54         self.users_rec_by_email = \
55             dict([ (record.email, record) \
56                 for record in self.all_records if record.type == 'user'])
57
58         # create hash by (type,pointer)
59         self.records_by_type_pointer = \
60             dict([ ( (str(record.type), record.pointer) , record) \
61                 for record in self.all_records  if record.pointer != -1])
62
63
64
65     @staticmethod
66     def hostname_to_hrn_escaped(root_auth, hostname):
67         """
68
69         Returns a node's hrn based on its hostname and the root
70         authority and by removing special caracters from the hostname.
71
72         :param root_auth: root authority name
73         :param hostname: nodes's hostname
74         :type  root_auth: string
75         :type hostname: string
76         :rtype: string
77         """
78         return '.'.join( [root_auth, Xrn.escape(hostname)] )
79
80
81     @staticmethod
82     def slicename_to_hrn(person_hrn):
83         """
84
85         Returns the slicename associated to a given person's hrn.
86
87         :param person_hrn: user's hrn
88         :type person_hrn: string
89         :rtype: string
90         """
91         return  (person_hrn +'_slice')
92
93     def add_options (self, parser):
94         # we don't have any options for now
95         pass
96
97     def find_record_by_type_hrn(self, record_type, hrn):
98         """
99
100         Returns the record associated with a given hrn and hrn type.
101         Returns None if the key tuple is not in dictionary.
102
103         :param record_type: the record's type (slice, node, authority...)
104         :type  record_type: string
105         :param hrn: Human readable name of the object's record
106         :type hrn: string
107         :rtype: RegUser if user, RegSlice if slice, RegNode if node...
108                 or None if record does not exist.
109
110         """
111         return self.records_by_type_hrn.get ( (record_type, hrn), None)
112
113     def locate_by_type_pointer (self, record_type, pointer):
114         """
115
116         Returns the record corresponding to the key pointer and record
117         type. Returns None if the record does not exist and is not in the
118         records_by_type_pointer dictionnary.
119
120         :param record_type: the record's type (slice, node, authority...)
121         :type  record_type: string
122         :param pointer:Pointer to where the record is in the origin db,
123         used in case the record comes from a trusted authority.
124         :type pointer: integer
125         :rtype: RegUser if user, RegSlice if slice, RegNode if node...
126                 or None if record does not exist.
127         """
128         return self.records_by_type_pointer.get ( (record_type, pointer), None)
129
130
131     def update_just_added_records_dict (self, record):
132         """
133
134         Updates the records_by_type_hrn dictionnary if the record has
135         just been created.
136
137         :param record: Record to add in the records_by_type_hrn dict.
138         :type record: dictionary
139         """
140         rec_tuple = (record.type, record.hrn)
141         if rec_tuple in self.records_by_type_hrn:
142             self.logger.warning ("IotlabImporter.update_just_added_records_dict:\
143                         duplicate (%s,%s)"%rec_tuple)
144             return
145         self.records_by_type_hrn [ rec_tuple ] = record
146
147     def import_sites_and_nodes(self, iotlabdriver):
148         """
149
150         Gets all the sites and nodes from OAR, process the information,
151         creates hrns and RegAuthority for sites, and feed them to the database.
152         For each site, import the site's nodes to the DB by calling
153         import_nodes.
154
155         :param iotlabdriver: IotlabDriver object, used to have access to iotlabdriver
156         methods and fetching info on sites and nodes.
157         :type iotlabdriver: IotlabDriver
158         """
159
160         sites_listdict  = iotlabdriver.iotlab_api.GetSites()
161         nodes_listdict  = iotlabdriver.iotlab_api.GetNodes()
162         nodes_by_id = dict([(node['node_id'], node) for node in nodes_listdict])
163         for site in sites_listdict:
164             site_hrn = site['name']
165             site_record = self.find_record_by_type_hrn ('authority', site_hrn)
166             if not site_record:
167                 try:
168                     urn = hrn_to_urn(site_hrn, 'authority')
169                     if not self.auth_hierarchy.auth_exists(urn):
170                         self.auth_hierarchy.create_auth(urn)
171
172                     auth_info = self.auth_hierarchy.get_auth_info(urn)
173                     site_record = RegAuthority(hrn=site_hrn, \
174                                                gid=auth_info.get_gid_object(),
175                                                pointer='-1',
176                                                authority=get_authority(site_hrn))
177                     site_record.just_created()
178                     dbsession.add(site_record)
179                     dbsession.commit()
180                     self.logger.info("IotlabImporter: imported authority (site) \
181                          %s" % site_record)
182                     self.update_just_added_records_dict(site_record)
183                 except SQLAlchemyError:
184                     # if the site import fails then there is no point in
185                     # trying to import the
186                     # site's child records(node, slices, persons), so skip them.
187                     self.logger.log_exc("IotlabImporter: failed to import site. \
188                         Skipping child records")
189                     continue
190             else:
191                 # xxx update the record ...
192                 pass
193
194
195             site_record.stale = False
196             self.import_nodes(site['node_ids'], nodes_by_id, iotlabdriver)
197
198             return
199
200     def import_nodes(self, site_node_ids, nodes_by_id, iotlabdriver):
201         """
202
203         Creates appropriate hostnames and RegNode records for
204         each node in site_node_ids, based on the information given by the
205         dict nodes_by_id that was made from data from OAR.
206         Saves the records to the DB.
207
208         :param site_node_ids: site's node ids
209         :type site_node_ids: list of integers
210         :param nodes_by_id: dictionary , key is the node id, value is the a dict
211         with node information.
212         :type nodes_by_id: dictionary
213         :param iotlabdriver:IotlabDriver object, used to have access to iotlabdriver
214         attributes.
215         :type iotlabdriver:IotlabDriver
216
217         """
218
219         for node_id in site_node_ids:
220             try:
221                 node = nodes_by_id[node_id]
222             except KeyError:
223                 self.logger.warning ("IotlabImporter: cannot find node_id %s \
224                         - ignored" %(node_id))
225                 continue
226             escaped_hrn =  \
227             self.hostname_to_hrn_escaped(iotlabdriver.iotlab_api.root_auth, \
228             node['hostname'])
229             self.logger.info("IOTLABIMPORTER node %s " %(node))
230             hrn =  node['hrn']
231
232
233             # xxx this sounds suspicious
234             if len(hrn) > 64:
235                 hrn = hrn[:64]
236             node_record = self.find_record_by_type_hrn( 'node', hrn )
237             if not node_record:
238                 pkey = Keypair(create=True)
239                 urn = hrn_to_urn(escaped_hrn, 'node')
240                 node_gid = \
241                     self.auth_hierarchy.create_gid(urn, \
242                     create_uuid(), pkey)
243
244                 def iotlab_get_authority(hrn):
245                     return hrn.split(".")[0]
246
247                 node_record = RegNode(hrn=hrn, gid=node_gid,
248                                     pointer = '-1',
249                                     authority=iotlab_get_authority(hrn))
250                 try:
251
252                     node_record.just_created()
253                     dbsession.add(node_record)
254                     dbsession.commit()
255                     self.logger.info("IotlabImporter: imported node: %s" \
256                                 % node_record)
257                     self.update_just_added_records_dict(node_record)
258                 except SQLAlchemyError:
259                     self.logger.log_exc("IotlabImporter: \
260                                     failed to import node")
261             else:
262                 #TODO:  xxx update the record ...
263                 pass
264             node_record.stale = False
265
266
267     def init_person_key (self, person, iotlab_key):
268         """
269
270         Returns a tuple pubkey and pkey.
271
272         :param person Person's data.
273         :type person: dict
274         :param iotlab_key: SSH public key, from LDAP user's data.
275         RSA type supported.
276         :type iotlab_key: string
277         :rtype (string, Keypair)
278         """
279         pubkey = None
280         if  person['pkey']:
281             # randomly pick first key in set
282             pubkey = iotlab_key
283
284             try:
285                 pkey = convert_public_key(pubkey)
286             except TypeError:
287                 #key not good. create another pkey
288                 self.logger.warn('IotlabImporter: \
289                                     unable to convert public \
290                                     key for %s' %person['hrn'])
291                 pkey = Keypair(create=True)
292
293         else:
294             # the user has no keys.
295             #Creating a random keypair for the user's gid
296             self.logger.warn("IotlabImporter: person %s does not have a  \
297                         public key" %(person['hrn']))
298             pkey = Keypair(create=True)
299         return (pubkey, pkey)
300
301
302     def import_persons_and_slices(self, iotlabdriver):
303         """
304
305         Gets user data from LDAP, process the information.
306         Creates hrn for the user's slice, the user's gid, creates
307         the RegUser record associated with user. Creates the RegKey record
308         associated nwith the user's key.
309         Saves those records into the SFA DB.
310         import the user's slice onto the database as well by calling
311         import_slice.
312
313         :param iotlabdriver:IotlabDriver object, used to have access to iotlabdriver
314         attributes.
315         :type iotlabdriver:IotlabDriver
316         """
317         ldap_person_listdict = iotlabdriver.iotlab_api.GetPersons()
318         self.logger.info("IOTLABIMPORT \t ldap_person_listdict %s \r\n" \
319                 %(ldap_person_listdict))
320
321          # import persons
322         for person in ldap_person_listdict :
323
324             self.logger.info("IotlabImporter: person :" %(person))
325             if 'ssh-rsa' not in person['pkey']:
326                 #people with invalid ssh key (ssh-dss, empty, bullshit keys...)
327                 #won't be imported
328                 continue
329             person_hrn = person['hrn']
330             slice_hrn = self.slicename_to_hrn(person['hrn'])
331
332             # xxx suspicious again
333             if len(person_hrn) > 64:
334                 person_hrn = person_hrn[:64]
335             person_urn = hrn_to_urn(person_hrn, 'user')
336
337
338             self.logger.info("IotlabImporter: users_rec_by_email %s " \
339                                             %(self.users_rec_by_email))
340
341             #Check if user using person['email'] from LDAP is already registered
342             #in SFA. One email = one person. In this case, do not create another
343             #record for this person
344             #person_hrn returned by GetPerson based on iotlab root auth +
345             #uid ldap
346             user_record = self.find_record_by_type_hrn('user', person_hrn)
347
348             if not user_record and  person['email'] in self.users_rec_by_email:
349                 user_record = self.users_rec_by_email[person['email']]
350                 person_hrn = user_record.hrn
351                 person_urn = hrn_to_urn(person_hrn, 'user')
352
353
354             slice_record = self.find_record_by_type_hrn ('slice', slice_hrn)
355
356             iotlab_key = person['pkey']
357             # new person
358             if not user_record:
359                 (pubkey, pkey) = self.init_person_key(person, iotlab_key)
360                 if pubkey is not None and pkey is not None :
361                     person_gid = \
362                     self.auth_hierarchy.create_gid(person_urn, \
363                     create_uuid(), pkey)
364                     if person['email']:
365                         self.logger.debug( "SLAB IMPORTER \
366                             PERSON EMAIL OK email %s " %(person['email']))
367                         person_gid.set_email(person['email'])
368                         user_record = RegUser(hrn=person_hrn, \
369                                             gid=person_gid,
370                                             pointer='-1',
371                                             authority=get_authority(person_hrn),
372                                             email=person['email'])
373                     else:
374                         user_record = RegUser(hrn=person_hrn, \
375                                             gid=person_gid,
376                                             pointer='-1',
377                                             authority=get_authority(person_hrn))
378
379                     if pubkey:
380                         user_record.reg_keys = [RegKey(pubkey)]
381                     else:
382                         self.logger.warning("No key found for user %s" \
383                         %(user_record))
384
385                         try:
386                             user_record.just_created()
387                             dbsession.add (user_record)
388                             dbsession.commit()
389                             self.logger.info("IotlabImporter: imported person %s"\
390                             %(user_record))
391                             self.update_just_added_records_dict( user_record )
392
393                         except SQLAlchemyError:
394                             self.logger.log_exc("IotlabImporter: \
395                                 failed to import person  %s"%(person))
396             else:
397                 # update the record ?
398                 # if user's primary key has changed then we need to update
399                 # the users gid by forcing an update here
400                 sfa_keys = user_record.reg_keys
401
402                 new_key = False
403                 if iotlab_key is not sfa_keys :
404                     new_key = True
405                 if new_key:
406                     self.logger.info("IotlabImporter: \t \t USER UPDATE \
407                         person: %s" %(person['hrn']))
408                     (pubkey, pkey) = self.init_person_key (person, iotlab_key)
409                     person_gid = \
410                         self.auth_hierarchy.create_gid(person_urn, \
411                         create_uuid(), pkey)
412                     if not pubkey:
413                         user_record.reg_keys = []
414                     else:
415                         user_record.reg_keys = [RegKey(pubkey)]
416                     self.logger.info("IotlabImporter: updated person: %s" \
417                     % (user_record))
418
419                 if person['email']:
420                     user_record.email = person['email']
421
422             try:
423                 dbsession.commit()
424                 user_record.stale = False
425             except SQLAlchemyError:
426                 self.logger.log_exc("IotlabImporter: \
427                 failed to update person  %s"%(person))
428
429             self.import_slice(slice_hrn, slice_record, user_record)
430
431
432     def import_slice(self, slice_hrn, slice_record, user_record):
433         """
434
435          Create RegSlice record according to the slice hrn if the slice
436          does not exist yet.Creates a relationship with the user record
437          associated with the slice.
438          Commit the record to the database.
439
440
441         :param slice_hrn: Human readable name of the slice.
442         :type slice_hrn: string
443         :param slice_record: record of the slice found in the DB, if any.
444         :type slice_record: RegSlice or None
445         :param user_record: user record found in the DB if any.
446         :type user_record: RegUser
447
448         .. todo::Update the record if a slice record already exists.
449         """
450         if not slice_record :
451             pkey = Keypair(create=True)
452             urn = hrn_to_urn(slice_hrn, 'slice')
453             slice_gid = \
454                 self.auth_hierarchy.create_gid(urn, \
455                 create_uuid(), pkey)
456             slice_record = RegSlice (hrn=slice_hrn, gid=slice_gid,
457                                         pointer='-1',
458                                         authority=get_authority(slice_hrn))
459             try:
460                 slice_record.just_created()
461                 dbsession.add(slice_record)
462                 dbsession.commit()
463
464
465                 self.update_just_added_records_dict ( slice_record )
466
467             except SQLAlchemyError:
468                 self.logger.log_exc("IotlabImporter: failed to import slice")
469
470         #No slice update upon import in iotlab
471         else:
472             # xxx update the record ...
473             self.logger.warning ("Slice update not yet implemented")
474             pass
475         # record current users affiliated with the slice
476
477
478         slice_record.reg_researchers =  [user_record]
479         try:
480             dbsession.commit()
481             slice_record.stale = False
482         except SQLAlchemyError:
483             self.logger.log_exc("IotlabImporter: failed to update slice")
484
485
486     def run (self, options):
487         """
488         Create the special iotlab table, iotlab_xp, in the iotlab database.
489         Import everything (users, slices, nodes and sites from OAR
490         and LDAP) into the SFA database.
491         Delete stale records that are no longer in OAR or LDAP.
492         :param options:
493         :type options:
494         """
495         config = Config()
496
497         iotlabdriver = IotlabDriver(config)
498
499         #Create special slice table for iotlab
500
501         if not iotlabdriver.db.exists('iotlab_xp'):
502             iotlabdriver.db.createtable()
503             self.logger.info ("IotlabImporter.run:  iotlab_xp table created ")
504
505
506         # import site and node records in site into the SFA db.
507         self.import_sites_and_nodes(iotlabdriver)
508         #import users and slice into the SFA DB.
509         self.import_persons_and_slices(iotlabdriver)
510
511          ### remove stale records
512         # special records must be preserved
513         system_hrns = [iotlabdriver.hrn, iotlabdriver.iotlab_api.root_auth,  \
514                                         iotlabdriver.hrn+ '.slicemanager']
515         for record in self.all_records:
516             if record.hrn in system_hrns:
517                 record.stale = False
518             if record.peer_authority:
519                 record.stale = False
520
521
522         for record in self.all_records:
523             if record.type == 'user':
524                 self.logger.info("IotlabImporter: stale records: hrn %s %s" \
525                                             %(record.hrn,record.stale) )
526             try:
527                 stale = record.stale
528             except :
529                 stale = True
530                 self.logger.warning("stale not found with %s"%record)
531             if stale:
532                 self.logger.info("IotlabImporter: deleting stale record: %s" \
533                 %(record))
534
535                 try:
536                     dbsession.delete(record)
537                     dbsession.commit()
538                 except SQLAlchemyError:
539                     self.logger.log_exc("IotlabImporter: failed to delete stale \
540                     record %s" %(record) )
541
542