synapse 2.176.0__py311-none-any.whl → 2.178.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 (95) hide show
  1. synapse/axon.py +24 -9
  2. synapse/cortex.py +337 -172
  3. synapse/cryotank.py +46 -37
  4. synapse/datamodel.py +17 -4
  5. synapse/exc.py +19 -0
  6. synapse/lib/agenda.py +7 -13
  7. synapse/lib/aha.py +361 -88
  8. synapse/lib/auth.py +1520 -0
  9. synapse/lib/base.py +27 -9
  10. synapse/lib/cell.py +422 -163
  11. synapse/lib/config.py +15 -11
  12. synapse/lib/coro.py +13 -0
  13. synapse/lib/grammar.py +5 -0
  14. synapse/lib/hive.py +24 -3
  15. synapse/lib/hiveauth.py +6 -32
  16. synapse/lib/layer.py +7 -9
  17. synapse/lib/link.py +22 -18
  18. synapse/lib/lmdbslab.py +152 -3
  19. synapse/lib/modelrev.py +1 -1
  20. synapse/lib/nexus.py +24 -12
  21. synapse/lib/schemas.py +136 -0
  22. synapse/lib/storm.py +61 -29
  23. synapse/lib/stormlib/aha.py +1 -1
  24. synapse/lib/stormlib/auth.py +185 -10
  25. synapse/lib/stormlib/cortex.py +16 -5
  26. synapse/lib/stormlib/gen.py +80 -0
  27. synapse/lib/stormlib/imap.py +6 -2
  28. synapse/lib/stormlib/model.py +55 -0
  29. synapse/lib/stormlib/modelext.py +60 -0
  30. synapse/lib/stormlib/smtp.py +12 -2
  31. synapse/lib/stormlib/tabular.py +212 -0
  32. synapse/lib/stormtypes.py +14 -1
  33. synapse/lib/trigger.py +1 -1
  34. synapse/lib/version.py +2 -2
  35. synapse/lib/view.py +55 -28
  36. synapse/models/base.py +7 -0
  37. synapse/models/biz.py +4 -0
  38. synapse/models/files.py +8 -1
  39. synapse/models/inet.py +8 -0
  40. synapse/telepath.py +32 -17
  41. synapse/tests/files/aha/certs/cas/synapse.crt +28 -0
  42. synapse/tests/files/aha/certs/cas/synapse.key +51 -0
  43. synapse/tests/files/aha/certs/hosts/00.aha.loop.vertex.link.crt +30 -0
  44. synapse/tests/files/aha/certs/hosts/00.aha.loop.vertex.link.key +51 -0
  45. synapse/tests/files/aha/certs/users/root@synapse.crt +29 -0
  46. synapse/tests/files/aha/certs/users/root@synapse.key +51 -0
  47. synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
  48. synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
  49. synapse/tests/files/rstorm/testsvc.py +1 -1
  50. synapse/tests/test_axon.py +8 -5
  51. synapse/tests/test_cortex.py +149 -141
  52. synapse/tests/test_cryotank.py +4 -4
  53. synapse/tests/test_datamodel.py +7 -0
  54. synapse/tests/test_lib_agenda.py +10 -3
  55. synapse/tests/test_lib_aha.py +336 -490
  56. synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
  57. synapse/tests/test_lib_base.py +20 -0
  58. synapse/tests/test_lib_cell.py +210 -30
  59. synapse/tests/test_lib_config.py +4 -3
  60. synapse/tests/test_lib_httpapi.py +18 -14
  61. synapse/tests/test_lib_layer.py +33 -33
  62. synapse/tests/test_lib_link.py +42 -1
  63. synapse/tests/test_lib_lmdbslab.py +68 -0
  64. synapse/tests/test_lib_nexus.py +12 -4
  65. synapse/tests/test_lib_node.py +0 -7
  66. synapse/tests/test_lib_storm.py +45 -0
  67. synapse/tests/test_lib_stormlib_aha.py +35 -36
  68. synapse/tests/test_lib_stormlib_auth.py +21 -0
  69. synapse/tests/test_lib_stormlib_cell.py +4 -15
  70. synapse/tests/test_lib_stormlib_cortex.py +12 -12
  71. synapse/tests/test_lib_stormlib_gen.py +99 -0
  72. synapse/tests/test_lib_stormlib_imap.py +14 -3
  73. synapse/tests/test_lib_stormlib_model.py +108 -0
  74. synapse/tests/test_lib_stormlib_modelext.py +64 -0
  75. synapse/tests/test_lib_stormlib_smtp.py +51 -0
  76. synapse/tests/test_lib_stormlib_tabular.py +226 -0
  77. synapse/tests/test_lib_stormsvc.py +4 -1
  78. synapse/tests/test_lib_stormtypes.py +10 -0
  79. synapse/tests/test_model_base.py +3 -0
  80. synapse/tests/test_model_biz.py +3 -0
  81. synapse/tests/test_model_files.py +12 -2
  82. synapse/tests/test_model_inet.py +24 -0
  83. synapse/tests/test_tools_aha.py +78 -101
  84. synapse/tests/test_tools_changelog.py +196 -0
  85. synapse/tests/test_tools_healthcheck.py +4 -3
  86. synapse/tests/utils.py +87 -121
  87. synapse/tools/aha/clone.py +50 -0
  88. synapse/tools/aha/enroll.py +2 -1
  89. synapse/tools/backup.py +2 -2
  90. synapse/tools/changelog.py +776 -15
  91. {synapse-2.176.0.dist-info → synapse-2.178.0.dist-info}/METADATA +48 -48
  92. {synapse-2.176.0.dist-info → synapse-2.178.0.dist-info}/RECORD +95 -82
  93. {synapse-2.176.0.dist-info → synapse-2.178.0.dist-info}/WHEEL +1 -1
  94. {synapse-2.176.0.dist-info → synapse-2.178.0.dist-info}/LICENSE +0 -0
  95. {synapse-2.176.0.dist-info → synapse-2.178.0.dist-info}/top_level.txt +0 -0
