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