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