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/aha.py CHANGED
@@ -12,6 +12,7 @@ import synapse.common as s_common
12
12
  import synapse.daemon as s_daemon
13
13
  import synapse.telepath as s_telepath
14
14
 
15
+ import synapse.lib.base as s_base
15
16
  import synapse.lib.cell as s_cell
16
17
  import synapse.lib.coro as s_coro
17
18
  import synapse.lib.nexus as s_nexus
@@ -125,11 +126,15 @@ class AhaServicesV1(s_httpapi.Handler):
125
126
 
126
127
  class AhaApi(s_cell.CellApi):
127
128
 
128
- async def getAhaUrls(self):
129
- ahaurls = self.cell._getAhaUrls()
130
- if ahaurls is not None:
131
- return ahaurls
132
- return()
129
+ @s_cell.adminapi()
130
+ async def addAhaClone(self, host, port=27492, conf=None):
131
+ return await self.cell.addAhaClone(host, port=port, conf=conf)
132
+
133
+ async def getAhaUrls(self, user='root'):
134
+ ahaurls = await self.cell.getAhaUrls(user=user)
135
+ if ahaurls is None:
136
+ return ()
137
+ return ahaurls
133
138
 
134
139
  async def getAhaSvc(self, name, filters=None):
135
140
  '''
@@ -260,7 +265,6 @@ class AhaApi(s_cell.CellApi):
260
265
  return await self.cell.signHostCsr(csrtext, signas=signas, sans=sans)
261
266
 
262
267
  async def signUserCsr(self, csrtext, signas=None):
263
-
264
268
  await self._reqUserAllowed(('aha', 'csr', 'user'))
265
269
  return await self.cell.signUserCsr(csrtext, signas=signas)
266
270
 
@@ -299,6 +303,20 @@ class AhaApi(s_cell.CellApi):
299
303
  async for item in self.cell.getAhaPools():
300
304
  yield item
301
305
 
306
+ async def getAhaServers(self):
307
+ return await self.cell.getAhaServers()
308
+
309
+ async def getAhaServer(self, host, port):
310
+ return await self.cell.getAhaServer(host, port)
311
+
312
+ @s_cell.adminapi()
313
+ async def addAhaServer(self, server):
314
+ return await self.cell.addAhaServer(server)
315
+
316
+ @s_cell.adminapi()
317
+ async def delAhaServer(self, host, port):
318
+ return await self.cell.delAhaServer(host, port)
319
+
302
320
  @s_cell.adminapi()
303
321
  async def addAhaSvcProv(self, name, provinfo=None):
304
322
  '''
