1 from sfa.util.xrn import get_authority, urn_to_hrn
2 from sfa.util.sfalogging import logger
9 rspec_to_slice_tag = {'max_rate':'net_max_rate'}
12 def __init__(self, driver):
16 def get_peer(self, xrn):
17 hrn, hrn_type = urn_to_hrn(xrn)
18 #Does this slice belong to a local site or a peer senslab site?
21 # get this slice's authority (site)
22 slice_authority = get_authority(hrn)
24 #This slice belongs to the current site
25 if slice_authority == self.driver.slab_api.root_auth:
26 site_authority = slice_authority
29 site_authority = get_authority(slice_authority).lower()
30 # get this site's authority (sfa root authority or sub authority)
32 logger.debug("SLABSLICES \ get_peer slice_authority %s \
33 site_authority %s hrn %s" %(slice_authority, \
37 # check if we are already peered with this site_authority, if so
38 #peers = self.driver.slab_api.GetPeers({})
39 peers = self.driver.slab_api.GetPeers(peer_filter = site_authority)
40 for peer_record in peers:
42 if site_authority == peer_record.hrn:
44 logger.debug(" SLABSLICES \tget_peer peer %s " %(peer))
47 def get_sfa_peer(self, xrn):
48 hrn, hrn_type = urn_to_hrn(xrn)
50 # return the authority for this hrn or None if we are the authority
52 slice_authority = get_authority(hrn)
53 site_authority = get_authority(slice_authority)
55 if site_authority != self.driver.hrn:
56 sfa_peer = site_authority
61 def verify_slice_leases(self, sfa_slice, requested_jobs_dict, peer):
63 Compare requested leases with the leases already scheduled/
64 running in OAR. If necessary, delete and recreate modified leases,
65 and delete no longer requested ones.
67 :param sfa_slice: sfa slice record
68 :param requested_jobs_dict: dictionary of requested leases
72 :type requested_jobs_dict: dict
74 :return: leases list of dictionary
79 logger.debug("SLABSLICES verify_slice_leases sfa_slice %s \
81 #First get the list of current leases from OAR
82 leases = self.driver.slab_api.GetLeases({'name':sfa_slice['hrn']})
83 logger.debug("SLABSLICES verify_slice_leases requested_jobs_dict %s \
84 leases %s "%(requested_jobs_dict, leases ))
86 current_nodes_reserved_by_start_time = {}
87 requested_nodes_by_start_time = {}
88 leases_by_start_time = {}
89 reschedule_jobs_dict = {}
92 #Create reduced dictionary with key start_time and value
94 #-for the leases already registered by OAR first
95 # then for the new leases requested by the user
97 #Leases already scheduled/running in OAR
99 current_nodes_reserved_by_start_time[lease['t_from']] = \
100 lease['reserved_nodes']
101 leases_by_start_time[lease['t_from']] = lease
105 for start_time in requested_jobs_dict:
106 requested_nodes_by_start_time[int(start_time)] = \
107 requested_jobs_dict[start_time]['hostname']
108 #Check if there is any difference between the leases already
109 #registered in OAR and the requested jobs.
110 #Difference could be:
111 #-Lease deleted in the requested jobs
112 #-Added/removed nodes
115 logger.debug("SLABSLICES verify_slice_leases \
116 requested_nodes_by_start_time %s \
117 "%(requested_nodes_by_start_time ))
118 #Find all deleted leases
120 list(set(leases_by_start_time.keys()).\
121 difference(requested_nodes_by_start_time.keys()))
122 deleted_leases = [leases_by_start_time[start_time]['lease_id'] \
123 for start_time in start_time_list]
127 #Find added or removed nodes in exisiting leases
128 for start_time in requested_nodes_by_start_time:
129 logger.debug("SLABSLICES verify_slice_leases start_time %s \
131 if start_time in current_nodes_reserved_by_start_time:
133 if requested_nodes_by_start_time[start_time] == \
134 current_nodes_reserved_by_start_time[start_time]:
139 set(requested_nodes_by_start_time[start_time])
141 update_node_set.difference(\
142 current_nodes_reserved_by_start_time[start_time])
144 update_node_set.intersection(\
145 current_nodes_reserved_by_start_time[start_time])
148 current_nodes_reserved_by_start_time[start_time])
150 old_nodes_set.difference(\
151 requested_nodes_by_start_time[start_time])
152 logger.debug("SLABSLICES verify_slice_leases \
153 shared_nodes %s added_nodes %s removed_nodes %s"\
154 %(shared_nodes, added_nodes,removed_nodes ))
155 #If the lease is modified, delete it before
157 #Add the deleted lease job id in the list
158 #WARNING :rescheduling does not work if there is already
159 # 2 running/scheduled jobs because deleting a job
160 #takes time SA 18/10/2012
161 if added_nodes or removed_nodes:
162 deleted_leases.append(\
163 leases_by_start_time[start_time]['lease_id'])
165 if added_nodes or shared_nodes:
166 reschedule_jobs_dict[str(start_time)] = \
167 requested_jobs_dict[str(start_time)]
172 job = requested_jobs_dict[str(start_time)]
173 logger.debug("SLABSLICES \
174 NEWLEASE slice %s job %s"\
176 self.driver.slab_api.AddLeases(job['hostname'], \
177 sfa_slice, int(job['start_time']), \
178 int(job['duration']))
180 #Deleted leases are the ones with lease id not declared in the Rspec
182 self.driver.slab_api.DeleteLeases(deleted_leases, sfa_slice['hrn'])
183 logger.debug("SLABSLICES \
184 verify_slice_leases slice %s deleted_leases %s"\
185 %(sfa_slice, deleted_leases))
188 if reschedule_jobs_dict :
189 for start_time in reschedule_jobs_dict:
190 job = reschedule_jobs_dict[start_time]
191 self.driver.slab_api.AddLeases(job['hostname'], \
192 sfa_slice, int(job['start_time']), \
193 int(job['duration']))
196 def verify_slice_nodes(self, sfa_slice, requested_slivers, peer):
200 if 'node_ids' in sfa_slice:
201 nodes = self.driver.slab_api.GetNodes(sfa_slice['list_node_ids'], \
203 current_slivers = [node['hostname'] for node in nodes]
205 # remove nodes not in rspec
206 deleted_nodes = list(set(current_slivers).\
207 difference(requested_slivers))
208 # add nodes from rspec
209 #added_nodes = list(set(requested_slivers).\
210 #difference(current_slivers))
213 logger.debug("SLABSLICES \tverify_slice_nodes slice %s\
214 \r\n \r\n deleted_nodes %s"\
215 %(sfa_slice, deleted_nodes))
218 #Delete the entire experience
219 self.driver.slab_api.DeleteSliceFromNodes(sfa_slice)
220 #self.driver.DeleteSliceFromNodes(sfa_slice['slice_hrn'], \
226 def free_egre_key(self):
228 for tag in self.driver.slab_api.GetSliceTags({'tagname': 'egre_key'}):
229 used.add(int(tag['value']))
231 for i in range(1, 256):
236 raise KeyError("No more EGRE keys available")
245 def handle_peer(self, site, sfa_slice, persons, peer):
250 self.driver.slab_api.BindObjectToPeer('site', site['site_id'], \
251 peer['shortname'], sfa_slice['site_id'])
252 except Exception, error:
253 self.driver.slab_api.DeleteSite(site['site_id'])
259 self.driver.slab_api.BindObjectToPeer('slice', slice['slice_id'], \
260 peer['shortname'], sfa_slice['slice_id'])
261 except Exception, error:
262 self.driver.slab_api.DeleteSlice(sfa_slice['slice_id'])
266 for person in persons:
268 self.driver.slab_api.BindObjectToPeer('person', \
269 person['person_id'], peer['shortname'], \
270 person['peer_person_id'])
272 for (key, remote_key_id) in zip(person['keys'], \
275 self.driver.slab_api.BindObjectToPeer( 'key', \
276 key['key_id'], peer['shortname'], \
279 self.driver.slab_api.DeleteKey(key['key_id'])
280 logger.log_exc("failed to bind key: %s \
281 to peer: %s " % (key['key_id'], \
283 except Exception, error:
284 self.driver.slab_api.DeletePerson(person['person_id'])
289 #def verify_site(self, slice_xrn, slice_record={}, peer=None, \
290 #sfa_peer=None, options={}):
291 #(slice_hrn, type) = urn_to_hrn(slice_xrn)
292 #site_hrn = get_authority(slice_hrn)
293 ## login base can't be longer than 20 characters
294 ##slicename = hrn_to_pl_slicename(slice_hrn)
295 #authority_name = slice_hrn.split('.')[0]
296 #login_base = authority_name[:20]
297 #logger.debug(" SLABSLICES.PY \tverify_site authority_name %s \
298 #login_base %s slice_hrn %s" \
299 #%(authority_name,login_base,slice_hrn)
301 #sites = self.driver.slab_api.GetSites(login_base)
303 ## create new site record
304 #site = {'name': 'geni.%s' % authority_name,
305 #'abbreviated_name': authority_name,
306 #'login_base': login_base,
308 #'max_slivers': 1000,
310 #'peer_site_id': None}
312 #site['peer_site_id'] = slice_record.get('site_id', None)
313 #site['site_id'] = self.driver.slab_api.AddSite(site)
314 ## exempt federated sites from monitor policies
315 #self.driver.slab_api.AddSiteTag(site['site_id'], 'exempt_site_until', \
318 ### is this still necessary?
319 ### add record to the local registry
320 ##if sfa_peer and slice_record:
321 ##peer_dict = {'type': 'authority', 'hrn': site_hrn, \
322 ##'peer_authority': sfa_peer, 'pointer': \
324 ##self.registry.register_peer_object(self.credential, peer_dict)
328 ## unbind from peer so we can modify if necessary.
329 ## Will bind back later
330 #self.driver.slab_api.UnBindObjectFromPeer('site', site['site_id'], \
335 def verify_slice(self, slice_hrn, slice_record, peer, sfa_peer):
337 #login_base = slice_hrn.split(".")[0]
338 slicename = slice_hrn
339 slices_list = self.driver.slab_api.GetSlices(slice_filter = slicename, \
340 slice_filter_type = 'slice_hrn')
343 for sl in slices_list:
345 logger.debug("SLABSLICE \tverify_slice slicename %s slices_list %s sl %s \
346 slice_record %s"%(slicename, slices_list,sl, \
349 sfa_slice.update(slice_record)
350 #del slice['last_updated']
351 #del slice['date_created']
353 #slice['peer_slice_id'] = slice_record.get('slice_id', None)
354 ## unbind from peer so we can modify if necessary.
355 ## Will bind back later
356 #self.driver.slab_api.UnBindObjectFromPeer('slice', \
357 #slice['slice_id'], \
359 #Update existing record (e.g. expires field)
360 #it with the latest info.
361 ##if slice_record and slice['expires'] != slice_record['expires']:
362 ##self.driver.slab_api.UpdateSlice( slice['slice_id'], {'expires' : \
363 #slice_record['expires']})
365 #Search for user in ldap based on email SA 14/11/12
366 ldap_user = self.driver.slab_api.ldap.LdapFindUser(slice_record['user'])
367 logger.debug(" SLABSLICES \tverify_slice Oups \
368 slice_record %s peer %s sfa_peer %s ldap_user %s"\
369 %(slice_record, peer,sfa_peer ,ldap_user ))
370 #User already registered in ldap, meaning user should be in SFA db
371 #and hrn = sfa_auth+ uid
372 sfa_slice = {'hrn': slicename,
373 #'url': slice_record.get('url', slice_hrn),
374 #'description': slice_record.get('description', slice_hrn)
376 'authority' : slice_record['authority'],
377 'gid':slice_record['gid'],
378 #'record_id_user' : user.record_id,
379 'slice_id' : slice_record['record_id'],
380 'reg-researchers':slice_record['reg-researchers'],
381 #'record_id_slice': slice_record['record_id'],
382 'peer_authority':str(sfa_peer)
386 hrn = self.driver.slab_api.root_auth +'.'+ ldap_user['uid']
388 user = self.driver.get_user_record(hrn)
390 logger.debug(" SLABSLICES \tverify_slice hrn %s USER %s" %(hrn, user))
391 #sfa_slice = {'slice_hrn': slicename,
392 ##'url': slice_record.get('url', slice_hrn),
393 ##'description': slice_record.get('description', slice_hrn)
395 #'authority' : slice_record['authority'],
396 #'gid':slice_record['gid'],
397 ##'record_id_user' : user.record_id,
398 #'slice_id' : slice_record['record_id'],
399 #'reg-researchers':slice_record['reg-researchers'],
400 ##'record_id_slice': slice_record['record_id'],
401 #'peer_authority':str(peer.hrn)
406 self.driver.slab_api.AddSlice(sfa_slice, user)
409 sfa_slice['slice_id'] = slice_record['record_id']
411 #slice['slice_id'] = self.driver.slab_api.AddSlice(slice)
412 logger.debug("SLABSLICES \tverify_slice ADDSLICE OK")
413 #slice['node_ids']=[]
414 #slice['person_ids'] = []
416 #sfa_slice['peer_slice_id'] = slice_record.get('slice_id', None)
417 # mark this slice as an sfa peer record
419 #peer_dict = {'type': 'slice', 'hrn': slice_hrn,
420 #'peer_authority': sfa_peer, 'pointer': \
422 #self.registry.register_peer_object(self.credential, peer_dict)
429 def verify_persons(self, slice_hrn, slice_record, users, peer, sfa_peer, \
432 users is a record list. Records can either be local records
433 or users records from known and trusted federated sites.
434 If the user is from another site that senslab doesn't trust yet,
435 then Resolve will raise an error before getting to create_sliver.
437 #TODO SA 21/08/12 verify_persons Needs review
439 logger.debug("SLABSLICES \tverify_persons \tslice_hrn %s \t slice_record %s\r\n users %s \t peer %s "%( slice_hrn, slice_record, users, peer))
443 #users_dict : dict whose keys can either be the user's hrn or its id.
444 #Values contains only id and hrn
447 #First create dicts by hrn and id for each user in the user record list:
450 if 'slice_record' in info :
451 slice_rec = info['slice_record']
452 user = slice_rec['user']
455 users_by_email[user['email']] = user
456 users_dict[user['email']] = user
459 #users_by_hrn[user['hrn']] = user
460 #users_dict[user['hrn']] = user
462 logger.debug( "SLABSLICE.PY \t verify_person \
463 users_dict %s \r\n user_by_email %s \r\n \
465 %(users_dict,users_by_email, users_by_id))
467 existing_user_ids = []
468 #existing_user_hrns = []
469 existing_user_emails = []
471 # Check if user is in Senslab LDAP using its hrn.
472 # Assuming Senslab is centralised : one LDAP for all sites,
473 # user_id unknown from LDAP
474 # LDAP does not provide users id, therefore we rely on hrns containing
475 # the login of the user.
476 # If the hrn is not a senslab hrn, the user may not be in LDAP.
479 #Construct the list of filters (list of dicts) for GetPersons
481 #for hrn in users_by_hrn:
482 for email in users_by_email :
483 #filter_user.append (users_by_hrn[hrn])
484 filter_user.append (users_by_email[email])
485 #Check user's in LDAP with GetPersons
486 #Needed because what if the user has been deleted in LDAP but
488 existing_users = self.driver.slab_api.GetPersons(filter_user)
489 logger.debug(" \r\n SLABSLICE.PY \tverify_person filter_user %s existing_users %s " \
490 %(filter_user, existing_users))
491 #User's in senslab LDAP
493 for user in existing_users :
494 users_dict[user['email']].update(user)
495 existing_user_emails.append(users_dict[user['email']]['email'])
497 #existing_user_hrns.append(users_dict[user['hrn']]['hrn'])
499 #append(users_dict[user['hrn']]['person_id'])
501 # User from another known trusted federated site. Check
502 # if a senslab account matching the email has already been created.
505 if isinstance(users, list):
507 req += users[0]['email']
509 req += users['email']
511 ldap_reslt = self.driver.slab_api.ldap.LdapSearch(req)
513 logger.debug(" SLABSLICE.PY \tverify_person users \
514 USER already in Senslab \t ldap_reslt %s \
516 existing_users.append(ldap_reslt[1])
519 #User not existing in LDAP
520 #TODO SA 21/08/12 raise smthg to add user or add it auto ?
522 #new_record['pkey'] = users[0]['keys'][0]
523 #new_record['mail'] = users[0]['email']
525 logger.debug(" SLABSLICE.PY \tverify_person users \
526 not in ldap ...NEW ACCOUNT NEEDED %s \r\n \t \
527 ldap_reslt %s " %(users, ldap_reslt))
529 #requested_user_ids = users_by_id.keys()
530 #requested_user_hrns = users_by_hrn.keys()
531 requested_user_emails = users_by_email.keys()
532 requested_user_hrns = [users_by_email[user]['hrn'] for user in users_by_email]
533 logger.debug("SLABSLICE.PY \tverify_person \
534 users_by_email %s " %( users_by_email))
535 #logger.debug("SLABSLICE.PY \tverify_person \
536 #user_by_hrn %s " %( users_by_hrn))
539 #Check that the user of the slice in the slice record
540 #matches the existing users
542 if slice_record['PI'][0] in requested_user_hrns:
543 #if slice_record['record_id_user'] in requested_user_ids and \
544 #slice_record['PI'][0] in requested_user_hrns:
545 logger.debug(" SLABSLICE \tverify_person ['PI'] slice_record %s" \
552 # users to be added, removed or updated
553 #One user in one senslab slice : there should be no need
554 #to remove/ add any user from/to a slice.
555 #However a user from SFA which is not registered in Senslab yet
556 #should be added to the LDAP.
557 added_user_emails = set(requested_user_emails).\
558 difference(set(existing_user_emails))
559 #added_user_hrns = set(requested_user_hrns).\
560 #difference(set(existing_user_hrns))
562 #self.verify_keys(existing_slice_users, updated_users_list, \
568 #requested_user_email is in existing_user_emails
569 if len(added_user_emails) == 0:
571 slice_record['login'] = users_dict[requested_user_emails[0]]['uid']
572 logger.debug(" SLABSLICE \tverify_person QUICK DIRTY %s" \
575 #for added_user_hrn in added_user_hrns:
576 #added_user = users_dict[added_user_hrn]
579 for added_user_email in added_user_emails:
580 #hrn, type = urn_to_hrn(added_user['urn'])
581 added_user = users_dict[added_user_email]
582 logger.debug(" SLABSLICE \r\n \r\n \t THE SECOND verify_person added_user %s" %(added_user))
584 person['peer_person_id'] = None
585 k_list = ['first_name','last_name','person_id']
588 person[k] = added_user[k]
590 person['pkey'] = added_user['keys'][0]
591 person['mail'] = added_user['email']
592 person['email'] = added_user['email']
593 person['key_ids'] = added_user.get('key_ids', [])
594 #person['urn'] = added_user['urn']
596 #person['person_id'] = self.driver.slab_api.AddPerson(person)
597 person['uid'] = self.driver.slab_api.AddPerson(person)
599 logger.debug(" SLABSLICE \r\n \r\n \t THE SECOND verify_person ppeersonne %s" %(person))
600 #Update slice_Record with the id now known to LDAP
601 slice_record['login'] = person['uid']
602 #slice_record['reg_researchers'] = [self.driver.slab_api.root_auth + '.' + person['uid']]
603 #slice_record['reg-researchers'] = slice_record['reg_researchers']
606 #person['peer_person_id'] = added_user['person_id']
607 added_persons.append(person)
610 #self.driver.slab_api.UpdatePerson(slice_record['reg_researchers'][0], added_user_email)
613 #self.driver.slab_api.AddPersonToSite(added_user_id, login_base)
615 #for key_string in added_user.get('keys', []):
616 #key = {'key':key_string, 'key_type':'ssh'}
617 #key['key_id'] = self.driver.slab_api.AddPersonKey(person['person_id'], \
619 #person['keys'].append(key)
621 # add the registry record
623 #peer_dict = {'type': 'user', 'hrn': hrn, 'peer_authority': \
625 #'pointer': person['person_id']}
626 #self.registry.register_peer_object(self.credential, peer_dict)
627 #for added_slice_user_hrn in \
628 #added_slice_user_hrns.union(added_user_hrns):
629 #self.driver.slab_api.AddPersonToSlice(added_slice_user_hrn, \
630 #slice_record['name'])
631 #for added_slice_user_id in \
632 #added_slice_user_ids.union(added_user_ids):
633 # add person to the slice
634 #self.driver.slab_api.AddPersonToSlice(added_slice_user_id, \
635 #slice_record['name'])
636 # if this is a peer record then it
637 # should already be bound to a peer.
638 # no need to return worry about it getting bound later
643 def verify_keys(self, persons, users, peer, options={}):
646 for person in persons:
647 key_ids.extend(person['key_ids'])
648 keylist = self.driver.slab_api.GetKeys(key_ids, ['key_id', 'key'])
651 keydict[key['key']] = key['key_id']
652 existing_keys = keydict.keys()
654 for person in persons:
655 persondict[person['email']] = person
661 user_keys = user.get('keys', [])
662 updated_persons.append(user)
663 for key_string in user_keys:
664 requested_keys.append(key_string)
665 if key_string not in existing_keys:
666 key = {'key': key_string, 'key_type': 'ssh'}
669 person = persondict[user['email']]
670 self.driver.slab_api.UnBindObjectFromPeer('person', \
671 person['person_id'], peer['shortname'])
673 self.driver.slab_api.AddPersonKey(user['email'], key)
675 key_index = user_keys.index(key['key'])
676 remote_key_id = user['key_ids'][key_index]
677 self.driver.slab_api.BindObjectToPeer('key', \
678 key['key_id'], peer['shortname'], \
683 self.driver.slab_api.BindObjectToPeer('person', \
684 person['person_id'], peer['shortname'], \
687 # remove old keys (only if we are not appending)
688 append = options.get('append', True)
690 removed_keys = set(existing_keys).difference(requested_keys)
691 for existing_key_id in keydict:
692 if keydict[existing_key_id] in removed_keys:
695 self.driver.slab_api.UnBindObjectFromPeer('key', \
696 existing_key_id, peer['shortname'])
697 self.driver.slab_api.DeleteKey(existing_key_id)
700 #def verify_slice_attributes(self, slice, requested_slice_attributes, \
701 #append=False, admin=False):
702 ## get list of attributes users ar able to manage
703 #filter = {'category': '*slice*'}
705 #filter['|roles'] = ['user']
706 #slice_attributes = self.driver.slab_api.GetTagTypes(filter)
707 #valid_slice_attribute_names = [attribute['tagname'] \
708 #for attribute in slice_attributes]
710 ## get sliver attributes
711 #added_slice_attributes = []
712 #removed_slice_attributes = []
713 #ignored_slice_attribute_names = []
714 #existing_slice_attributes = self.driver.slab_api.GetSliceTags({'slice_id': \
717 ## get attributes that should be removed
718 #for slice_tag in existing_slice_attributes:
719 #if slice_tag['tagname'] in ignored_slice_attribute_names:
720 ## If a slice already has a admin only role
721 ## it was probably given to them by an
722 ## admin, so we should ignore it.
723 #ignored_slice_attribute_names.append(slice_tag['tagname'])
725 ## If an existing slice attribute was not
726 ## found in the request it should
728 #attribute_found=False
729 #for requested_attribute in requested_slice_attributes:
730 #if requested_attribute['name'] == slice_tag['tagname'] \
731 #and requested_attribute['value'] == slice_tag['value']:
732 #attribute_found=True
735 #if not attribute_found and not append:
736 #removed_slice_attributes.append(slice_tag)
738 ## get attributes that should be added:
739 #for requested_attribute in requested_slice_attributes:
740 ## if the requested attribute wasn't found we should add it
741 #if requested_attribute['name'] in valid_slice_attribute_names:
742 #attribute_found = False
743 #for existing_attribute in existing_slice_attributes:
744 #if requested_attribute['name'] == \
745 #existing_attribute['tagname'] and \
746 #requested_attribute['value'] == \
747 #existing_attribute['value']:
748 #attribute_found=True
750 #if not attribute_found:
751 #added_slice_attributes.append(requested_attribute)
754 ## remove stale attributes
755 #for attribute in removed_slice_attributes:
757 #self.driver.slab_api.DeleteSliceTag(attribute['slice_tag_id'])
758 #except Exception, error:
759 #self.logger.warn('Failed to remove sliver attribute. name: \
760 #%s, value: %s, node_id: %s\nCause:%s'\
761 #% (name, value, node_id, str(error)))
763 ## add requested_attributes
764 #for attribute in added_slice_attributes:
766 #self.driver.slab_api.AddSliceTag(slice['name'], attribute['name'], \
767 #attribute['value'], attribute.get('node_id', None))
768 #except Exception, error:
769 #self.logger.warn('Failed to add sliver attribute. name: %s, \
770 #value: %s, node_id: %s\nCause:%s'\
771 #% (name, value, node_id, str(error)))