synapse 2.213.0__py311-none-any.whl → 2.215.0__py311-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synapse might be problematic. Click here for more details.

Files changed (76) hide show
  1. synapse/cortex.py +37 -6
  2. synapse/daemon.py +6 -6
  3. synapse/exc.py +13 -1
  4. synapse/lib/aha.py +5 -0
  5. synapse/lib/ast.py +2 -6
  6. synapse/lib/boss.py +47 -2
  7. synapse/lib/cell.py +193 -3
  8. synapse/lib/certdir.py +44 -1
  9. synapse/lib/cmd.py +24 -0
  10. synapse/lib/coro.py +8 -2
  11. synapse/lib/drive.py +7 -2
  12. synapse/lib/jsonstor.py +4 -1
  13. synapse/lib/layer.py +3 -1
  14. synapse/lib/link.py +11 -3
  15. synapse/lib/schemas.py +1 -1
  16. synapse/lib/snap.py +76 -65
  17. synapse/lib/storm.py +2 -1
  18. synapse/lib/stormlib/imap.py +3 -2
  19. synapse/lib/stormlib/spooled.py +1 -0
  20. synapse/lib/task.py +1 -0
  21. synapse/lib/version.py +2 -2
  22. synapse/models/inet.py +5 -0
  23. synapse/models/infotech.py +45 -0
  24. synapse/tests/files/testpkg_build_docs/docs/bar.rst +15 -0
  25. synapse/tests/files/testpkg_build_docs/docs/foo.rst +4 -0
  26. synapse/tests/files/testpkg_build_docs/storm/commands/testcmd.storm +0 -0
  27. synapse/tests/files/testpkg_build_docs/storm/modules/apimod.storm +0 -0
  28. synapse/tests/files/testpkg_build_docs/storm/modules/testmod.storm +0 -0
  29. synapse/tests/files/testpkg_build_docs/storm/testcmd.storm +5 -0
  30. synapse/tests/files/testpkg_build_docs/testpkg.yaml +69 -0
  31. synapse/tests/test_cortex.py +20 -1
  32. synapse/tests/test_daemon.py +1 -1
  33. synapse/tests/test_exc.py +6 -0
  34. synapse/tests/test_lib_ast.py +69 -14
  35. synapse/tests/test_lib_boss.py +8 -0
  36. synapse/tests/test_lib_cell.py +104 -5
  37. synapse/tests/test_lib_certdir.py +8 -0
  38. synapse/tests/test_lib_coro.py +5 -0
  39. synapse/tests/test_lib_httpapi.py +10 -2
  40. synapse/tests/test_lib_jsonstor.py +45 -0
  41. synapse/tests/test_lib_layer.py +10 -0
  42. synapse/tests/test_lib_link.py +1 -1
  43. synapse/tests/test_lib_storm.py +121 -1
  44. synapse/tests/test_lib_stormlib_iters.py +1 -1
  45. synapse/tests/test_lib_stormlib_spooled.py +20 -0
  46. synapse/tests/test_lib_stormtypes.py +15 -0
  47. synapse/tests/test_lib_types.py +5 -1
  48. synapse/tests/test_model_inet.py +7 -0
  49. synapse/tests/test_model_infotech.py +31 -0
  50. synapse/tests/test_telepath.py +32 -5
  51. synapse/tests/test_tools_axon.py +304 -0
  52. synapse/tests/test_tools_cortex_layer.py +419 -0
  53. synapse/tests/test_tools_demote.py +114 -0
  54. synapse/tests/test_tools_pkgs_gendocs.py +100 -0
  55. synapse/tests/test_tools_shutdown.py +95 -0
  56. synapse/tests/test_utils.py +22 -1
  57. synapse/tests/utils.py +44 -29
  58. synapse/tools/aha/easycert.py +2 -0
  59. synapse/tools/aha/enroll.py +3 -0
  60. synapse/tools/axon/__init__.py +0 -0
  61. synapse/tools/axon/dump.py +155 -0
  62. synapse/tools/axon/load.py +89 -0
  63. synapse/tools/cortex/__init__.py +0 -0
  64. synapse/tools/cortex/layer/__init__.py +0 -0
  65. synapse/tools/cortex/layer/dump.py +184 -0
  66. synapse/tools/cortex/layer/load.py +129 -0
  67. synapse/tools/demote.py +52 -0
  68. synapse/tools/healthcheck.py +1 -1
  69. synapse/tools/pkgs/gendocs.py +176 -0
  70. synapse/tools/pkgs/pandoc_filter.py +79 -0
  71. synapse/tools/shutdown.py +52 -0
  72. {synapse-2.213.0.dist-info → synapse-2.215.0.dist-info}/METADATA +1 -1
  73. {synapse-2.213.0.dist-info → synapse-2.215.0.dist-info}/RECORD +76 -53
  74. {synapse-2.213.0.dist-info → synapse-2.215.0.dist-info}/WHEEL +0 -0
  75. {synapse-2.213.0.dist-info → synapse-2.215.0.dist-info}/licenses/LICENSE +0 -0
  76. {synapse-2.213.0.dist-info → synapse-2.215.0.dist-info}/top_level.txt +0 -0
