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