synapse/lib/nexus.py CHANGED
@@ -91,7 +91,6 @@ class NexsRoot(s_base.Base):
91
91
  self.writeholds = set()
92
92
 
93
93
  self.applytask = None
94
- self.applylock = asyncio.Lock()
95
94
 
96
95
  self.ready = asyncio.Event()
97
96
  self.donexslog = self.cell.conf.get('nexslog:en')
@@ -110,9 +109,7 @@ class NexsRoot(s_base.Base):
110
109
 
111
110
  logpath = s_common.genpath(self.dirn, 'slabs', 'nexuslog')
112
111
 
113
- self.map_async = self.cell.conf.get('nexslog:async')
114
- self.nexsslab = await s_lmdbslab.Slab.anit(path, map_async=self.map_async)
115
- self.nexsslab.addResizeCallback(cell.checkFreeSpace)
112
+ self.nexsslab = await cell._initSlabFile(path)
116
113
 
117
114
  self.nexshot = await self.nexsslab.getHotCount('nexs:indx')
118
115
 
@@ -126,8 +123,7 @@ class NexsRoot(s_base.Base):
126
123
  elif vers != 2:
127
124
  raise s_exc.BadStorageVersion(mesg=f'Got nexus log version {vers}. Expected 2. Accidental downgrade?')
128
125
 
129
- slabopts = {'map_async': self.map_async}
130
- self.nexslog = await s_multislabseqn.MultiSlabSeqn.anit(logpath, slabopts=slabopts, cell=cell)
126
+ self.nexslog = await s_multislabseqn.MultiSlabSeqn.anit(logpath, cell=cell)
131
127
 
132
128
  # just in case were previously configured differently
133
129
  logindx = self.nexslog.index()
@@ -147,6 +143,9 @@ class NexsRoot(s_base.Base):
147
143
 
148
144
  self.onfini(fini)
149
145
 
146
+ def getNexsKids(self):
147
+ return list(self._nexskids.values())
148
+
150
149
  async def _migrateV1toV2(self, nexspath, logpath):