@@ -341,6 +359,14 @@ class AhaApi(s_cell.CellApi):
341
359
  '''
342
360
  return await self.cell.clearAhaUserEnrolls()
343
361
 
362
+ @s_cell.adminapi()
363
+ async def clearAhaClones(self):
364
+ '''
365
+ Remove all unused AHA clone provisioning values.
366
+ '''
367
+ return await self.cell.clearAhaClones()
368
+
369
+
344
370
  class ProvDmon(s_daemon.Daemon):
345
371
 
346
372
  async def __anit__(self, aha):
@@ -366,10 +392,34 @@ class ProvDmon(s_daemon.Daemon):
366
392
  await self.aha.delAhaUserEnroll(name)
367
393
  return EnrollApi(self.aha, userinfo)
368
394
 
395
+ clone = await self.aha.getAhaClone(name)
396
+ if clone is not None:
397
+ host = clone.get('host')
398
+ mesg = f'Retrieved AHA clone info for {host} iden {name}'
399
+ logger.info(mesg, extra=await self.aha.getLogExtra(iden=name, host=host))
400
+ return CloneApi(self.aha, clone)
401
+
369
402
  mesg = f'Invalid provisioning identifier name={name}. This could be' \
370
403
  f' caused by the re-use of a provisioning URL.'
371
404
  raise s_exc.NoSuchName(mesg=mesg, name=name)
372
405
 
406
+ class CloneApi:
407
+
408
+ def __init__(self, aha, clone):
409
+ self.aha = aha
410
+ self.clone = clone
411
+
412
+ async def getCloneDef(self):
413
+ return self.clone
414
+
415
+ async def readyToMirror(self):
416
+ return await self.aha.readyToMirror()
417
+
418
+ async def iterNewBackupArchive(self, name=None, remove=False):
419
+ async with self.aha.getLocalProxy() as proxy:
420
+ async for byts in proxy.iterNewBackupArchive(name=name, remove=remove):
421
+ yield byts
422
+
373
423
  class EnrollApi:
374
424
 
375
425
  def __init__(self, aha, userinfo):
@@ -377,20 +427,21 @@ class EnrollApi:
377
427
  self.userinfo = userinfo
378
428
 
379
429
  async def getUserInfo(self):
430
+ user = self.userinfo.get('name')
380
431
  return {
381
- 'aha:urls': self.aha._getAhaUrls(),
382
- 'aha:user': self.userinfo.get('name'),
383
- 'aha:network': self.aha.conf.get('aha:network'),
432
+ 'aha:urls': await self.aha.getAhaUrls(user=user),
433
+ 'aha:user': user,
434
+ 'aha:network': self.aha.conf.req('aha:network'),
384
435
  }
385
436
 
386
437
  async def getCaCert(self):
387
- ahanetw = self.aha.conf.get('aha:network')
438
+ ahanetw = self.aha.conf.req('aha:network')
388
439
  return self.aha.certdir.getCaCertBytes(ahanetw)
389
440
 
390
441
  async def signUserCsr(self, byts):
391
442
 
392
443
  ahauser = self.userinfo.get('name')
393
- ahanetw = self.aha.conf.get('aha:network')
444
+ ahanetw = self.aha.conf.req('aha:network')
394
445
 
395
446
  username = f'{ahauser}@{ahanetw}'
396
447
 
@@ -416,7 +467,7 @@ class ProvApi:
416
467
  return self.provinfo
417
468
 
418
469
  async def getCaCert(self):
419
- ahanetw = self.aha.conf.get('aha:network')
470
+ ahanetw = self.aha.conf.req('aha:network')
420
471
  return self.aha.certdir.getCaCertBytes(ahanetw)
421
472
 
422
473
  async def signHostCsr(self, byts):
@@ -464,8 +515,17 @@ class AhaCell(s_cell.Cell):
464
515
  confbase['mirror']['hidedocs'] = False # type: ignore
465
516
  confbase['mirror']['hidecmdl'] = False # type: ignore
466
517
  confdefs = {
518
+ 'clone': {
519
+ 'hidecmdl': True,
520
+ 'description': 'Bootstrap a clone from the AHA clone URL.',
521
+ 'type': ['string', 'null'],
522
+ },
523
+ 'dns:name': {
524
+ 'description': 'The registered DNS name used to reach the AHA service.',
525
+ 'type': ['string', 'null'],
526
+ },
467
527
  'aha:urls': {
468
- 'description': 'A list of all available AHA server URLs.',
528
+ 'description': 'Deprecated. AHA servers can now manage this automatically.',
469
529
  'type': ['string', 'array'],
470
530
  'items': {'type': 'string'},
471
531
  },
@@ -480,6 +540,35 @@ class AhaCell(s_cell.Cell):
480
540
  def getEnvPrefix(cls):
481
541
  return (f'SYN_AHA', f'SYN_{cls.__name__.upper()}', )
482
542
 
543
+ async def _initCellBoot(self):
544
+
545
+ curl = self.conf.get('clone')
546
+ if curl is None:
547
+ return
548
+
549
+ path = s_common.genpath(self.dirn, 'cell.guid')
550
+ if os.path.isfile(path):
551
+ logger.info('Cloning AHA: cell.guid detected. Skipping.')
552
+ return
553
+
554
+ logger.warning(f'Cloning AHA: {curl}')
555
+
556
+ async with await s_telepath.openurl(curl) as proxy:
557
+ clone = await proxy.getCloneDef()
558
+ await self._initCloneCell(proxy)
559
+
560
+ logger.warning('Cloning AHA: done!')
561
+
562
+ conf = s_common.yamlload(self.dirn, 'cell.yaml')
563
+ if conf is None:
564
+ conf = {}
565
+
566
+ conf.update(clone.get('conf', {}))
567
+
568
+ s_common.yamlsave(conf, self.dirn, 'cell.yaml')
569
+
570
+ self.conf.update(conf)
571
+
483
572
  async def initServiceStorage(self):
484
573
 
485
574
  # TODO plumb using a remote jsonstor?
@@ -499,9 +588,66 @@ class AhaCell(s_cell.Cell):
499
588
  self.slab.initdb('aha:provs')
500
589
  self.slab.initdb('aha:enrolls')
501
590
 
591
+ self.slab.initdb('aha:clones')
592
+ self.slab.initdb('aha:servers')
593
+
502
594
  self.slab.initdb('aha:pools')
595
+
503
596
  self.poolwindows = collections.defaultdict(list)
504
597
 
598
+ async def getAhaServer(self, host, port):
599
+ lkey = s_msgpack.en((host, port))
600
+ byts = self.slab.get(lkey, db='aha:servers')
601
+ if byts is not None:
602
+ return s_msgpack.un(byts)
603
+
604
+ async def addAhaServer(self, server):
605
+
606
+ host = server.get('host')
607
+ port = server.setdefault('port', 27492)
608
+
609
+ # avoid a noop nexus change...
610
+ oldv = await self.getAhaServer(host, port)
611
+ if s_common.flatten(server) == s_common.flatten(oldv):
612
+ return False
613
+
614
+ return await self._push('aha:server:add', server)
615
+
616
+ @s_nexus.Pusher.onPush('aha:server:add')
617
+ async def _addAhaServer(self, server):
618
+ # TODO schema
619
+ host = server.get('host')
620
+ port = server.get('port')
621
+
622
+ lkey = s_msgpack.en((host, port))
623
+
624
+ byts = self.slab.get(lkey, db='aha:servers')
625
+ if byts is not None:
626
+ oldv = s_msgpack.un(byts)
627
+ if s_common.flatten(server) == s_common.flatten(oldv):
628
+ return False
629
+
630
+ self.slab.put(lkey, s_msgpack.en(server), db='aha:servers')
631
+
632
+ return True
633
+
634
+ @s_nexus.Pusher.onPushAuto('aha:server:del')
635
+ async def delAhaServer(self, host, port):
636
+
637
+ lkey = s_msgpack.en((host, port))
638
+
639
+ byts = self.slab.pop(lkey, db='aha:servers')
640
+ if byts is None:
641
+ return None
642
+
643
+ return s_msgpack.un(byts)
644
+
645
+ async def getAhaServers(self):
646
+ servers = []
647
+ for _, byts in self.slab.scanByFull(db='aha:servers'):
648
+ servers.append(s_msgpack.un(byts))
649
+ return servers
650
+
505
651
  async def iterPoolTopo(self, name):
506
652
 
507
653
  name = self._getAhaName(name)
@@ -537,41 +683,100 @@ class AhaCell(s_cell.Cell):
537
683
  self.addHttpApi('/api/v1/aha/provision/service', AhaProvisionServiceV1, {'cell': self})
538
684
 
539
685
  async def initServiceRuntime(self):
686
+
540
687
  self.addActiveCoro(self._clearInactiveSessions)
541
688
 
542
689
  if self.isactive:
543
- # bootstrap a CA for our aha:network
544
- netw = self.conf.get('aha:network')
545
- if netw is not None:
546
690
 
547
- if self.certdir.getCaCertPath(netw) is None:
548
- logger.info(f'Adding CA certificate for {netw}')
549
- await self.genCaCert(netw)
691
+ # bootstrap a CA for our aha:network
692
+ netw = self.conf.req('aha:network')
550
693
 
551
- name = self.conf.get('aha:name')
694
+ if self.certdir.getCaCertPath(netw) is None:
695
+ logger.info(f'Adding CA certificate for {netw}')
696
+ await self.genCaCert(netw)
552
697
 
698
+ name = self.conf.get('aha:name')
699
+ if name is not None:
553
700
  host = f'{name}.{netw}'
554
701
  if self.certdir.getHostCertPath(host) is None:
555
702
  logger.info(f'Adding server certificate for {host}')
556
703
  await self._genHostCert(host, signas=netw)
557
704
 
558
- user = self._getAhaAdmin()
559
- if user is not None:
560
- if self.certdir.getUserCertPath(user) is None:
561
- logger.info(f'Adding user certificate for {user}@{netw}')
562
- await self._genUserCert(user, signas=netw)
705
+ root = f'root@{netw}'
706
+ await self._genUserCert(root, signas=netw)
707
+
708
+ user = self.conf.get('aha:admin')
709
+ if user is not None:
710
+ await self._genUserCert(user, signas=netw)
711
+
712
+ def _getDnsName(self):
713
+ # emulate the old aha name.network behavior if the
714
+ # explicit option is not set.
715
+
716
+ hostname = self.conf.get('dns:name')
717
+ if hostname is not None:
718
+ return hostname
719
+
720
+ ahaname = self.conf.get('aha:name')
721
+ ahanetw = self.conf.get('aha:network')
722
+ if ahaname is not None and ahanetw is not None:
723
+ return f'{ahaname}.{ahanetw}'
724
+
725
+ def _getProvListen(self):
726
+
727
+ lisn = self.conf.get('provision:listen')
728
+ if lisn is not None:
729
+ return lisn
730
+
731
+ # this may not use _getDnsName() in order to maintain
732
+ # backward compatibilty with aha name.network configs
733
+ # that do not intend to listen for provisioning.
734
+ hostname = self.conf.get('dns:name')
735
+ if hostname is not None:
736
+ return f'ssl://{hostname}:27272'
737
+
738
+ def _getDmonListen(self):
739
+
740
+ lisn = self.conf.get('dmon:listen', s_common.novalu)
741
+ if lisn is not s_common.novalu:
742
+ return lisn
743
+
744
+ network = self.conf.req('aha:network')
745
+ dnsname = self._getDnsName()
746
+ if dnsname is not None:
747
+ return f'ssl://0.0.0.0?hostname={dnsname}&ca={network}'
748
+
749
+ def _reqProvListen(self):
750
+ lisn = self._getProvListen()
751
+ if lisn is not None:
752
+ return lisn
753
+
754
+ mesg = 'The AHA server is not configured for provisioning.'
755
+ raise s_exc.NeedConfValu(mesg=mesg)
563
756
 
564
757
  async def initServiceNetwork(self):
565
758
 
759
+ # bootstrap CA/host certs first
760
+ network = self.conf.req('aha:network')
761
+
762
+ hostname = self._getDnsName()
763
+ if hostname is not None and network is not None:
764
+ await self._genHostCert(hostname, signas=network)
765
+
566
766
  await s_cell.Cell.initServiceNetwork(self)
567
767
 
768
+ # all AHA mirrors are registered
769
+ if hostname is not None and self.sockaddr is not None:
770
+ server = {'host': hostname, 'port': self.sockaddr[1]}
771
+ await self.addAhaServer(server)
772
+
568
773
  self.provdmon = None
569
774
 
570
- provurl = self.conf.get('provision:listen')
775
+ provurl = self._getProvListen()
571
776
  if provurl is not None:
572
777
  self.provdmon = await ProvDmon.anit(self)
573
778
  self.onfini(self.provdmon)
574
- await self.provdmon.listen(provurl)
779
+ self.provaddr = await self.provdmon.listen(provurl)
575
780
 
576
781
  async def _clearInactiveSessions(self):
577
782
 
@@ -590,6 +795,41 @@ class AhaCell(s_cell.Cell):
590
795
  # Wait until we are cancelled or the cell is fini.
591
796
  await self.waitfini()
592
797
 
798
+ async def _waitAhaSvcOnline(self, name, timeout=None):
799
+
800
+ name = self._getAhaName(name)
801
+
802
+ while True:
803
+
804
+ async with self.nexslock:
805
+
806
+ retn = await self.getAhaSvc(name)
807
+ if retn['svcinfo'].get('online') is not None:
808
+ return retn
809
+
810
+ waiter = self.waiter(1, f'aha:svcadd:{name}')
811
+
812
+ if await waiter.wait(timeout=timeout) is None:
813
+ raise s_exc.TimeOut(mesg=f'Timeout waiting for aha:svcadd:{name}')
814
+
815
+ async def _waitAhaSvcDown(self, name, timeout=None):
816
+
817
+ name = self._getAhaName(name)
818
+
819
+ while True:
820
+
821
+ async with self.nexslock:
822
+
823
+ retn = await self.getAhaSvc(name)
824
+ online = retn['svcinfo'].get('online')
825
+ if online is None:
826
+ return retn
827
+
828
+ waiter = self.waiter(1, f'aha:svcdown:{name}')
829
+
830
+ if await waiter.wait(timeout=timeout) is None:
831
+ raise s_exc.TimeOut(mesg=f'Timeout waiting for aha:svcdown:{name}')
832
+
593
833
  async def getAhaSvcs(self, network=None):
594
834
  path = ('aha', 'services')
595
835
  if network is not None:
@@ -654,15 +894,12 @@ class AhaCell(s_cell.Cell):
654
894
 
655
895
  # mostly for testing...
656
896
  await self.fire('aha:svcadd', svcinfo=svcinfo)
897
+ await self.fire(f'aha:svcadd:{svcfull}', svcinfo=svcinfo)
657
898
 
658
899
  def _getAhaName(self, name):
659
900
  # the modern version of names is absolute or ...
660
901
  if name.endswith('...'):
661
- netw = self.conf.get('aha:network')
662
- if netw is None: # pragma: no cover
663
- mesg = 'AHA Server requires aha:network configuration.'
664
- raise s_exc.NeedConfValu(mesg=mesg)
665
- name = name[:-2] + netw
902
+ return name[:-2] + self.conf.req('aha:network')
666
903
  return name
667
904
 
668
905
  async def getAhaPool(self, name):
@@ -780,6 +1017,7 @@ class AhaCell(s_cell.Cell):
780
1017
  @s_nexus.Pusher.onPushAuto('aha:svc:del')
781
1018
  async def delAhaSvc(self, name, network=None):
782
1019
 
1020
+ name = self._getAhaName(name)
783
1021
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
784
1022
 
785
1023
  logger.info(f'Deleting service [{svcfull}].', extra=await self.getLogExtra(name=svcname, netw=svcnetw))
@@ -794,6 +1032,8 @@ class AhaCell(s_cell.Cell):
794
1032
  await self.fire('aha:svcdel', svcname=svcname, svcnetw=svcnetw)
795
1033
 
796
1034
  async def setAhaSvcDown(self, name, linkiden, network=None):
1035
+
1036
+ name = self._getAhaName(name)
797
1037
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
798
1038
  path = ('aha', 'services', svcnetw, svcname)
799
1039
 
@@ -805,8 +1045,10 @@ class AhaCell(s_cell.Cell):
805
1045
 
806
1046
  @s_nexus.Pusher.onPush('aha:svc:down')
807
1047
  async def _setAhaSvcDown(self, name, linkiden, network=None):
1048
+
808
1049
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
809
1050
  path = ('aha', 'services', svcnetw, svcname)
1051
+
810
1052
  if await self.jsonstor.cmpDelPathObjProp(path, 'svcinfo/online', linkiden):
811
1053
  await self.jsonstor.setPathObjProp(path, 'svcinfo/ready', False)
812
1054
 
@@ -818,6 +1060,7 @@ class AhaCell(s_cell.Cell):
818
1060
  await link.fini()
819
1061
 
820
1062
  await self.fire('aha:svcdown', svcname=svcname, svcnetw=svcnetw)
1063
+ await self.fire(f'aha:svcdown:{svcfull}', svcname=svcname, svcnetw=svcnetw)
821
1064
 
822
1065
  logger.info(f'Set [{svcfull}] offline.',
823
1066
  extra=await self.getLogExtra(name=svcname, netw=svcnetw))
@@ -918,7 +1161,7 @@ class AhaCell(s_cell.Cell):
918
1161
 
919
1162
  async def _genHostCert(self, hostname, signas=None):
920
1163
 
921
- if os.path.isfile(os.path.join(self.dirn, 'certs', 'hosts', '{hostname}.crt')):
1164
+ if self.certdir.getHostCertPath(hostname) is not None:
922
1165
  return
923
1166
 
924
1167
  pkey, cert = await s_coro.executor(self.certdir.genHostCert, hostname, signas=signas, save=False)
@@ -927,8 +1170,12 @@ class AhaCell(s_cell.Cell):
927
1170
  await self.saveHostCert(hostname, pkey, cert)
928
1171
 
929
1172
  async def _genUserCert(self, username, signas=None):
930
- if os.path.isfile(os.path.join(self.dirn, 'certs', 'users', '{username}.crt')):
1173
+
1174
+ if self.certdir.getUserCertPath(username) is not None:
931
1175
  return
1176
+
1177
+ logger.info(f'Adding user certificate for {username}')
1178
+
932
1179
  pkey, cert = await s_coro.executor(self.certdir.genUserCert, username, signas=signas, save=False)
933
1180
  pkey = self.certdir._pkeyToByts(pkey).decode()
934
1181
  cert = self.certdir._certToByts(cert).decode()
@@ -971,8 +1218,8 @@ class AhaCell(s_cell.Cell):
971
1218
 
972
1219
  hostname = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value
973
1220
 
974
- hostpath = s_common.genpath(self.dirn, 'certs', 'hosts', f'{hostname}.crt')
975
- if os.path.isfile(hostpath):
1221
+ hostpath = self.certdir.getHostCertPath(hostname)
1222
+ if hostpath is not None:
976
1223
  os.unlink(hostpath)
977
1224
 
978
1225
  if signas is None:
@@ -990,8 +1237,8 @@ class AhaCell(s_cell.Cell):
990
1237
 
991
1238
  username = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value
992
1239
 
993
- userpath = s_common.genpath(self.dirn, 'certs', 'users', f'{username}.crt')
994
- if os.path.isfile(userpath):
1240
+ userpath = self.certdir.getUserCertPath(username)
1241
+ if userpath is not None:
995
1242
  os.unlink(userpath)
996
1243
 
997
1244
  if signas is None:
@@ -1004,41 +1251,75 @@ class AhaCell(s_cell.Cell):
1004
1251
 
1005
1252
  return self.certdir._certToByts(cert).decode()
1006
1253
 
1007
- def _getAhaUrls(self):
1254
+ async def getAhaUrls(self, user='root'):
1255
+
1256
+ # for backward compat...
1008
1257
  urls = self.conf.get('aha:urls')
1009
1258
  if urls is not None:
1010
- if isinstance(urls, str):
1011
- return (urls,)
1012
1259
  return urls
1013
1260
 
1014
- ahaname = self.conf.get('aha:name')
1015
- if ahaname is None:
1016
- return None
1261
+ network = self.conf.req('aha:network')
1017
1262
 
1018
- ahanetw = self.conf.get('aha:network')
1019
- if ahanetw is None:
1020
- return None
1263
+ urls = []
1264
+ for server in await self.getAhaServers():
1265
+ host = server.get('host')
1266
+ port = server.get('port')
1267
+ urls.append(f'ssl://{host}:{port}?certname={user}@{network}')
1021
1268
 
1022
- if self.sockaddr is None or not isinstance(self.sockaddr, tuple):
1023
- return None
1269
+ return urls
1024
1270
 
1025
- # TODO this could eventually enumerate others via itself
1026
- return (f'ssl://{ahaname}.{ahanetw}:{self.sockaddr[1]}',)
1271
+ def getMyUrl(self, user='root'):
1272
+ port = self.sockaddr[1]
1273
+ host = self._getDnsName()
1274
+ network = self.conf.req('aha:network')
1275
+ return f'ssl://{host}:{port}?certname={user}@{network}'
1027
1276
 
1028
- async def addAhaSvcProv(self, name, provinfo=None):
1277
+ async def getAhaClone(self, iden):
1278
+ lkey = s_common.uhex(iden)
1279
+ byts = self.slab.get(lkey, db='aha:clones')
1280
+ if byts is not None:
1281
+ return s_msgpack.un(byts)
1029
1282
 
1030
- ahaurls = self._getAhaUrls()
1031
- if ahaurls is None:
1032
- mesg = 'AHA server has no configured aha:urls.'
1033
- raise s_exc.NeedConfValu(mesg=mesg)
1283
+ async def addAhaClone(self, host, port=27492, conf=None):
1284
+
1285
+ if conf is None:
1286
+ conf = {}
1287
+
1288
+ network = self.conf.req('aha:network')
1289
+
1290
+ conf['mirror'] = self.getMyUrl()
1291
+
1292
+ conf['dns:name'] = host
1293
+ conf['aha:network'] = network
1294
+ conf['dmon:listen'] = f'ssl://0.0.0.0:{port}?hostname={host}&ca={network}'
1295
+
1296
+ iden = s_common.guid()
1297
+ clone = {
1298
+ 'iden': iden,
1299
+ 'host': host,
1300
+ 'port': port,
1301
+ 'conf': conf,
1302
+ }
1303
+ await self._push('aha:clone:add', clone)
1304
+
1305
+ logger.info(f'Created AHA clone provisioning for {host} with iden {iden}',
1306
+ extra=await self.getLogExtra(iden=iden, name=host, netw=network))
1307
+
1308
+ return self._getProvClientUrl(iden)
1034
1309
 
1035
- if self.conf.get('provision:listen') is None:
1036
- mesg = 'The AHA server does not have a provision:listen URL!'
1037
- raise s_exc.NeedConfValu(mesg=mesg)
1310
+ @s_nexus.Pusher.onPush('aha:clone:add')
1311
+ async def _addAhaClone(self, clone):
1312
+ iden = clone.get('iden')
1313
+ lkey = s_common.uhex(iden)
1314
+ self.slab.put(lkey, s_msgpack.en(clone), db='aha:clones')
1315
+
1316
+ async def addAhaSvcProv(self, name, provinfo=None):
1038
1317
 
1039
1318
  if not name:
1040
1319
  raise s_exc.BadArg(mesg='Empty name values are not allowed for provisioning.')
1041
1320
 
1321
+ self._reqProvListen()
1322
+
1042
1323
  if provinfo is None:
1043
1324
  provinfo = {}
1044
1325
 
@@ -1048,23 +1329,16 @@ class AhaCell(s_cell.Cell):
1048
1329
 
1049
1330
  conf = provinfo.setdefault('conf', {})
1050
1331
 
1051
- mynetw = self.conf.get('aha:network')
1332
+ netw = self.conf.req('aha:network')
1052
1333
 
1053
1334
  ahaadmin = self.conf.get('aha:admin')
1054
1335
  if ahaadmin is not None: # pragma: no cover
1055
1336
  conf.setdefault('aha:admin', ahaadmin)
1056
1337
 
1057
- conf.setdefault('aha:user', 'root')
1058
- conf.setdefault('aha:network', mynetw)
1059
-
1060
- netw = conf.get('aha:network')
1061
- if netw is None:
1062
- mesg = 'AHA server has no configured aha:network.'
1063
- raise s_exc.NeedConfValu(mesg=mesg)
1338
+ ahauser = conf.setdefault('aha:user', 'root')
1339
+ ahaurls = await self.getAhaUrls(user=ahauser)
1064
1340
 
1065
- if netw != mynetw:
1066
- mesg = f'Provisioning aha:network must be equal to the Aha servers network. Expected {mynetw}, got {netw}'
1067
- raise s_exc.BadConfValu(mesg=mesg, name='aha:network', expected=mynetw, got=netw)
1341
+ conf['aha:network'] = netw
1068
1342
 
1069
1343
  hostname = f'{name}.{netw}'
1070
1344
 
@@ -1088,15 +1362,11 @@ class AhaCell(s_cell.Cell):
1088
1362
  if peer:
1089
1363
  conf.setdefault('aha:leader', leader)
1090
1364
 
1091
- # allow user to win over leader
1092
- ahauser = conf.get('aha:user')
1093
- ahaurls = s_telepath.modurl(ahaurls, user=ahauser)
1094
-
1095
1365
  conf.setdefault('aha:registry', ahaurls)
1096
1366
 
1097
1367
  mirname = provinfo.get('mirror')
1098
1368
  if mirname is not None:
1099
- conf['mirror'] = f'aha://{ahauser}@{mirname}.{netw}'
1369
+ conf['mirror'] = f'aha://{ahauser}@{mirname}...'
1100
1370
 
1101
1371
  user = await self.auth.getUserByName(ahauser)
1102
1372
  if user is None:
@@ -1122,7 +1392,10 @@ class AhaCell(s_cell.Cell):
1122
1392
 
1123
1393
  def _getProvClientUrl(self, iden):
1124
1394
 
1125
- provlisn = self.conf.get('provision:listen')
1395
+ provlisn = self._getProvListen()
1396
+
1397
+ provport = self.provaddr[1]
1398
+ provhost = self._getDnsName()
1126
1399
 
1127
1400
  urlinfo = s_telepath.chopurl(provlisn)
1128
1401
 
@@ -1133,8 +1406,8 @@ class AhaCell(s_cell.Cell):
1133
1406
  host = urlinfo.get('host')
1134
1407
 
1135
1408
  newinfo = {
1136
- 'host': host,
1137
- 'port': urlinfo.get('port'),
1409
+ 'host': provhost,
1410
+ 'port': provport,
1138
1411
  'scheme': scheme,
1139
1412
  'path': '/' + iden,
1140
1413
  }
@@ -1171,25 +1444,25 @@ class AhaCell(s_cell.Cell):
1171
1444
  userinfo = s_msgpack.un(byts)
1172
1445
  logger.info(f'Deleted user enrollment username={userinfo.get("name")}, iden={iden.decode()}')
1173
1446
 
1447
+ @s_nexus.Pusher.onPushAuto('aha:clone:clear')
1448
+ async def clearAhaClones(self):
1449
+ for lkey, byts in self.slab.scanByFull(db='aha:clones'):
1450
+ self.slab.delete(lkey, db='aha:clones')
1451
+ cloninfo = s_msgpack.un(byts)
1452
+ logger.info(f'Deleted AHA clone enrollment username={cloninfo.get("host")}, iden={s_common.ehex(lkey)}')
1453
+
1174
1454
  @s_nexus.Pusher.onPushAuto('aha:svc:prov:del')
1175
1455
  async def delAhaSvcProv(self, iden):
1176
1456
  self.slab.delete(iden.encode(), db='aha:provs')
1177
1457
 
1178
1458
  async def addAhaUserEnroll(self, name, userinfo=None, again=False):
1179
1459
 
1180
- provurl = self.conf.get('provision:listen')
1181
- if provurl is None:
1182
- mesg = 'The AHA server does not have a provision:listen URL!'
1183
- raise s_exc.NeedConfValu(mesg=mesg)
1184
-
1185
- ahanetw = self.conf.get('aha:network')
1186
- if ahanetw is None:
1187
- mesg = 'AHA server requires aha:network configuration.'
1188
- raise s_exc.NeedConfValu(mesg=mesg)
1189
-
1190
1460
  if not name:
1191
1461
  raise s_exc.BadArg(mesg='Empty name values are not allowed for provisioning.')
1192
1462
 
1463
+ provurl = self._reqProvListen()
1464
+ ahanetw = self.conf.req('aha:network')
1465
+
1193
1466
  username = f'{name}@{ahanetw}'
1194
1467
 
1195
1468
  if len(username) > 64: