synapse 2.177.0__py311-none-any.whl → 2.179.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 (73) hide show
  1. synapse/cortex.py +170 -31
  2. synapse/datamodel.py +47 -1
  3. synapse/exc.py +1 -0
  4. synapse/lib/aha.py +362 -88
  5. synapse/lib/ast.py +26 -22
  6. synapse/lib/base.py +39 -12
  7. synapse/lib/cell.py +315 -119
  8. synapse/lib/config.py +15 -11
  9. synapse/lib/coro.py +27 -0
  10. synapse/lib/drive.py +551 -0
  11. synapse/lib/layer.py +0 -5
  12. synapse/lib/link.py +1 -1
  13. synapse/lib/lmdbslab.py +3 -3
  14. synapse/lib/nexus.py +24 -12
  15. synapse/lib/schemas.py +39 -0
  16. synapse/lib/snap.py +17 -7
  17. synapse/lib/storm.py +3 -1
  18. synapse/lib/stormhttp.py +1 -0
  19. synapse/lib/stormlib/imap.py +6 -2
  20. synapse/lib/stormlib/modelext.py +29 -3
  21. synapse/lib/stormlib/smtp.py +12 -2
  22. synapse/lib/stormlib/stix.py +40 -17
  23. synapse/lib/stormlib/vault.py +2 -2
  24. synapse/lib/stormtypes.py +1 -1
  25. synapse/lib/types.py +9 -0
  26. synapse/lib/version.py +2 -2
  27. synapse/lookup/pe.py +303 -38
  28. synapse/models/dns.py +24 -1
  29. synapse/models/geospace.py +4 -1
  30. synapse/models/infotech.py +26 -1
  31. synapse/telepath.py +32 -17
  32. synapse/tests/files/aha/certs/cas/synapse.crt +28 -0
  33. synapse/tests/files/aha/certs/cas/synapse.key +51 -0
  34. synapse/tests/files/aha/certs/hosts/00.aha.loop.vertex.link.crt +30 -0
  35. synapse/tests/files/aha/certs/hosts/00.aha.loop.vertex.link.key +51 -0
  36. synapse/tests/files/aha/certs/users/root@synapse.crt +29 -0
  37. synapse/tests/files/aha/certs/users/root@synapse.key +51 -0
  38. synapse/tests/files/rstorm/testsvc.py +1 -1
  39. synapse/tests/test_axon.py +1 -1
  40. synapse/tests/test_cortex.py +67 -60
  41. synapse/tests/test_lib_agenda.py +3 -3
  42. synapse/tests/test_lib_aha.py +353 -490
  43. synapse/tests/test_lib_base.py +20 -0
  44. synapse/tests/test_lib_cell.py +273 -22
  45. synapse/tests/test_lib_config.py +4 -3
  46. synapse/tests/test_lib_coro.py +12 -0
  47. synapse/tests/test_lib_nexus.py +8 -0
  48. synapse/tests/test_lib_stormhttp.py +40 -0
  49. synapse/tests/test_lib_stormlib_aha.py +35 -35
  50. synapse/tests/test_lib_stormlib_cell.py +4 -15
  51. synapse/tests/test_lib_stormlib_imap.py +14 -3
  52. synapse/tests/test_lib_stormlib_modelext.py +55 -3
  53. synapse/tests/test_lib_stormlib_smtp.py +51 -0
  54. synapse/tests/test_lib_stormlib_stix.py +15 -0
  55. synapse/tests/test_lib_stormlib_vault.py +11 -1
  56. synapse/tests/test_lib_stormtypes.py +5 -0
  57. synapse/tests/test_lib_types.py +9 -0
  58. synapse/tests/test_model_dns.py +8 -0
  59. synapse/tests/test_model_geospace.py +3 -1
  60. synapse/tests/test_model_infotech.py +47 -0
  61. synapse/tests/test_model_syn.py +11 -0
  62. synapse/tests/test_tools_aha.py +78 -101
  63. synapse/tests/test_utils_stormcov.py +1 -1
  64. synapse/tests/utils.py +86 -120
  65. synapse/tools/aha/clone.py +50 -0
  66. synapse/tools/aha/enroll.py +2 -1
  67. synapse/tools/backup.py +2 -2
  68. synapse/tools/changelog.py +31 -1
  69. {synapse-2.177.0.dist-info → synapse-2.179.0.dist-info}/METADATA +48 -48
  70. {synapse-2.177.0.dist-info → synapse-2.179.0.dist-info}/RECORD +73 -65
  71. {synapse-2.177.0.dist-info → synapse-2.179.0.dist-info}/WHEEL +1 -1
  72. {synapse-2.177.0.dist-info → synapse-2.179.0.dist-info}/LICENSE +0 -0
  73. {synapse-2.177.0.dist-info → synapse-2.179.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,101 @@ 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://0.0.0.0:27272?hostname={hostname}'
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
+ logger.info(f'provision listening: {provurl}')
780
+ self.provaddr = await self.provdmon.listen(provurl)
575
781
 
576
782
  async def _clearInactiveSessions(self):
577
783
 
@@ -590,6 +796,41 @@ class AhaCell(s_cell.Cell):
590
796
  # Wait until we are cancelled or the cell is fini.
591
797
  await self.waitfini()
592
798
 
799
+ async def _waitAhaSvcOnline(self, name, timeout=None):
800
+
801
+ name = self._getAhaName(name)
802
+
803
+ while True:
804
+
805
+ async with self.nexslock:
806
+
807
+ retn = await self.getAhaSvc(name)
808
+ if retn['svcinfo'].get('online') is not None:
809
+ return retn
810
+
811
+ waiter = self.waiter(1, f'aha:svcadd:{name}')
812
+
813
+ if await waiter.wait(timeout=timeout) is None:
814
+ raise s_exc.TimeOut(mesg=f'Timeout waiting for aha:svcadd:{name}')
815
+
816
+ async def _waitAhaSvcDown(self, name, timeout=None):
817
+
818
+ name = self._getAhaName(name)
819
+
820
+ while True:
821
+
822
+ async with self.nexslock:
823
+
824
+ retn = await self.getAhaSvc(name)
825
+ online = retn['svcinfo'].get('online')
826
+ if online is None:
827
+ return retn
828
+
829
+ waiter = self.waiter(1, f'aha:svcdown:{name}')
830
+
831
+ if await waiter.wait(timeout=timeout) is None:
832
+ raise s_exc.TimeOut(mesg=f'Timeout waiting for aha:svcdown:{name}')
833
+
593
834
  async def getAhaSvcs(self, network=None):
594
835
  path = ('aha', 'services')
595
836
  if network is not None:
@@ -654,15 +895,12 @@ class AhaCell(s_cell.Cell):
654
895
 
655
896
  # mostly for testing...
656
897
  await self.fire('aha:svcadd', svcinfo=svcinfo)
898
+ await self.fire(f'aha:svcadd:{svcfull}', svcinfo=svcinfo)
657
899
 
658
900
  def _getAhaName(self, name):
659
901
  # the modern version of names is absolute or ...
660
902
  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
903
+ return name[:-2] + self.conf.req('aha:network')
666
904
  return name
667
905
 
668
906
  async def getAhaPool(self, name):
@@ -780,6 +1018,7 @@ class AhaCell(s_cell.Cell):
780
1018
  @s_nexus.Pusher.onPushAuto('aha:svc:del')
781
1019
  async def delAhaSvc(self, name, network=None):
782
1020
 
1021
+ name = self._getAhaName(name)
783
1022
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
784
1023
 
785
1024
  logger.info(f'Deleting service [{svcfull}].', extra=await self.getLogExtra(name=svcname, netw=svcnetw))
@@ -794,6 +1033,8 @@ class AhaCell(s_cell.Cell):
794
1033
  await self.fire('aha:svcdel', svcname=svcname, svcnetw=svcnetw)
795
1034
 
796
1035
  async def setAhaSvcDown(self, name, linkiden, network=None):
1036
+
1037
+ name = self._getAhaName(name)
797
1038
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
798
1039
  path = ('aha', 'services', svcnetw, svcname)
799
1040
 
@@ -805,8 +1046,10 @@ class AhaCell(s_cell.Cell):
805
1046
 
806
1047
  @s_nexus.Pusher.onPush('aha:svc:down')
807
1048
  async def _setAhaSvcDown(self, name, linkiden, network=None):
1049
+
808
1050
  svcname, svcnetw, svcfull = self._nameAndNetwork(name, network)
809
1051
  path = ('aha', 'services', svcnetw, svcname)
1052
+
810
1053
  if await self.jsonstor.cmpDelPathObjProp(path, 'svcinfo/online', linkiden):
811
1054
  await self.jsonstor.setPathObjProp(path, 'svcinfo/ready', False)
812
1055
 
@@ -818,6 +1061,7 @@ class AhaCell(s_cell.Cell):
818
1061
  await link.fini()
819
1062
 
820
1063
  await self.fire('aha:svcdown', svcname=svcname, svcnetw=svcnetw)
1064
+ await self.fire(f'aha:svcdown:{svcfull}', svcname=svcname, svcnetw=svcnetw)
821
1065
 
822
1066
  logger.info(f'Set [{svcfull}] offline.',
823
1067
  extra=await self.getLogExtra(name=svcname, netw=svcnetw))
@@ -918,7 +1162,7 @@ class AhaCell(s_cell.Cell):
918
1162
 
919
1163
  async def _genHostCert(self, hostname, signas=None):
920
1164
 
921
- if os.path.isfile(os.path.join(self.dirn, 'certs', 'hosts', '{hostname}.crt')):
1165
+ if self.certdir.getHostCertPath(hostname) is not None:
922
1166
  return
923
1167
 
924
1168
  pkey, cert = await s_coro.executor(self.certdir.genHostCert, hostname, signas=signas, save=False)
@@ -927,8 +1171,12 @@ class AhaCell(s_cell.Cell):
927
1171
  await self.saveHostCert(hostname, pkey, cert)
928
1172
 
929
1173
  async def _genUserCert(self, username, signas=None):
930
- if os.path.isfile(os.path.join(self.dirn, 'certs', 'users', '{username}.crt')):
1174
+
1175
+ if self.certdir.getUserCertPath(username) is not None:
931
1176
  return
1177
+
1178
+ logger.info(f'Adding user certificate for {username}')
1179
+
932
1180
  pkey, cert = await s_coro.executor(self.certdir.genUserCert, username, signas=signas, save=False)
933
1181
  pkey = self.certdir._pkeyToByts(pkey).decode()
934
1182
  cert = self.certdir._certToByts(cert).decode()
@@ -971,8 +1219,8 @@ class AhaCell(s_cell.Cell):
971
1219
 
972
1220
  hostname = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value
973
1221
 
974
- hostpath = s_common.genpath(self.dirn, 'certs', 'hosts', f'{hostname}.crt')
975
- if os.path.isfile(hostpath):
1222
+ hostpath = self.certdir.getHostCertPath(hostname)
1223
+ if hostpath is not None:
976
1224
  os.unlink(hostpath)
977
1225
 
978
1226
  if signas is None:
@@ -990,8 +1238,8 @@ class AhaCell(s_cell.Cell):
990
1238
 
991
1239
  username = xcsr.subject.get_attributes_for_oid(c_x509.NameOID.COMMON_NAME)[0].value
992
1240
 
993
- userpath = s_common.genpath(self.dirn, 'certs', 'users', f'{username}.crt')
994
- if os.path.isfile(userpath):
1241
+ userpath = self.certdir.getUserCertPath(username)
1242
+ if userpath is not None:
995
1243
  os.unlink(userpath)
996
1244
 
997
1245
  if signas is None:
@@ -1004,41 +1252,75 @@ class AhaCell(s_cell.Cell):
1004
1252
 
1005
1253
  return self.certdir._certToByts(cert).decode()
1006
1254
 
1007
- def _getAhaUrls(self):
1255
+ async def getAhaUrls(self, user='root'):
1256
+
1257
+ # for backward compat...
1008
1258
  urls = self.conf.get('aha:urls')
1009
1259
  if urls is not None:
1010
- if isinstance(urls, str):
1011
- return (urls,)
1012
1260
  return urls
1013
1261
 
1014
- ahaname = self.conf.get('aha:name')
1015
- if ahaname is None:
1016
- return None
1262
+ network = self.conf.req('aha:network')
1017
1263
 
1018
- ahanetw = self.conf.get('aha:network')
1019
- if ahanetw is None:
1020
- return None
1264
+ urls = []
1265
+ for server in await self.getAhaServers():
1266
+ host = server.get('host')
1267
+ port = server.get('port')
1268
+ urls.append(f'ssl://{host}:{port}?certname={user}@{network}')
1021
1269
 
1022
- if self.sockaddr is None or not isinstance(self.sockaddr, tuple):
1023
- return None
1270
+ return urls
1024
1271
 
1025
- # TODO this could eventually enumerate others via itself
1026
- return (f'ssl://{ahaname}.{ahanetw}:{self.sockaddr[1]}',)
1272
+ def getMyUrl(self, user='root'):
1273
+ port = self.sockaddr[1]
1274
+ host = self._getDnsName()
1275
+ network = self.conf.req('aha:network')
1276
+ return f'ssl://{host}:{port}?certname={user}@{network}'
1027
1277
 
1028
- async def addAhaSvcProv(self, name, provinfo=None):
1278
+ async def getAhaClone(self, iden):
1279
+ lkey = s_common.uhex(iden)
1280
+ byts = self.slab.get(lkey, db='aha:clones')
1281
+ if byts is not None:
1282
+ return s_msgpack.un(byts)
1029
1283
 
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)
1284
+ async def addAhaClone(self, host, port=27492, conf=None):
1285
+
1286
+ if conf is None:
1287
+ conf = {}
1288
+
1289
+ network = self.conf.req('aha:network')
1290
+
1291
+ conf['mirror'] = self.getMyUrl()
1292
+
1293
+ conf['dns:name'] = host
1294
+ conf['aha:network'] = network
1295
+ conf['dmon:listen'] = f'ssl://0.0.0.0:{port}?hostname={host}&ca={network}'
1296
+
1297
+ iden = s_common.guid()
1298
+ clone = {
1299
+ 'iden': iden,
1300
+ 'host': host,
1301
+ 'port': port,
1302
+ 'conf': conf,
1303
+ }
1304
+ await self._push('aha:clone:add', clone)
1305
+
1306
+ logger.info(f'Created AHA clone provisioning for {host} with iden {iden}',
1307
+ extra=await self.getLogExtra(iden=iden, name=host, netw=network))
1308
+
1309
+ return self._getProvClientUrl(iden)
1034
1310
 
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)
1311
+ @s_nexus.Pusher.onPush('aha:clone:add')
1312
+ async def _addAhaClone(self, clone):
1313
+ iden = clone.get('iden')
1314
+ lkey = s_common.uhex(iden)
1315
+ self.slab.put(lkey, s_msgpack.en(clone), db='aha:clones')
1316
+
1317
+ async def addAhaSvcProv(self, name, provinfo=None):
1038
1318
 