151
150
  '''
152
151
  Close the slab, move it to the new multislab location, then copy out the nexshot
@@ -195,8 +194,7 @@ class NexsRoot(s_base.Base):
195
194
 
196
195
  # Open a fresh slab where the old one used to be
197
196
  logger.warning(f'Re-opening fresh nexslog slab at {nexspath} for nexshot')
198
- self.nexsslab = await s_lmdbslab.Slab.anit(nexspath, map_async=self.map_async)
199
- self.nexsslab.addResizeCallback(self.cell.checkFreeSpace)
197
+ self.nexsslab = await self.cell._initSlabFile(nexspath)
200
198
 
201
199
  self.nexshot = await self.nexsslab.getHotCount('nexs:indx')
202
200
 
@@ -230,7 +228,7 @@ class NexsRoot(s_base.Base):
230
228
 
231
229
  async def enNexsLog(self):
232
230
 
233
- async with self.applylock:
231
+ async with self.cell.nexslock:
234
232
 
235
233
  if self.donexslog:
236
234
  return
@@ -309,7 +307,6 @@ class NexsRoot(s_base.Base):
309
307
  If I'm not a follower, mutate, otherwise, ask the leader to make the change and wait for the follower loop
310
308
  to hand me the result through a future.
311
309
  '''
312
-
313
310
  # pick up a reference to avoid race when we eventually can promote
314
311
  client = self.client
315
312
 
@@ -344,7 +341,7 @@ class NexsRoot(s_base.Base):
344
341
  if meta is None:
345
342
  meta = {}
346
343
 
347
- async with self.applylock:
344
+ async with self.cell.nexslock:
348
345
  self.reqNotReadOnly()
349
346
  # Keep a reference to the shielded task to ensure it isn't GC'd
350
347
  self.applytask = asyncio.create_task(self._eat((nexsiden, event, args, kwargs, meta)))
@@ -577,6 +574,9 @@ class NexsRoot(s_base.Base):
577
574
  if respfutu is not None:
578
575
  respfutu.set_result(retn)
579
576
 
577
+ except s_exc.LinkShutDown:
578
+ logger.warning(f'mirror loop: leader closed the connection.')
579
+
580
580
  except Exception as exc: # pragma: no cover
581
581
  logger.exception(f'error in mirror loop: {exc}')
582
582
 
@@ -634,9 +634,21 @@ class Pusher(s_base.Base, metaclass=RegMethType):
634
634
  assert prev is not None, f'Failed removing {self.nexsiden}'
635
635
 
636
636
  self.onfini(onfini)
637
-
638
637
  self.nexsroot = nexsroot
639
638
 
639
+ async def modNexsRoot(self, ctor):
640
+
641
+ kids = [self]
642
+ if self.nexsroot is not None:
643
+ kids = self.nexsroot.getNexsKids()
644
+ await self.nexsroot.fini()
645
+
646
+ nexsroot = await ctor()
647
+
648
+ [kid.setNexsRoot(nexsroot) for kid in kids]
649
+
650
+ await nexsroot.startup()
651
+
640
652
  @classmethod
641
653
  def onPush(cls, event: str, passitem=False) -> Callable:
642
654
  '''
synapse/lib/schemas.py CHANGED
@@ -288,6 +288,93 @@ _stormPoolOptsSchema = {
288
288
  }
289
289
  reqValidStormPoolOpts = s_config.getJsValidator(_stormPoolOptsSchema)
290
290
 
291
+ _authRulesSchema = {
292
+ 'type': 'array',
293
+ 'items': {
294
+ 'type': 'array',
295
+ 'items': [
296
+ {'type': 'boolean'},
297
+ {'type': 'array', 'items': {'type': 'string'}},
298
+ ],
299
+ 'minItems': 2,
300
+ 'maxItems': 2,
301
+ }
302
+ }
303
+ reqValidRules = s_config.getJsValidator(_authRulesSchema)
304
+
305
+ _passwdPolicySchema = {
306
+ 'type': 'object',
307
+ 'properties': {
308
+ 'complexity': {
309
+ 'type': ['object', 'null'],
310
+ 'properties': {
311
+ 'length': {
312
+ 'type': ['number', 'null'],
313
+ 'minimum': 1,
314
+ 'description': 'Minimum password character length.',
315
+ },
316
+ 'sequences': {
317
+ 'type': ['number', 'null'],
318
+ 'minimum': 2,
319
+ 'description': 'Maximum sequence length in a password. Sequences can be letters or number, forward or reverse.',
320
+ },
321
+ 'upper:count': {
322
+ 'type': ['number', 'null'],
323
+ 'description': 'The minimum number of uppercase characters required in password.',
324
+ },
325
+ 'upper:valid': {
326
+ 'type': ['string', 'null'],
327
+ 'minLength': 1,
328
+ 'description': 'All valid uppercase characters.',
329
+ },
330
+ 'lower:count': {
331
+ 'type': ['number', 'null'],
332
+ 'minimum': 0,
333
+ 'description': 'The minimum number of lowercase characters required in password.',
334
+ },
335
+ 'lower:valid': {
336
+ 'type': ['string', 'null'],
337
+ 'minLength': 1,
338
+ 'description': 'All valid lowercase characters.',
339
+ },
340
+ 'special:count': {
341
+ 'type': ['number', 'null'],
342
+ 'minimum': 0,
343
+ 'description': 'The minimum number of special characters required in password.',
344
+ },
345
+ 'special:valid': {
346
+ 'type': ['string', 'null'],
347
+ 'minLength': 1,
348
+ 'description': 'All valid special characters.',
349
+ },
350
+ 'number:count': {
351
+ 'type': ['number', 'null'],
352
+ 'minimum': 0,
353
+ 'description': 'The minimum number of digit characters required in password.',
354
+ },
355
+ 'number:valid': {
356
+ 'type': ['string', 'null'],
357
+ 'minLength': 1,
358
+ 'description': 'All valid digit characters.',
359
+ },
360
+ },
361
+ 'additionalProperties': False,
362
+ },
363
+ 'attempts': {
364
+ 'type': ['number', 'null'],
365
+ 'minimum': 1,
366
+ 'description': 'Maximum number of incorrect attempts before locking user account.',
367
+ },
368
+ 'previous': {
369
+ 'type': ['number', 'null'],
370
+ 'minimum': 1,
371
+ 'description': 'Number of previous passwords to disallow.',
372
+ },
373
+ },
374
+ 'additionalProperties': False,
375
+ }
376
+ reqValidPasswdPolicy = s_config.getJsValidator(_passwdPolicySchema)
377
+
291
378
  # These types are order sensitive
292
379
  _changelogTypes = {'migration': 'Automatic Migrations',
293
380
  'model': 'Model Changes',
@@ -318,3 +405,52 @@ _changelogSchema = {
318
405
  'required': ['type', 'desc']
319
406
  }
320
407
  _reqChanglogSchema = s_config.getJsValidator(_changelogSchema)
408
+
409
+ tabularConfSchema = {
410
+ 'type': 'object',
411
+ 'properties': {
412
+ 'separators': {
413
+ 'type': 'object',
414
+ 'properties': {
415
+ 'row:outline': {'type': 'boolean', 'default': False,
416
+ 'description': 'Add the row separator before the header data and after each row.'},
417
+ 'column:outline': {'type': 'boolean', 'default': False,
418
+ 'description': 'Add the column separator to the beginning and end of each row.'},
419
+ 'header:row': {'type': 'string', 'default': '=',
420
+ 'description': 'The string to use to create a separator row when printing the header.'},
421
+ 'data:row': {'type': 'string', 'default': '-',
422
+ 'description': 'The string to use to create a separator row when printing data rows.'},
423
+ 'column': {'type': 'string', 'default': '|',
424
+ 'description': 'The string to use to separate columns.'},
425
+ },
426
+ 'additionalProperties': False,
427
+ },
428
+ 'columns': {
429
+ 'type': 'array',
430
+ 'items': {
431
+ 'type': 'object',
432
+ 'properties': {
433
+ 'name': {'type': 'string',
434
+ 'description': 'The column name which will be used in the header row.'},
435
+ 'width': {'type': 'number', 'default': None, 'exclusiveMinimum': 0,
436
+ 'description': 'If not provided each cell will expand to fit the data.'},
437
+ 'justify': {'type': 'string', 'default': 'left', 'enum': ['left', 'center', 'right'],
438
+ 'description': 'Justification for the header titles and data rows.'},
439
+ 'overflow': {'type': 'string', 'default': 'trim', 'enum': ['wrap', 'trim'],
440
+ 'description': 'For text exceeding the width, '
441
+ 'either wrap text in multiple lines or trim and append "...".'},
442
+ 'newlines': {'type': 'string', 'default': 'replace', 'enum': ['replace', 'split'],
443
+ 'description': 'Replace newlines with a space or split into multiple lines.'
444
+ 'Split is only applied if width is undefined.'},
445
+ },
446
+ 'required': ['name'],
447
+ 'minItems': 1,
448
+ 'additionalProperties': False,
449
+ },
450
+ },
451
+ },
452
+ 'required': ['columns'],
453
+ 'additionalProperties': False,
454
+ }
455
+
456
+ reqValidTabularConf = s_config.getJsValidator(tabularConfSchema)
synapse/lib/storm.py CHANGED
@@ -12,6 +12,7 @@ import synapse.telepath as s_telepath
12
12
  import synapse.datamodel as s_datamodel
13
13
 
14
14
  import synapse.lib.ast as s_ast
15
+ import synapse.lib.auth as s_auth
15
16
  import synapse.lib.base as s_base
16
17
  import synapse.lib.chop as s_chop
17
18
  import synapse.lib.coro as s_coro
@@ -28,7 +29,6 @@ import synapse.lib.msgpack as s_msgpack
28
29
  import synapse.lib.spooled as s_spooled
29
30
  import synapse.lib.version as s_version
30
31
  import synapse.lib.hashitem as s_hashitem
31
- import synapse.lib.hiveauth as s_hiveauth
32
32
  import synapse.lib.stormctrl as s_stormctrl
33
33
  import synapse.lib.stormtypes as s_stormtypes
34
34
 
@@ -1066,7 +1066,7 @@ stormcmds = (
1066
1066
  $lib.view.del($view.iden)
1067
1067
  $lib.layer.del($layriden)
1068
1068
  } else {
1069
- $view.wipeLayer()
1069
+ $view.swapLayer()
1070
1070
  }
1071
1071
  $lib.print("View merged: {iden}", iden=$cmdopts.iden)
1072
1072
  ''',