synapse/cortex.py CHANGED
@@ -957,6 +957,7 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
957
957
  await self._bumpCellVers('cortex:storage', (
958
958
  (1, self._storUpdateMacros),
959
959
  (4, self._storCortexHiveMigration),
960
+ (5, self._storCleanQueueAuthGates),
960
961
  ), nexs=False)
961
962
 
962
963
  # Perform module loading
@@ -1095,6 +1096,23 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
1095
1096
  await view.setViewInfo('protected', nomerge)
1096
1097
  await view.setViewInfo('nomerge', None)
1097
1098
 
1099
+ async def _storCleanQueueAuthGates(self):
1100
+
1101
+ logger.warning('removing AuthGates for Queues which no longer exist')
1102
+
1103
+ path = os.path.join(self.dirn, 'slabs', 'queues.lmdb')
1104
+
1105
+ async with await s_lmdbslab.Slab.anit(path) as slab:
1106
+ async with await slab.getMultiQueue('cortex:queue', nexsroot=self.nexsroot) as multiqueue:
1107
+ for info in self.auth.getAuthGates():
1108
+ if info.type == 'queue':
1109
+ iden = info.iden
1110
+ name = info.iden.split(':', 1)[1]
1111
+ if not multiqueue.exists(name):
1112
+ await self.auth.delAuthGate(info.iden)
1113
+
1114
+ logger.warning('...Queue AuthGate cleanup complete!')
1115
+
1098
1116
  async def _storUpdateMacros(self):
1099
1117
  for name, node in await self.hive.open(('cortex', 'storm', 'macros')):
1100
1118
 
@@ -1933,13 +1951,17 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
1933
1951
  raise s_exc.NoSuchName(mesg=mesg)
1934
1952
 
1935
1953
  await self._push('queue:del', name)
1936
- await self.auth.delAuthGate(f'queue:{name}')
1937
1954
 
1938
1955
  @s_nexus.Pusher.onPush('queue:del')
1939
1956
  async def _delCoreQueue(self, name):
1940
1957
  if not self.multiqueue.exists(name):
1941
1958
  return
1942
1959
 
1960
+ try:
1961
+ await self.auth.delAuthGate(f'queue:{name}')
1962
+ except s_exc.NoSuchAuthGate:
1963
+ pass
1964
+
1943
1965
  await self.multiqueue.rem(name)
1944
1966
 
1945
1967
  async def coreQueueGet(self, name, offs=0, cull=True, wait=False):
@@ -5435,7 +5457,8 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
5435
5457
  # push() will refire as needed
5436
5458
 
5437
5459
  async def push():
5438
- async with await self.boss.promote(f'layer push: {layr.iden} {iden}', self.auth.rootuser):
5460
+ taskname = f'layer push: {layr.iden} {iden}'
5461
+ async with await self.boss.promote(taskname, self.auth.rootuser, background=True):
5439
5462
  async with await s_telepath.openurl(url) as proxy:
5440
5463
  await self._pushBulkEdits(layr, proxy, pdef)
5441
5464
 
@@ -5447,7 +5470,8 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
5447
5470
  # pull() will refire as needed
5448
5471
 
5449
5472
  async def pull():
5450
- async with await self.boss.promote(f'layer pull: {layr.iden} {iden}', self.auth.rootuser):
5473
+ taskname = f'layer pull: {layr.iden} {iden}'
5474
+ async with await self.boss.promote(taskname, self.auth.rootuser, background=True):
5451
5475
  async with await s_telepath.openurl(url) as proxy:
5452
5476
  await self._pushBulkEdits(proxy, layr, pdef)
5453
5477
 
@@ -5898,15 +5922,17 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
5898
5922
  logger.warning('Storm query mirror pool is empty, running query locally.')
5899
5923
  return None
5900
5924
 
5925
+ timeout = self.stormpoolopts.get('timeout:connection')
5926
+
5901
5927
  for _ in range(size):
5902
5928
 
5903
5929
  try:
5904
- timeout = self.stormpoolopts.get('timeout:connection')
5905
5930
  proxy = await self.stormpool.proxy(timeout=timeout)
5931
+
5906
5932
  proxyname = proxy._ahainfo.get('name')
5907
5933
  if proxyname is not None and proxyname == self.ahasvcname:
5908
- # we are part of the pool and were selected. Convert to local use.
5909
- return None
5934
+ # we are part of the pool and were selected. Skip.
5935
+ continue
5910
5936
 
5911
5937
  except TimeoutError:
5912
5938
  logger.warning('Timeout waiting for pool mirror proxy.')
@@ -5922,6 +5948,10 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
5922
5948
  mesg = f'Pool mirror [{proxyname}] is too far out of sync. Skipping.'
5923
5949
  logger.warning(mesg, extra=await self.getLogExtra(delta=delta, mirror=proxyname, mirror_offset=miroffs))
5924
5950
 
5951
+ except s_exc.ShuttingDown:
5952
+ mesg = f'Proxy for pool mirror [{proxyname}] is shutting down. Skipping.'
5953
+ logger.warning(mesg, extra=await self.getLogExtra(mirror=proxyname))
5954
+
5925
5955
  except s_exc.IsFini:
5926
5956
  mesg = f'Proxy for pool mirror [{proxyname}] was shutdown. Skipping.'
5927
5957
  logger.warning(mesg, extra=await self.getLogExtra(mirror=proxyname))
@@ -5998,6 +6028,7 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell): # type: ignore
5998
6028
  return await view.callStorm(text, opts=opts)
5999
6029
 
6000
6030
  async def exportStorm(self, text, opts=None):
6031
+
6001
6032
  opts = self._initStormOpts(opts)
6002
6033
 
6003
6034
  if self.stormpool is not None and opts.get('mirror', True):
synapse/daemon.py CHANGED
@@ -177,6 +177,8 @@ async def t2call(link, meth, args, kwargs):
177
177
 
178
178
  if isinstance(e, asyncio.CancelledError):
179
179
  logger.info('t2call task %s cancelled', meth.__name__)
180
+ elif isinstance(e, (BrokenPipeError, ConnectionResetError)):
181
+ logger.debug(f'tx closed unexpectedly {e} link={link.getAddrInfo()} meth={meth.__name__}')
180
182
  else:
181
183
  logger.exception(f'error during task {meth.__name__} {e}')
182
184
 
@@ -527,12 +529,11 @@ class Daemon(s_base.Base):
527
529
  methname, args, kwargs = todo
528
530
 
529
531
  if methname[0] == '_':
530
- raise s_exc.NoSuchMeth(name=methname)
532
+ raise s_exc.NoSuchMeth.init(methname, item)
531
533
 
532
534
  meth = getattr(item, methname, None)
533
535
  if meth is None:
534
- logger.warning('%r has no method: %r', item, methname)
535
- raise s_exc.NoSuchMeth(name=methname)
536
+ raise s_exc.NoSuchMeth.init(methname, item)
536
537
 
537
538
  sessitem = await t2call(link, meth, args, kwargs)
538
539
  if sessitem is not None:
@@ -562,12 +563,11 @@ class Daemon(s_base.Base):
562
563
  methname, args, kwargs = mesg[1].get('todo')
563
564
 
564
565
  if methname[0] == '_':
565
- raise s_exc.NoSuchMeth(name=methname)
566
+ raise s_exc.NoSuchMeth.init(methname, item)
566
567
 
567
568
  meth = getattr(item, methname, None)
568
569
  if meth is None:
569
- logger.warning('%r has no method: %s', item, methname)
570
- raise s_exc.NoSuchMeth(name=methname)
570
+ raise s_exc.NoSuchMeth.init(methname, item)
571
571
 
572
572
  valu = await self._runTodoMeth(link, meth, args, kwargs)
573
573
 
synapse/exc.py CHANGED
@@ -226,6 +226,7 @@ class IsFini(SynErr): pass
226
226
  class IsReadOnly(SynErr): pass
227
227
  class IsDeprLocked(SynErr): pass
228
228
  class IsRuntForm(SynErr): pass
229
+ class ShuttingDown(SynErr): pass
229
230
 
230
231
  class LayerInUse(SynErr): pass
231
232
 
@@ -300,7 +301,11 @@ class NoSuchImpl(SynErr): pass
300
301
  class NoSuchIndx(SynErr): pass