1039
1319
  if not name:
1040
1320
  raise s_exc.BadArg(mesg='Empty name values are not allowed for provisioning.')
1041
1321
 
1322
+ self._reqProvListen()
1323
+
1042
1324
  if provinfo is None:
1043
1325
  provinfo = {}
1044
1326
 
@@ -1048,23 +1330,16 @@ class AhaCell(s_cell.Cell):
1048
1330
 
1049
1331
  conf = provinfo.setdefault('conf', {})
1050
1332
 
1051
- mynetw = self.conf.get('aha:network')
1333
+ netw = self.conf.req('aha:network')
1052
1334
 
1053
1335
  ahaadmin = self.conf.get('aha:admin')
1054
1336
  if ahaadmin is not None: # pragma: no cover
1055
1337
  conf.setdefault('aha:admin', ahaadmin)
1056
1338
 
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)
1339
+ ahauser = conf.setdefault('aha:user', 'root')
1340
+ ahaurls = await self.getAhaUrls(user=ahauser)
1064
1341
 
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)
1342
+ conf['aha:network'] = netw
1068
1343
 
1069
1344
  hostname = f'{name}.{netw}'
1070
1345
 
@@ -1088,15 +1363,11 @@ class AhaCell(s_cell.Cell):
1088
1363
  if peer:
1089
1364
  conf.setdefault('aha:leader', leader)
1090
1365
 
1091
- # allow user to win over leader
1092
- ahauser = conf.get('aha:user')
1093
- ahaurls = s_telepath.modurl(ahaurls, user=ahauser)
1094
-
1095
1366
  conf.setdefault('aha:registry', ahaurls)
1096
1367
 
1097
1368
  mirname = provinfo.get('mirror')
1098
1369
  if mirname is not None:
1099
- conf['mirror'] = f'aha://{ahauser}@{mirname}.{netw}'
1370
+ conf['mirror'] = f'aha://{ahauser}@{mirname}...'
1100
1371
 
1101
1372
  user = await self.auth.getUserByName(ahauser)
1102
1373
  if user is None:
@@ -1122,7 +1393,10 @@ class AhaCell(s_cell.Cell):
1122
1393
 
1123
1394
  def _getProvClientUrl(self, iden):
1124
1395
 
1125
- provlisn = self.conf.get('provision:listen')
1396
+ provlisn = self._getProvListen()
1397
+
1398
+ provport = self.provaddr[1]
1399
+ provhost = self._getDnsName()
1126
1400
 
1127
1401
  urlinfo = s_telepath.chopurl(provlisn)
1128
1402
 
@@ -1133,8 +1407,8 @@ class AhaCell(s_cell.Cell):
1133
1407
  host = urlinfo.get('host')
