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