301
302
  class NoSuchLayer(SynErr): pass
302
303
  class NoSuchLift(SynErr): pass
303
- class NoSuchMeth(SynErr): pass
304
+ class NoSuchMeth(SynErr):
305
+ @classmethod
306
+ def init(cls, name, item):
307
+ return cls(mesg=f'{item.__class__.__name__} has no method: {name}.', name=name)
308
+
304
309
  class NoSuchName(SynErr): pass
305
310
  class NoSuchObj(SynErr): pass
306
311
  class NoSuchOpt(SynErr): pass
@@ -361,3 +366,10 @@ class FatalErr(SynErr):
361
366
  pass
362
367
 
363
368
  class LmdbLock(SynErr): pass
369
+
370
+ def reprexc(e):
371
+ if isinstance(e, SynErr):
372
+ text = e.get('mesg')
373
+ if text is not None:
374
+ return text
375
+ return repr(e)
synapse/lib/aha.py CHANGED
@@ -178,6 +178,10 @@ class AhaApi(s_cell.CellApi):
178
178
  async for info in self.cell.getAhaSvcs(network=network):
179
179
  yield info
180
180
 
181
+ async def getAhaSvcsByIden(self, iden, online=True, skiprun=None):
182
+ async for svcdef in self.cell.getAhaSvcsByIden(iden, online=online, skiprun=skiprun):
183
+ yield svcdef
184
+
181
185
  async def addAhaSvc(self, name, info, network=None):
182
186
  '''
183
187
  Register a service with the AHA discovery server.
@@ -584,6 +588,7 @@ class AhaCell(s_cell.Cell):
584
588
  async def initServiceStorage(self):
585
589
 
586
590
  self.features['callpeers'] = 1
591
+ self.features['getAhaSvcsByIden'] = 1
587
592
 
588
593
  dirn = s_common.gendir(self.dirn, 'slabs', 'jsonstor')
589
594
 
synapse/lib/ast.py CHANGED
@@ -444,8 +444,8 @@ class SubGraph:
444
444
 
445
445
  for pivq in self.rules.get('pivots'):
446
446
  indx = 0
447
- async for node, path in node.storm(runt, pivq):
448
- yield node, path, {'type': 'rules', 'scope': 'global', 'index': indx}
447
+ async for n, p in node.storm(runt, pivq):
448
+ yield n, p, {'type': 'rules', 'scope': 'global', 'index': indx}
449
449
  indx += 1
450
450
 
451
451
  scope = node.form.name
@@ -885,7 +885,6 @@ class InitBlock(AstNode):
885
885
  async def run(self, runt, genr):
886
886
 
887
887
  subq = self.kids[0]
888
- self.reqRuntSafe(runt, 'Init block query must be runtsafe')
889
888
 
890
889
  once = False
891
890
  async for item in genr:
@@ -923,7 +922,6 @@ class EmptyBlock(AstNode):
923
922
  async def run(self, runt, genr):
924
923
 
925
924
  subq = self.kids[0]
926
- self.reqRuntSafe(runt, 'Empty block query must be runtsafe')
927
925
 
928
926
  empty = True
929
927
  async for item in genr:
@@ -955,8 +953,6 @@ class FiniBlock(AstNode):
955
953
 
956
954
  subq = self.kids[0]
957
955
 
958
- self.reqRuntSafe(runt, 'Fini block query must be runtsafe')
959
-
960
956
  async for item in genr:
961
957
  yield item
962
958
 
synapse/lib/boss.py CHANGED
@@ -1,7 +1,10 @@
1
1
  import asyncio
2
2
  import logging
3
3
 
4
+ import synapse.exc as s_exc
5
+
4
6
  import synapse.lib.base as s_base
7
+ import synapse.lib.coro as s_coro
5
8
  import synapse.lib.task as s_task
6
9
 
7
10
  logger = logging.getLogger(__name__)
@@ -18,8 +21,44 @@ class Boss(s_base.Base):
18
21
  async def __anit__(self):
19
22
  await s_base.Base.__anit__(self)
20
23
  self.tasks = {}
24
+ self.is_shutdown = False
25
+ self.shutdown_lock = asyncio.Lock()
21
26
  self.onfini(self._onBossFini)
22
27
 
28
+ async def shutdown(self, timeout=None):
29
+ # when a boss is "shutting down" it should not promote any new tasks,
30
+ # but await the completion of any which are already underway...
31
+
32
+ self.reqNotShut()
33
+
34
+ async with self.shutdown_lock:
35
+
36
+ for task in list(self.tasks.values()):
37
+
38
+ # do not wait on child tasks
39
+ if task.root is not None:
40
+ continue
41
+
42
+ # do not wait on background tasks
43
+ if task.background:
44
+ continue
45
+
46
+ if not await s_coro.waittask(task.task, timeout=timeout):
47
+ return False
48
+
49
+ self.is_shutdown = True
50
+ return True
51
+
52
+ def reqNotShut(self, mesg=None):
53
+ if self.shutdown_lock.locked():
54
+ if mesg is None:
55
+ mesg = 'The service is shutting down.'
56
+ raise s_exc.ShuttingDown(mesg=mesg)
57
+ if self.is_shutdown:
58
+ if mesg is None:
59
+ mesg = 'The service is shut down.'
60
+ raise s_exc.ShuttingDown(mesg=mesg)
61
+
23
62
  async def _onBossFini(self):
24
63
  for task in list(self.tasks.values()):
25
64
  await task.kill()
@@ -31,7 +70,7 @@ class Boss(s_base.Base):
31
70
  def get(self, iden):
32
71
  return self.tasks.get(iden)
33
72
 
34
- async def promote(self, name, user, info=None, taskiden=None):
73
+ async def promote(self, name, user, info=None, taskiden=None, background=False):
35
74
  '''
36
75
  Promote the currently running task.
37
76
 
@@ -45,10 +84,15 @@ class Boss(s_base.Base):
45
84
  s_task.Task: The Synapse Task object.
46
85
  '''
47
86
  task = asyncio.current_task()
48
- return await self.promotetask(task, name, user, info=info, taskiden=taskiden)
87
+
88
+ syntask = await self.promotetask(task, name, user, info=info, taskiden=taskiden)
89
+ syntask.background = background
90
+
91
+ return syntask
49
92
 
50
93
  async def promotetask(self, task, name, user, info=None, taskiden=None):
51
94
 
95
+ self.reqNotShut()
52
96
  synt = s_task.syntask(task)
53
97
 
54
98
  if synt is not None:
@@ -69,5 +113,6 @@ class Boss(s_base.Base):
69
113
  '''
70
114
  Create a synapse task from the given coroutine.
71
115
  '''
116
+ self.reqNotShut()
72
117
  task = self.schedCoro(coro)
73
118
  return await s_task.Task.anit(self, task, name, user, info=info, iden=iden)
synapse/lib/cell.py CHANGED
@@ -209,6 +209,10 @@ class CellApi(s_base.Base):
209
209
  async def initCellApi(self):
210
210
  pass
211
211
 
212
+ @adminapi(log=True)
213
+ async def shutdown(self, timeout=None):
214
+ return await self.cell.shutdown(timeout=timeout)
215
+
212
216
  @adminapi(log=True)
213
217
  async def freeze(self, timeout=30):
214
218
  return await self.cell.freeze(timeout=timeout)
@@ -299,6 +303,7 @@ class CellApi(s_base.Base):
299
303
 
300
304
  @adminapi()
301
305
  def getNexsIndx(self):
306
+ self.cell.boss.reqNotShut()
302
307
  return self.cell.getNexsIndx()
303
308
 
304
309
  @adminapi()
@@ -381,6 +386,10 @@ class CellApi(s_base.Base):
381
386
  async def handoff(self, turl, timeout=30):
382
387
  return await self.cell.handoff(turl, timeout=timeout)
383
388
 
389
+ @adminapi(log=True)
390
+ async def demote(self, timeout=None):
391
+ return await self.cell.demote(timeout=timeout)
392
+
384
393
  def getCellUser(self):
385
394
  return self.user.pack()
386
395
 
@@ -1024,6 +1033,11 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1024
1033
  'description': 'The username of this service when connecting to others.',
1025
1034
  'type': 'string',
1026
1035
  },
1036
+ 'aha:promotable': {
1037
+ 'description': 'Set to false to prevent this service from being promoted to leader.',
1038
+ 'type': 'boolean',
1039
+ 'default': True,
1040
+ },
1027
1041
  'aha:leader': {
1028
1042
  'description': 'The AHA service name to claim as the active instance of a storm service.',
1029
1043
  'type': 'string',
@@ -1493,6 +1507,16 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1493
1507
 
1494
1508
  logger.warning(f'...Cell ({self.getCellType()}) auth migration complete!')
1495
1509
 
1510
+ async def _drivePermMigration(self):
1511
+ for lkey, lval in self.slab.scanByPref(s_drive.LKEY_INFO, db=self.drive.dbname):
1512
+ info = s_msgpack.un(lval)
1513
+ perm = info.pop('perm', None)
1514
+ if perm is not None:
1515
+ perm.setdefault('users', {})
1516
+ perm.setdefault('roles', {})
1517
+ info['permissions'] = perm
1518
+ self.slab.put(lkey, s_msgpack.en(info), db=self.drive.dbname)
1519
+
1496
1520
  def getPermDef(self, perm):
1497
1521
  perm = tuple(perm)
1498
1522
  if self.permlook is None:
@@ -1533,6 +1557,33 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1533
1557
  self._onFiniCellGuid()
1534
1558
  return retn
1535
1559
 
1560
+ async def shutdown(self, timeout=None):
1561
+ '''
1562
+ Execute a graceful shutdown by allowing any promoted boss tasks to complete
1563
+ and prevents the boss from accepting additional tasks.
1564
+ '''
1565
+ extra = await self.getLogExtra()
1566
+ logger.warning('Graceful shutdown initiated...', extra=extra)
1567
+
1568
+ # if we're the leader, lets see if we can handoff...
1569
+ if self.isactive and self.ahaclient is not None:
1570
+ peers = await self._getDemotePeers(timeout=timeout)
1571
+ if peers:
1572
+ if not await self.demote(peers=peers, timeout=timeout): # pragma: no cover
1573
+ logger.warning('...we are the leader and failed to demote. Aborting shutdown.', extra=extra)
1574
+ return False
1575
+
1576
+ if not await self.boss.shutdown(timeout=timeout):
1577
+ logger.warning('...tasks did not complete within timeout. Aborting shutdown.', extra=extra)
1578
+ return False
1579
+
1580
+ def syncfini(futu):
1581
+ logger.warning('...all tasks exited. Shutting down.')
1582
+ s_coro.create_task(self.fini())
1583
+
1584
+ asyncio.current_task().add_done_callback(syncfini)
1585
+ return True
1586
+
1536
1587
  def _onFiniCellGuid(self):
1537
1588
  fcntl.lockf(self._cellguidfd, fcntl.LOCK_UN)
1538
1589
  self._cellguidfd.close()
@@ -1831,6 +1882,10 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1831
1882
 
1832
1883
  async def initCellStorage(self):
1833
1884
  self.drive = await s_drive.Drive.anit(self.slab, 'celldrive')
1885
+ await self._bumpCellVers('drive:storage', (
1886
+ (1, self._drivePermMigration),
1887
+ ), nexs=False)
1888
+
1834
1889
  self.onfini(self.drive.fini)
1835
1890
 
1836
1891
  async def addDriveItem(self, info, path=None, reldir=s_drive.rootdir):
@@ -1852,6 +1907,13 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1852
1907
  if self.drive.hasItemInfo(iden): # pragma: no cover
1853
1908
  return await self.drive.getItemPath(iden)
1854
1909
 
1910
+ # TODO: Remove this in synapse-3xx
1911
+ perm = info.pop('perm', None)
1912
+ if perm:
1913
+ perm.setdefault('users', {})
1914
+ perm.setdefault('roles', {})
1915
+ info['permissions'] = perm
1916
+
1855
1917
  return await self.drive.addItemInfo(info, path=path, reldir=reldir)
1856
1918
 
1857
1919
  async def getDriveInfo(self, iden, typename=None):
@@ -1897,7 +1959,7 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1897
1959
 
1898
1960
  info = {
1899
1961
  'name': name,
1900
- 'perm': perm,
1962
+ 'permissions': perm,
1901
1963
  'iden': s_common.guid(),
1902
1964
  'created': tick,
1903
1965
  'creator': user,
@@ -1927,6 +1989,50 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
1927
1989
  async def setDriveInfoPerm(self, iden, perm):
1928
1990
  return self.drive.setItemPerm(iden, perm)
1929
1991
 
1992
+ @s_nexus.Pusher.onPushAuto('drive:data:path:set')
1993
+ async def setDriveItemProp(self, iden, vers, path, valu):
1994
+ if isinstance(path, str):
1995
+ path = (path,)
1996
+
1997
+ data = await self.getDriveData(iden)
1998
+ if data is None:
1999
+ mesg = f'No drive item with ID {iden}.'
2000
+ raise s_exc.NoSuchIden(mesg=mesg)
2001
+
2002
+ _, item = data
2003
+
2004
+ try:
2005
+ step = item
2006
+ for p in path[:-1]:
2007
+ step = step[p]
2008
+ step[path[-1]] = valu
2009
+ except (KeyError, IndexError):
2010
+ raise s_exc.BadArg(mesg=f'Invalid path {path}')
2011
+
2012
+ return await self.drive.setItemData(iden, vers, item)
2013
+
2014
+ @s_nexus.Pusher.onPushAuto('drive:data:path:del')
2015
+ async def delDriveItemProp(self, iden, vers, path):
2016
+ if isinstance(path, str):
2017
+ path = (path,)
2018
+
2019
+ data = await self.getDriveData(iden)
2020
+ if data is None:
2021
+ mesg = f'No drive item with ID {iden}.'
2022
+ raise s_exc.NoSuchIden(mesg=mesg)
2023
+
2024
+ _, item = data
2025
+
2026
+ try:
2027
+ step = item
2028
+ for p in path[:-1]:
2029
+ step = step[p]
2030
+ del step[path[-1]]
2031
+ except (KeyError, IndexError):
2032
+ return
2033
+
2034
+ return await self.drive.setItemData(iden, vers, item)
2035
+
1930
2036
  @s_nexus.Pusher.onPushAuto('drive:set:path')
1931
2037
  async def setDriveInfoPath(self, iden, path):
1932
2038
 
@@ -2043,6 +2149,7 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
2043
2149
  'leader': ahalead,
2044
2150
  'urlinfo': urlinfo,
2045
2151
  'ready': ready,
2152
+ 'promotable': self.conf.get('aha:promotable'),
2046
2153
  }
2047
2154
 
2048
2155
  return ahainfo
@@ -2267,11 +2374,92 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
2267
2374
 
2268
2375
  logger.warning(f'HANDOFF: Done performing the leadership handoff with {s_urlhelp.sanitizeUrl(turl)}{_dispname}.')
2269
2376
 
2270
- async def reqAhaProxy(self, timeout=None):
2377
+ async def _getDemotePeers(self, timeout=None):
2378
+
2379
+ ahaproxy = await self.reqAhaProxy(timeout=timeout, feats=[('getAhaSvcsByIden', 1)])
2380
+
2381
+ retn = []
2382
+ async for svcdef in ahaproxy.getAhaSvcsByIden(self.iden, skiprun=self.runid):
2383
+
2384
+ if not svcdef['svcinfo'].get('promotable'): # pragma: no cover
2385
+ continue
2386
+
2387
+ retn.append(svcdef)
2388
+
2389
+ return retn
2390
+
2391
+ async def demote(self, peers=None, timeout=None):
2392
+
2393
+ logger.warning('Service demotion requested. Locating a suitable service for promotion...')
2394
+
2395
+ extra = await self.getLogExtra()
2396
+
2397
+ if not self.isactive:
2398
+ logger.warning('...service is not the leader. Aborting demotion.', extra=extra)
2399
+ return False
2400
+
2401
+ user = self.conf.get('aha:user')
2402
+
2403
+ if peers is None:
2404
+ peers = await self._getDemotePeers(timeout=timeout)
2405
+
2406
+ if not peers:
2407
+ logger.warning('...no suitable services discovered. Aborting demotion.', extra=extra)
2408
+ return False
2409
+
2410
+ try:
2411
+
2412
+ async with await s_base.Base.anit() as base:
2413
+
2414
+ cands = []
2415
+
2416
+ for svcdef in peers:
2417
+
2418
+ name = svcdef.get('name')
2419
+
2420
+ try:
2421
+ svcproxy = await base.enter_context(await s_telepath.openurl(f'aha://{user}@{name}'))
2422
+
2423
+ svcindx = await svcproxy.getNexsIndx()
2424
+ cands.append((svcindx, svcproxy))
2425
+
2426
+ except Exception as e:
2427
+ logger.warning(f'...error retrieving nexus index for {name}: {s_exc.reprexc(e)}. Skipping.', extra=extra)
2428
+
2429
+ if not cands:
2430
+ logger.warning('...no suitable services discovered. Aborting demotion.', extra=extra)
2431
+ return False
2432
+
2433
+ for svcindx, svcproxy in sorted(cands):
2434
+
2435
+ try:
2436
+ await svcproxy.promote(graceful=True)
2437
+
2438
+ logger.warning(f'... successfully promoted: {svcproxy._ahainfo["name"]}', extra=extra)
2439
+ return True
2440
+
2441
+ except Exception as e:
2442
+ logger.warning(f'...error promoting {svcproxy._ahainfo["name"]}. Skipping.', extra=extra)
2443
+
2444
+ except Exception as e:
2445
+ logger.exception(f'...error demoting service: {s_exc.reprexc(e)}', extra=extra)
2446
+ return False
2447
+
2448
+ async def reqAhaProxy(self, timeout=None, feats=None):
2449
+
2271
2450
  if self.ahaclient is None:
2272
2451
  mesg = 'AHA is not configured on this service.'
2273
2452
  raise s_exc.NeedConfValu(mesg=mesg)
2274
- return await self.ahaclient.proxy(timeout=timeout)
2453
+
2454
+ proxy = await self.ahaclient.proxy(timeout=timeout)
2455
+
2456
+ if feats is not None:
2457
+ for name, vers in feats:
2458
+ if not proxy._hasTeleFeat(name, vers):
2459
+ mesg = f'AHA server does not support feature: {name} >= {vers}'
2460
+ raise s_exc.BadVersion(mesg=mesg)
2461
+
2462
+ return proxy
2275
2463
 
2276
2464
  async def _setAhaActive(self):
2277
2465
 
@@ -4035,6 +4223,7 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
4035
4223
  os.unlink(_csr)
4036
4224
  hostcsr = certdir.genHostCsr(hostname)
4037
4225
  certdir.saveHostCertByts(await prov.signHostCsr(hostcsr))
4226
+ certdir.delHostCsr(hostname)
4038
4227
 
4039
4228
  userfull = f'{ahauser}@{ahanetw}'
4040
4229
  _crt = certdir.getUserCertPath(userfull)
@@ -4051,6 +4240,7 @@ class Cell(s_nexus.Pusher, s_telepath.Aware):
4051
4240
  os.unlink(_csr)
4052
4241
  usercsr = certdir.genUserCsr(userfull)
4053
4242
  certdir.saveUserCertByts(await prov.signUserCsr(usercsr))
4243
+ certdir.delUserCsr(userfull)
4054
4244
 
4055
4245
  with s_common.genfile(self.dirn, 'prov.done') as fd:
4056
4246
  fd.write(providen.encode())
synapse/lib/certdir.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import io
2
2
  import os
3
3
  import ssl
4
- import time
5
4
  import shutil
6
5
  import socket
7
6
  import logging
@@ -397,6 +396,28 @@ class CertDir:
397
396
  '''
398
397
  return self._genPkeyCsr(name, 'hosts', outp=outp)
399
398
 
399
+ def delHostCsr(self, name: str, outp: OutPutOrNone = None) -> bool:
400
+ '''
401
+ Delete an existing host CSR.
402
+
403
+ Args:
404
+ name: The name of the host CSR.
405
+ outp: The output buffer.
406
+
407
+ Returns:
408
+ bool: True if the CSR is deleted, False if it did not exist.
409
+ '''
410
+ path = self.getHostCsrPath(name)
411
+ if path is None:
412
+ return False
413
+ try:
414
+ os.unlink(path)
415
+ except Exception as e: # pragma: no cover
416
+ raise s_exc.SynErr(mesg=f'Failed to delete CSR {path} - {e}') from e
417
+ if outp:
418
+ outp.printf(f'Deleted CSR at {path}')
419
+ return True
420
+
400
421
  def genUserCert(self,
401
422
  name: str,
402
423
  signas: StrOrNone = None,
@@ -697,6 +718,28 @@ class CertDir:
697
718
  '''
698
719
  return self._genPkeyCsr(name, 'users', outp=outp)
699
720
 
721
+ def delUserCsr(self, name: str, outp: OutPutOrNone = None) -> bool:
722
+ '''
723
+ Delete an existing user CSR.
724
+
725
+ Args:
726
+ name: The name of the user CSR.
727
+ outp: The output buffer.
728
+
729
+ Returns:
730
+ bool: True if the CSR is deleted, False if it did not exist.
731
+ '''
732
+ path = self.getUserCsrPath(name)
733
+ if path is None:
734
+ return False
735
+ try:
736
+ os.unlink(path)
737
+ except Exception as e: # pragma: no cover
738
+ raise s_exc.SynErr(mesg=f'Failed to delete CSR {path} - {e}') from e
739
+ if outp:
740
+ outp.printf(f'Deleted CSR at {path}')
741
+ return True
742
+
700
743
  def getCaCert(self, name: str) -> CertOrNone:
701
744
  '''
702
745
  Loads the X509 object for a given CA.