1134
1408
 
1135
1409
  newinfo = {
1136
- 'host': host,
1137
- 'port': urlinfo.get('port'),
1410
+ 'host': provhost,
1411
+ 'port': provport,
1138
1412
  'scheme': scheme,
1139
1413
  'path': '/' + iden,
1140
1414
  }
@@ -1171,25 +1445,25 @@ class AhaCell(s_cell.Cell):
1171
1445
  userinfo = s_msgpack.un(byts)
1172
1446
  logger.info(f'Deleted user enrollment username={userinfo.get("name")}, iden={iden.decode()}')
1173
1447
 
1448
+ @s_nexus.Pusher.onPushAuto('aha:clone:clear')
1449
+ async def clearAhaClones(self):
1450
+ for lkey, byts in self.slab.scanByFull(db='aha:clones'):
1451
+ self.slab.delete(lkey, db='aha:clones')
1452
+ cloninfo = s_msgpack.un(byts)
1453
+ logger.info(f'Deleted AHA clone enrollment username={cloninfo.get("host")}, iden={s_common.ehex(lkey)}')
1454
+
1174
1455
  @s_nexus.Pusher.onPushAuto('aha:svc:prov:del')
1175
1456
  async def delAhaSvcProv(self, iden):
1176
1457
  self.slab.delete(iden.encode(), db='aha:provs')
1177
1458
 
1178
1459
  async def addAhaUserEnroll(self, name, userinfo=None, again=False):
1179
1460
 
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
1461
  if not name:
1191
1462
  raise s_exc.BadArg(mesg='Empty name values are not allowed for provisioning.')
1192
1463
 
1464
+ provurl = self._reqProvListen()
1465
+ ahanetw = self.conf.req('aha:network')
1466
+
1193
1467
  username = f'{name}@{ahanetw}'
1194
1468
 
1195
1469
  if len(username) > 64: