1# -*- coding: utf-8 -*- 2''' 3keeping.py raet protocol keep classes 4''' 5# pylint: skip-file 6# pylint: disable=W0611 7 8# Import python libs 9import os 10from collections import deque 11 12try: 13 import simplejson as json 14except ImportError: 15 import json 16 17# Import ioflo libs 18from ioflo.aid.odicting import odict 19 20# Import raet libs 21from ..abiding import * # import globals 22from .. import raeting 23from ..raeting import AutoMode, Acceptance 24from .. import nacling 25from .. import keeping 26 27from ioflo.base.consoling import getConsole 28console = getConsole() 29 30class RoadKeep(keeping.Keep): 31 ''' 32 RAET protocol estate on road data persistence for a given estate 33 road specific data but not key data 34 35 keep/ 36 stackname/ 37 local/ 38 estate.ext 39 role.ext 40 remote/ 41 estate.name.ext 42 estate.name.ext 43 role/ 44 role.role.ext 45 role.role.ext 46 ''' 47 LocalFields = ['name', 'uid', 'ha', 'iha', 'natted', 'fqdn', 'dyned', 'sid', 48 'puid', 'aha', 'role', 'sighex','prihex'] 49 LocalDumpFields = ['name', 'uid', 'ha', 'iha', 'natted', 'fqdn', 'dyned', 'sid', 50 'puid', 'aha', 'role'] 51 LocalRoleFields = ['role', 'sighex','prihex'] 52 RemoteFields = ['name', 'uid', 'fuid', 'ha', 'iha', 'natted', 'fqdn', 'dyned', 53 'sid', 'main', 'kind', 'joined', 54 'role', 'acceptance', 'verhex', 'pubhex'] 55 RemoteDumpFields = ['name', 'uid', 'fuid', 'ha', 'iha', 'natted', 'fqdn', 'dyned', 56 'sid', 'main', 'kind', 'joined', 'role'] 57 RemoteRoleFields = ['role', 'acceptance', 'verhex', 'pubhex'] 58 Auto = AutoMode.never.value #auto accept 59 60 def __init__(self, 61 stackname='stack', 62 prefix='estate', 63 auto=None, 64 baseroledirpath='', 65 roledirpath='', 66 **kwa): 67 ''' 68 Setup RoadKeep instance 69 ''' 70 super(RoadKeep, self).__init__(stackname=stackname, 71 prefix=prefix, 72 **kwa) 73 self.auto = auto if auto is not None else self.Auto 74 75 if not roledirpath: 76 if baseroledirpath: 77 roledirpath = os.path.join(baseroledirpath, stackname, 'role') 78 else: 79 roledirpath = os.path.join(self.dirpath, 'role') 80 roledirpath = os.path.abspath(os.path.expanduser(roledirpath)) 81 82 if not os.path.exists(roledirpath): 83 try: 84 os.makedirs(roledirpath) 85 except OSError as ex: 86 roledirpath = os.path.join(self.AltKeepDir, stackname, 'role') 87 roledirpath = os.path.abspath(os.path.expanduser(roledirpath)) 88 if not os.path.exists(roledirpath): 89 os.makedirs(roledirpath) 90 else: 91 if not os.access(roledirpath, os.R_OK | os.W_OK): 92 roledirpath = os.path.join(self.AltKeepDir, stackname, 'role') 93 roledirpath = os.path.abspath(os.path.expanduser(roledirpath)) 94 if not os.path.exists(roledirpath): 95 os.makedirs(roledirpath) 96 97 self.roledirpath = roledirpath 98 99 remoteroledirpath = os.path.join(self.roledirpath, 'remote') 100 if not os.path.exists(remoteroledirpath): 101 os.makedirs(remoteroledirpath) 102 self.remoteroledirpath = remoteroledirpath 103 104 localroledirpath = os.path.join(self.roledirpath, 'local') 105 if not os.path.exists(localroledirpath): 106 os.makedirs(localroledirpath) 107 self.localroledirpath = localroledirpath 108 109 self.localrolepath = os.path.join(self.localroledirpath, 110 "{0}.{1}".format('role', self.ext)) 111 112 def clearAllDir(self): 113 ''' 114 Clear all keep directories 115 ''' 116 super(RoadKeep, self).clearAllDir() 117 self.clearRoleDir() 118 119 def clearRoleDir(self): 120 ''' 121 Clear the Role directory 122 ''' 123 if os.path.exists(self.roledirpath): 124 os.rmdir(self.roledirpath) 125 126 def dumpLocalRoleData(self, data): 127 ''' 128 Dump the local role data to file 129 ''' 130 self.dump(data, self.localrolepath) 131 132 def loadLocalRoleData(self): 133 ''' 134 Load and Return the role data from the localrolefile 135 ''' 136 data = odict([(key, None) for key in self.LocalRoleFields]) 137 if not os.path.exists(self.localrolepath): 138 return data 139 data.update(self.load(self.localrolepath)) 140 return data 141 142 def clearLocalRoleData(self): 143 ''' 144 Clear the local file 145 ''' 146 if os.path.exists(self.localrolepath): 147 os.remove(self.localrolepath) 148 149 def clearLocalRoleDir(self): 150 ''' 151 Clear the Local Role directory 152 ''' 153 if os.path.exists(self.localroledirpath): 154 os.rmdir(self.localroledirpath) 155 156 def dumpRemoteRoleData(self, data, role): 157 ''' 158 Dump the role data to file 159 ''' 160 filepath = os.path.join(self.remoteroledirpath, 161 "{0}.{1}.{2}".format('role', role, self.ext)) 162 163 self.dump(data, filepath) 164 165 def dumpAllRemoteRoleData(self, roles): 166 ''' 167 Dump the data in the roles keyed by role to role data files 168 ''' 169 for role, data in roles.items(): 170 self.dumpRemoteRoleData(data, role) 171 172 def loadRemoteRoleData(self, role): 173 ''' 174 Load and Return the data from the role file 175 ''' 176 data = odict([(key, None) for key in self.RemoteRoleFields]) 177 filepath = os.path.join(self.remoteroledirpath, 178 "{0}.{1}.{2}".format('role', role, self.ext)) 179 if not os.path.exists(filepath): 180 data.update(role=role) 181 return data 182 data.update(self.load(filepath)) 183 return data 184 185 def loadAllRemoteRoleData(self): 186 ''' 187 Load and Return the roles dict from the all the role data files 188 indexed by role in filenames 189 ''' 190 roles = odict() 191 for filename in os.listdir(self.remoteroledirpath): 192 root, ext = os.path.splitext(filename) 193 if ext not in ['.json', '.msgpack']: 194 continue 195 prefix, sep, role = root.partition('.') 196 if not role or prefix != 'role': 197 continue 198 filepath = os.path.join(self.remoteroledirpath, filename) 199 roles[role] = self.load(filepath) 200 return roles 201 202 def clearRemoteRoleData(self, role): 203 ''' 204 Clear data from the role data file 205 ''' 206 filepath = os.path.join(self.remoteroledirpath, 207 "{0}.{1}.{2}".format('role', role, self.ext)) 208 if os.path.exists(filepath): 209 os.remove(filepath) 210 211 def clearAllRemoteRoleData(self): 212 ''' 213 Remove all the role data files 214 ''' 215 for filename in os.listdir(self.remoteroledirpath): 216 root, ext = os.path.splitext(filename) 217 if ext not in ['.json', '.msgpack']: 218 continue 219 prefix, sep, role = root.partition('.') 220 if not role or prefix != 'role': 221 continue 222 filepath = os.path.join(self.remoteroledirpath, filename) 223 if os.path.exists(filepath): 224 os.remove(filepath) 225 226 def clearRemoteRoleDir(self): 227 ''' 228 Clear the Remote Role directory 229 ''' 230 if os.path.exists(self.remoteroledirpath): 231 os.rmdir(self.remoteroledirpath) 232 233 def loadLocalData(self): 234 ''' 235 Load and Return the data from the local estate 236 ''' 237 238 data = super(RoadKeep, self).loadLocalData() 239 if not data: 240 return None 241 roleData = self.loadLocalRoleData() # if not present defaults None values 242 data.update([('sighex', roleData.get('sighex')), 243 ('prihex', roleData.get('prihex'))]) 244 return data 245 246 def loadRemoteData(self, name): 247 ''' 248 Load and Return the data from the remote file 249 ''' 250 data = super(RoadKeep, self).loadRemoteData(name) 251 if not data: 252 return None 253 254 role = data['role'] 255 roleData = self.loadRemoteRoleData(role) # if not found defaults to None values 256 257 data.update(acceptance=roleData.get('acceptance'), 258 verhex=roleData.get('verhex'), 259 pubhex=roleData.get('pubhex')) 260 return data 261 262 def loadAllRemoteData(self): 263 ''' 264 Load and Return the data from the all the remote estate files 265 ''' 266 keeps = super(RoadKeep, self).loadAllRemoteData() 267 roles = self.loadAllRemoteRoleData() 268 for name, data in keeps.items(): 269 role = data['role'] 270 roleData = roles.get(role, odict([('acceptance', None), 271 ('verhex', None), 272 ('pubhex', None)]) ) 273 keeps[name].update([('acceptance', roleData['acceptance']), 274 ('verhex', roleData['verhex']), 275 ('pubhex', roleData['pubhex'])]) 276 return keeps 277 278 def dumpLocalRole(self, local): 279 ''' 280 Dump role data for local 281 ''' 282 data = odict([ 283 ('role', local.role), 284 ('sighex', local.signer.keyhex), 285 ('prihex', local.priver.keyhex), 286 ]) 287 if self.verifyLocalData(data, localFields=self.LocalRoleFields): 288 self.dumpLocalRoleData(data) 289 290 def dumpLocal(self, local): 291 ''' 292 Dump local estate 293 ''' 294 data = odict([ 295 ('name', local.name), 296 ('uid', local.uid), 297 ('ha', local.ha), 298 ('iha', local.iha), 299 ('natted', local.natted), 300 ('fqdn', local.fqdn), 301 ('dyned', local.dyned), 302 ('sid', local.sid), 303 ('puid', local.stack.puid), 304 ('aha', local.stack.aha), 305 ('role', local.role), 306 ]) 307 if self.verifyLocalData(data, localFields =self.LocalDumpFields): 308 self.dumpLocalData(data) 309 310 self.dumpLocalRole(local) 311 312 def dumpRemoteRole(self, remote): 313 ''' 314 Dump the role data for remote 315 ''' 316 data = odict([ 317 ('role', remote.role), 318 ('acceptance', remote.acceptance), 319 ('verhex', str(remote.verfer.keyhex.decode('ISO-8859-1'))), 320 ('pubhex', str(remote.pubber.keyhex.decode('ISO-8859-1'))), 321 ]) 322 if self.verifyRemoteData(data, remoteFields=self.RemoteRoleFields): 323 self.dumpRemoteRoleData(data, remote.role) 324 325 def dumpRemote(self, remote): 326 ''' 327 Dump remote estate 328 ''' 329 data = odict([ 330 ('name', remote.name), 331 ('uid', remote.uid), 332 ('fuid', remote.fuid), 333 ('ha', remote.ha), 334 ('iha', remote.iha), 335 ('natted', remote.natted), 336 ('fqdn', remote.fqdn), 337 ('dyned', remote.dyned), 338 ('sid', remote.sid), 339 ('main', remote.main), 340 ('kind', remote.kind), 341 ('joined', remote.joined), 342 ('role', remote.role), 343 ]) 344 if self.verifyRemoteData(data, remoteFields=self.RemoteDumpFields): 345 self.dumpRemoteData(data, remote.name) 346 347 self.dumpRemoteRole(remote) 348 349 def statusRemote(self, remote, dump=True): 350 ''' 351 Calls .statusRole on remote role and keys and updates remote.acceptance 352 dump indicates if statusRole should update persisted values when 353 appropriate. 354 355 Returns status 356 Where status is acceptance status of role and keys 357 and has value from raeting.acceptances 358 ''' 359 status = self.statusRole(role=remote.role, 360 verhex=str(remote.verfer.keyhex.decode('ISO-8859-1')), 361 pubhex=str(remote.pubber.keyhex.decode('ISO-8859-1')), 362 dump=dump, ) 363 364 remote.acceptance = status 365 366 return status 367 368 def statusRole(self, role, verhex, pubhex, dump=True): 369 ''' 370 Returns status 371 372 Where status is acceptance status of role and keys 373 and has value from raeting.acceptances 374 375 If dump when appropriate 376 Then persist key data differentially based on status 377 378 In many cases a status of rejected is returned because the provided keys are 379 different but this does not change the acceptance status for 380 the persisted keys which keys were not changed. 381 382 Persisted status is only set to rejected by an outside entity. It is never 383 set to rejected by this function, that is, a status of rejected may be returned 384 but the persisted status on disk is not changed to rejected. 385 ''' 386 data = self.loadRemoteRoleData(role) 387 status = data.get('acceptance') if data else None # pre-existing status 388 389 if self.auto == AutoMode.always: 390 status = Acceptance.accepted.value 391 392 elif self.auto == AutoMode.once: 393 if status is None: # first time so accept once 394 status = Acceptance.accepted.value 395 396 elif status == Acceptance.accepted: 397 # already been accepted if keys not match then reject 398 if (data and ( 399 (verhex != data.get('verhex')) or 400 (pubhex != data.get('pubhex')) )): 401 status = Acceptance.rejected.value 402 console.concise("Rejection Reason: Once keys not match prior accepted.\n") 403 404 elif status == Acceptance.pending: 405 # already pending prior mode of never if keys not match then reject 406 if (data and ( 407 (verhex != data.get('verhex')) or 408 (pubhex != data.get('pubhex')) )): 409 status = Acceptance.rejected.value 410 console.concise("Rejection Reason: Once keys not match prior pended.\n") 411 else: # in once mode convert pending to accepted 412 status = Acceptance.accepted.value 413 else: 414 console.concise("Rejection Reason: Once keys already rejected.\n") 415 416 elif self.auto == AutoMode.never: 417 if status is None: # first time so pend 418 status = Acceptance.pending.value 419 420 elif status == Acceptance.accepted: 421 # already been accepted if keys not match then reject 422 if (data and ( 423 (verhex != data.get('verhex')) or 424 (pubhex != data.get('pubhex')) )): 425 status = Acceptance.rejected.value 426 console.concise("Rejection Reason: Never keys not match prior accepted.\n") 427 428 elif status == Acceptance.pending: 429 # already pending if keys not match then reject 430 if (data and ( 431 (verhex != data.get('verhex')) or 432 (pubhex != data.get('pubhex')) )): 433 status = Acceptance.rejected.value 434 console.concise("Rejection Reason: Never keys not match prior pended.\n") 435 else: 436 console.concise("Rejection Reason: Never keys already rejected.\n") 437 438 else: # unrecognized autoMode 439 raise raeting.KeepError("Unrecognized auto mode '{0}'".format(self.auto)) 440 441 if dump: 442 dirty = False 443 # update changed keys if any when accepted or pending 444 if status != Acceptance.rejected: 445 if (verhex and verhex != data.get('verhex')): 446 data['verhex'] = verhex 447 dirty = True 448 if (pubhex and pubhex != data.get('pubhex')): 449 data['pubhex'] = pubhex 450 dirty = True 451 if status != data.get('acceptance'): 452 data['acceptance'] = status 453 dirty = True 454 455 if dirty and self.verifyRemoteData(data, 456 remoteFields=self.RemoteRoleFields): 457 self.dumpRemoteRoleData(data, role) 458 459 return status 460 461 def rejectRemote(self, remote): 462 ''' 463 Set acceptance status to rejected 464 ''' 465 remote.acceptance = Acceptance.rejected.value 466 self.dumpRemoteRole(remote) 467 468 def pendRemote(self, remote): 469 ''' 470 Set acceptance status to pending 471 ''' 472 remote.acceptance = Acceptance.pending.value 473 self.dumpRemoteRole(remote) 474 475 def acceptRemote(self, remote): 476 ''' 477 Set acceptance status to accepted 478 ''' 479 remote.acceptance = Acceptance.accepted.value 480 self.dumpRemoteRole(remote) 481 482def clearAllKeep(dirpath): 483 ''' 484 Convenience function to clear all road keep data in dirpath 485 ''' 486 road = RoadKeep(dirpath=dirpath) 487 road.clearLocalData() 488 road.clearLocalRoleData() 489 road.clearAllRemoteData() 490 road.clearAllRemoteRoleData() 491 492