@@ -1831,7 +1831,7 @@ class Runtime(s_base.Base):
1831
1831
 
1832
1832
  '''
1833
1833
 
1834
- _admin_reason = s_hiveauth._allowedReason(True, isadmin=True)
1834
+ _admin_reason = s_auth._allowedReason(True, isadmin=True)
1835
1835
  async def __anit__(self, query, snap, opts=None, user=None, root=None):
1836
1836
 
1837
1837
  await s_base.Base.__anit__(self)
@@ -3728,6 +3728,8 @@ class MergeCmd(Cmd):
3728
3728
  pars = Cmd.getArgParser(self)
3729
3729
  pars.add_argument('--apply', default=False, action='store_true',
3730
3730
  help='Execute the merge changes.')
3731
+ pars.add_argument('--wipe', default=False, action='store_true',
3732
+ help='Replace the top layer in the view with a fresh layer.')
3731
3733
  pars.add_argument('--no-tags', default=False, action='store_true',
3732
3734
  help='Do not merge tags/tagprops or syn:tag nodes.')
3733
3735
  pars.add_argument('--only-tags', default=False, action='store_true',
@@ -3801,13 +3803,15 @@ class MergeCmd(Cmd):
3801
3803
  layr1 = runt.snap.view.layers[1].iden
3802
3804
 
3803
3805
  if not allows['forms'] and sode.get('valu') is not None:
3804
- runt.confirm(('node', 'del', node.form.name), gateiden=layr0)
3806
+ if not self.opts.wipe:
3807
+ runt.confirm(('node', 'del', node.form.name), gateiden=layr0)
3805
3808
  runt.confirm(('node', 'add', node.form.name), gateiden=layr1)
3806
3809
 
3807
3810
  if not allows['props']:
3808
3811
  for name in sode.get('props', {}).keys():
3809
3812
  prop = node.form.prop(name)
3810
- runt.confirmPropDel(prop, layriden=layr0)
3813
+ if not self.opts.wipe:
3814
+ runt.confirmPropDel(prop, layriden=layr0)
3811
3815
  runt.confirmPropSet(prop, layriden=layr1)
3812
3816
 
3813
3817
  if not allows['tags']:
@@ -3818,7 +3822,8 @@ class MergeCmd(Cmd):
3818
3822
  if valu != (None, None):
3819
3823
  tagadds.append(tag)
3820
3824
  tagperm = tuple(tag.split('.'))
3821
- runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3825
+ if not self.opts.wipe:
3826
+ runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3822
3827
  runt.confirm(('node', 'tag', 'add') + tagperm, gateiden=layr1)
3823
3828
  else:
3824
3829
  tags.append((len(tag), tag))
@@ -3830,22 +3835,26 @@ class MergeCmd(Cmd):
3830
3835
 
3831
3836
  tagadds.append(tag)
3832
3837
  tagperm = tuple(tag.split('.'))
3833
- runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3838
+ if not self.opts.wipe:
3839
+ runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3834
3840
  runt.confirm(('node', 'tag', 'add') + tagperm, gateiden=layr1)
3835
3841
 
3836
3842
  for tag in sode.get('tagprops', {}).keys():
3837
3843
  tagperm = tuple(tag.split('.'))
3838
- runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3844
+ if not self.opts.wipe:
3845
+ runt.confirm(('node', 'tag', 'del') + tagperm, gateiden=layr0)
3839
3846
  runt.confirm(('node', 'tag', 'add') + tagperm, gateiden=layr1)
3840
3847
 
3841
3848
  if not allows['ndata']:
3842
3849
  async for name in runt.snap.view.layers[0].iterNodeDataKeys(node.buid):
3843
- runt.confirm(('node', 'data', 'pop', name), gateiden=layr0)
3850
+ if not self.opts.wipe:
3851
+ runt.confirm(('node', 'data', 'pop', name), gateiden=layr0)
3844
3852
  runt.confirm(('node', 'data', 'set', name), gateiden=layr1)
3845
3853
 
3846
3854
  if not allows['edges']:
3847
3855
  async for verb in runt.snap.view.layers[0].iterNodeEdgeVerbsN1(node.buid):
3848
- runt.confirm(('node', 'edge', 'del', verb), gateiden=layr0)
3856
+ if not self.opts.wipe:
3857
+ runt.confirm(('node', 'edge', 'del', verb), gateiden=layr0)
3849
3858
  runt.confirm(('node', 'edge', 'add', verb), gateiden=layr1)
3850
3859
 
3851
3860
  async def execStormCmd(self, runt, genr):
@@ -3854,6 +3863,11 @@ class MergeCmd(Cmd):
3854
3863
  mesg = 'You may only merge nodes in forked views'
3855
3864
  raise s_exc.CantMergeView(mesg=mesg)
3856
3865
 
3866
+ if self.opts.wipe:
3867
+ mesg = 'merge --wipe requires view admin'
3868
+ runt.reqAdmin(gateiden=runt.snap.view.iden, mesg=mesg)
3869
+ runt.confirm(('layer', 'del'), gateiden=runt.snap.view.layers[0].iden)
3870
+
3857
3871
  notags = self.opts.no_tags
3858
3872
  onlytags = self.opts.only_tags
3859
3873
  doapply = self.opts.apply
@@ -3867,18 +3881,27 @@ class MergeCmd(Cmd):
3867
3881
  doperms = doapply and not (runt.isAdmin(gateiden=layr0.iden) and runt.isAdmin(gateiden=layr1.iden))
3868
3882
 
3869
3883
  if doperms:
3870
- allows = {
3871
- 'forms': runt.user.allowed(('node', 'del'), gateiden=layr0.iden, deepdeny=True) and
3872
- runt.user.allowed(('node', 'add'), gateiden=layr1.iden, deepdeny=True),
3873
- 'props': runt.user.allowed(('node', 'prop', 'del'), gateiden=layr0.iden, deepdeny=True) and
3874
- runt.user.allowed(('node', 'prop', 'set'), gateiden=layr1.iden, deepdeny=True),
3875
- 'tags': runt.user.allowed(('node', 'tag', 'del'), gateiden=layr0.iden, deepdeny=True) and
3876
- runt.user.allowed(('node', 'tag', 'add'), gateiden=layr1.iden, deepdeny=True),
3877
- 'ndata': runt.user.allowed(('node', 'data', 'pop'), gateiden=layr0.iden, deepdeny=True) and
3878
- runt.user.allowed(('node', 'data', 'set'), gateiden=layr1.iden, deepdeny=True),
3879
- 'edges': runt.user.allowed(('node', 'edge', 'del'), gateiden=layr0.iden, deepdeny=True) and
3880
- runt.user.allowed(('node', 'edge', 'add'), gateiden=layr1.iden, deepdeny=True),
3881
- }
3884
+ if not self.opts.wipe:
3885
+ allows = {
3886
+ 'forms': runt.user.allowed(('node', 'del'), gateiden=layr0.iden, deepdeny=True) and
3887
+ runt.user.allowed(('node', 'add'), gateiden=layr1.iden, deepdeny=True),
3888
+ 'props': runt.user.allowed(('node', 'prop', 'del'), gateiden=layr0.iden, deepdeny=True) and
3889
+ runt.user.allowed(('node', 'prop', 'set'), gateiden=layr1.iden, deepdeny=True),
3890
+ 'tags': runt.user.allowed(('node', 'tag', 'del'), gateiden=layr0.iden, deepdeny=True) and
3891
+ runt.user.allowed(('node', 'tag', 'add'), gateiden=layr1.iden, deepdeny=True),
3892
+ 'ndata': runt.user.allowed(('node', 'data', 'pop'), gateiden=layr0.iden, deepdeny=True) and
3893
+ runt.user.allowed(('node', 'data', 'set'), gateiden=layr1.iden, deepdeny=True),
3894
+ 'edges': runt.user.allowed(('node', 'edge', 'del'), gateiden=layr0.iden, deepdeny=True) and
3895
+ runt.user.allowed(('node', 'edge', 'add'), gateiden=layr1.iden, deepdeny=True),
3896
+ }
3897
+ else:
3898
+ allows = {
3899
+ 'forms': runt.user.allowed(('node', 'add'), gateiden=layr1.iden, deepdeny=True),
3900
+ 'props': runt.user.allowed(('node', 'prop', 'set'), gateiden=layr1.iden, deepdeny=True),
3901
+ 'tags': runt.user.allowed(('node', 'tag', 'add'), gateiden=layr1.iden, deepdeny=True),
3902
+ 'ndata': runt.user.allowed(('node', 'data', 'set'), gateiden=layr1.iden, deepdeny=True),
3903
+ 'edges': runt.user.allowed(('node', 'edge', 'add'), gateiden=layr1.iden, deepdeny=True),
3904
+ }
3882
3905
 
3883
3906
  doperms = not all(allows.values())
3884
3907
 
@@ -3966,7 +3989,8 @@ class MergeCmd(Cmd):
3966
3989
  if name == '.created':
3967
3990
  if doapply:
3968
3991
  protonode.props['.created'] = valu
3969
- subs.append((s_layer.EDIT_PROP_DEL, (name, valu, stortype), ()))
3992
+ if not self.opts.wipe:
3993
+ subs.append((s_layer.EDIT_PROP_DEL, (name, valu, stortype), ()))
3970
3994
  continue
3971
3995
 
3972
3996
  isset = False
@@ -3990,7 +4014,8 @@ class MergeCmd(Cmd):
3990
4014
  await runt.printf(f'{nodeiden} {form}:{name} = {valurepr}')
3991
4015
  else:
3992
4016
  await protonode.set(name, valu)
3993
- subs.append((s_layer.EDIT_PROP_DEL, (name, valu, stortype), ()))
4017
+ if not self.opts.wipe:
4018
+ subs.append((s_layer.EDIT_PROP_DEL, (name, valu, stortype), ()))
3994
4019
 
3995
4020
  if doapply and protonode is None:
3996
4021
  protonode = await editor.addNode(form, node.ndef[1], norminfo={})
@@ -4009,7 +4034,8 @@ class MergeCmd(Cmd):
4009
4034
  await runt.printf(f'{nodeiden} {form}#{tag}{valurepr}')
4010
4035
  else:
4011
4036
  await protonode.addTag(tag, valu)
4012
- subs.append((s_layer.EDIT_TAG_DEL, (tag, valu), ()))
4037
+ if not self.opts.wipe:
4038
+ subs.append((s_layer.EDIT_TAG_DEL, (tag, valu), ()))
4013
4039
 
4014
4040
  for tag, tagdict in sode.get('tagprops', {}).items():
4015
4041
 
@@ -4022,7 +4048,8 @@ class MergeCmd(Cmd):
4022
4048
  await runt.printf(f'{nodeiden} {form}#{tag}:{prop} = {valurepr}')
4023
4049
  else:
4024
4050
  await protonode.setTagProp(tag, prop, valu)
4025
- subs.append((s_layer.EDIT_TAGPROP_DEL, (tag, prop, valu, stortype), ()))
4051
+ if not self.opts.wipe:
4052
+ subs.append((s_layer.EDIT_TAGPROP_DEL, (tag, prop, valu, stortype), ()))
4026
4053
 
4027
4054
  if not onlytags or form == 'syn:tag':
4028
4055
 
@@ -4032,7 +4059,8 @@ class MergeCmd(Cmd):
4032
4059
  await runt.printf(f'{nodeiden} {form} DATA {name} = {valurepr}')
4033
4060
  else:
4034
4061
  await protonode.setData(name, valu)
4035
- subs.append((s_layer.EDIT_NODEDATA_DEL, (name, valu), ()))
4062
+ if not self.opts.wipe:
4063
+ subs.append((s_layer.EDIT_NODEDATA_DEL, (name, valu), ()))
4036
4064
 
4037
4065
  async for edge in s_coro.pause(layr0.iterNodeEdgesN1(node.buid)):
4038
4066
  name, dest = edge
@@ -4040,9 +4068,10 @@ class MergeCmd(Cmd):
4040
4068
  await runt.printf(f'{nodeiden} {form} +({name})> {dest}')
4041
4069
  else:
4042
4070
  await protonode.addEdge(name, dest)
4043
- subs.append((s_layer.EDIT_EDGE_DEL, edge, ()))
4071
+ if not self.opts.wipe:
4072
+ subs.append((s_layer.EDIT_EDGE_DEL, edge, ()))
4044
4073
 
4045
- if delnode:
4074
+ if delnode and not self.opts.wipe:
4046
4075
  subs.append((s_layer.EDIT_NODE_DEL, valu, ()))
4047
4076
 
4048
4077
  if doapply:
@@ -4057,6 +4086,9 @@ class MergeCmd(Cmd):
4057
4086
  runt.snap.clearCachedNode(node.buid)
4058
4087
  yield await runt.snap.getNodeByBuid(node.buid), path
4059
4088
 
4089
+ if doapply and self.opts.wipe:
4090
+ await runt.snap.view.swapLayer()
4091
+
4060
4092
  class MoveNodesCmd(Cmd):
4061
4093
  '''
4062
4094
  Move storage nodes between layers.
@@ -486,7 +486,7 @@ The ready column indicates that a service has entered into the realtime change w
486
486
  if $svcinfo.online {
487
487
  $nexusOffset = $_getNexus($name)
488
488
  } else {
489
- $nexusOffset = 'Service is not online. Will not attempt to retrieve its nexus offset.'
489
+ $nexusOffset = '<offline>'
490
490
  }
491
491
  }
492
492
  $name=$name.ljust(45)