synapse 2.212.0__py311-none-any.whl → 2.214.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.
- synapse/cortex.py +37 -6
- synapse/daemon.py +6 -6
- synapse/exc.py +13 -1
- synapse/lib/aha.py +5 -0
- synapse/lib/ast.py +2 -6
- synapse/lib/boss.py +47 -2
- synapse/lib/cell.py +199 -6
- synapse/lib/certdir.py +44 -1
- synapse/lib/cmd.py +24 -0
- synapse/lib/coro.py +8 -2
- synapse/lib/drive.py +7 -2
- synapse/lib/link.py +11 -3
- synapse/lib/schemas.py +1 -1
- synapse/lib/scrape.py +3 -1
- synapse/lib/snap.py +89 -80
- synapse/lib/storm.py +2 -1
- synapse/lib/stormlib/imap.py +3 -2
- synapse/lib/stormlib/spooled.py +4 -0
- synapse/lib/stormtypes.py +18 -0
- synapse/lib/task.py +1 -0
- synapse/lib/types.py +36 -8
- synapse/lib/version.py +2 -2
- synapse/models/inet.py +5 -0
- synapse/telepath.py +4 -2
- synapse/tests/files/testpkg_build_docs/docs/bar.rst +15 -0
- synapse/tests/files/testpkg_build_docs/docs/foo.rst +4 -0
- synapse/tests/files/testpkg_build_docs/storm/commands/testcmd.storm +0 -0
- synapse/tests/files/testpkg_build_docs/storm/modules/apimod.storm +0 -0
- synapse/tests/files/testpkg_build_docs/storm/modules/testmod.storm +0 -0
- synapse/tests/files/testpkg_build_docs/storm/testcmd.storm +5 -0
- synapse/tests/files/testpkg_build_docs/testpkg.yaml +69 -0
- synapse/tests/test_cortex.py +20 -1
- synapse/tests/test_daemon.py +1 -1
- synapse/tests/test_exc.py +6 -0
- synapse/tests/test_lib_ast.py +69 -14
- synapse/tests/test_lib_boss.py +8 -0
- synapse/tests/test_lib_cell.py +119 -8
- synapse/tests/test_lib_certdir.py +8 -0
- synapse/tests/test_lib_coro.py +5 -0
- synapse/tests/test_lib_httpapi.py +10 -2
- synapse/tests/test_lib_link.py +1 -1
- synapse/tests/test_lib_scrape.py +6 -0
- synapse/tests/test_lib_storm.py +123 -1
- synapse/tests/test_lib_stormlib_spooled.py +31 -0
- synapse/tests/test_lib_stormtypes.py +11 -0
- synapse/tests/test_lib_types.py +137 -45
- synapse/tests/test_model_crypto.py +8 -0
- synapse/tests/test_model_inet.py +7 -0
- synapse/tests/test_telepath.py +50 -5
- synapse/tests/test_tools_axon.py +304 -0
- synapse/tests/test_tools_cortex_layer.py +419 -0
- synapse/tests/test_tools_demote.py +114 -0
- synapse/tests/test_tools_pkgs_gendocs.py +100 -0
- synapse/tests/test_tools_shutdown.py +95 -0
- synapse/tests/test_utils.py +22 -1
- synapse/tests/utils.py +44 -29
- synapse/tools/aha/easycert.py +2 -0
- synapse/tools/aha/enroll.py +3 -0
- synapse/tools/axon/__init__.py +0 -0
- synapse/tools/axon/dump.py +155 -0
- synapse/tools/axon/load.py +89 -0
- synapse/tools/cortex/__init__.py +0 -0
- synapse/tools/cortex/layer/__init__.py +0 -0
- synapse/tools/cortex/layer/dump.py +184 -0
- synapse/tools/cortex/layer/load.py +129 -0
- synapse/tools/demote.py +52 -0
- synapse/tools/healthcheck.py +1 -1
- synapse/tools/pkgs/gendocs.py +176 -0
- synapse/tools/pkgs/pandoc_filter.py +79 -0
- synapse/tools/shutdown.py +52 -0
- {synapse-2.212.0.dist-info → synapse-2.214.0.dist-info}/METADATA +1 -1
- {synapse-2.212.0.dist-info → synapse-2.214.0.dist-info}/RECORD +75 -52
- {synapse-2.212.0.dist-info → synapse-2.214.0.dist-info}/WHEEL +0 -0
- {synapse-2.212.0.dist-info → synapse-2.214.0.dist-info}/licenses/LICENSE +0 -0
- {synapse-2.212.0.dist-info → synapse-2.214.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import synapse.common as s_common
|
|
2
|
+
|
|
3
|
+
import synapse.lib.cell as s_cell
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import synapse.tests.utils as s_test
|
|
7
|
+
import synapse.tools.shutdown as s_t_shutdown
|
|
8
|
+
|
|
9
|
+
class ShutdownToolTest(s_test.SynTest):
|
|
10
|
+
|
|
11
|
+
async def test_tool_shutdown_base(self):
|
|
12
|
+
|
|
13
|
+
async with self.getTestCore() as core:
|
|
14
|
+
|
|
15
|
+
msgs = await core.stormlist('background { $lib.time.sleep(10) }')
|
|
16
|
+
self.stormHasNoWarnErr(msgs)
|
|
17
|
+
|
|
18
|
+
# add a dmon to ensure task.background=True works correctly
|
|
19
|
+
await core.addStormDmon({
|
|
20
|
+
'iden': s_common.guid(),
|
|
21
|
+
'storm': 'while (true) { $lib.time.sleep(1) }',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
argv = ['--url', core.getLocalUrl(), '--timeout', '0']
|
|
25
|
+
|
|
26
|
+
self.eq(1, await s_t_shutdown.main(argv))
|
|
27
|
+
|
|
28
|
+
for task in core.boss.ps():
|
|
29
|
+
if task.name == 'storm':
|
|
30
|
+
await task.kill()
|
|
31
|
+
|
|
32
|
+
self.eq(0, await s_t_shutdown.main(['--url', core.getLocalUrl()]))
|
|
33
|
+
|
|
34
|
+
self.true(await core.waitfini(timeout=1))
|
|
35
|
+
|
|
36
|
+
outp = self.getTestOutp()
|
|
37
|
+
self.eq(1, await s_t_shutdown.main(['--url', 'newp://hehe'], outp=outp))
|
|
38
|
+
outp.expect('Error while attempting graceful shutdown')
|
|
39
|
+
|
|
40
|
+
async def test_tool_shutdown_leader(self):
|
|
41
|
+
|
|
42
|
+
async with self.getTestAha() as aha:
|
|
43
|
+
|
|
44
|
+
with self.getTestDir() as dirn:
|
|
45
|
+
|
|
46
|
+
dirn00 = s_common.genpath(dirn, '00.cell')
|
|
47
|
+
dirn01 = s_common.genpath(dirn, '01.cell')
|
|
48
|
+
|
|
49
|
+
cell00 = await aha.enter_context(self.addSvcToAha(aha, '00.cell', s_cell.Cell, dirn=dirn00))
|
|
50
|
+
cell01 = await aha.enter_context(self.addSvcToAha(aha, '01.cell', s_cell.Cell, dirn=dirn01,
|
|
51
|
+
provinfo={'mirror': 'cell'}))
|
|
52
|
+
self.true(cell00.isactive)
|
|
53
|
+
self.false(cell01.isactive)
|
|
54
|
+
|
|
55
|
+
await cell01.sync()
|
|
56
|
+
|
|
57
|
+
# confirm that graceful shutdown with peers also demotes...
|
|
58
|
+
outp = self.getTestOutp()
|
|
59
|
+
argv = ['--url', cell00.getLocalUrl(), '--timeout', '12']
|
|
60
|
+
self.eq(0, await s_t_shutdown.main(argv, outp=outp))
|
|
61
|
+
|
|
62
|
+
self.false(cell00.isactive)
|
|
63
|
+
self.true(cell01.isactive)
|
|
64
|
+
self.true(await cell00.waitfini(timeout=12))
|
|
65
|
+
|
|
66
|
+
# and that graceful shutdown without any cluster peers works too...
|
|
67
|
+
outp.clear()
|
|
68
|
+
argv = ['--url', cell01.getLocalUrl(), '--timeout', '12']
|
|
69
|
+
self.eq(0, await s_t_shutdown.main(argv, outp=outp))
|
|
70
|
+
self.true(await cell01.waitfini(timeout=12))
|
|
71
|
+
|
|
72
|
+
async def test_tool_shutdown_no_features(self):
|
|
73
|
+
|
|
74
|
+
async with self.getTestAha() as aha:
|
|
75
|
+
aha.features.pop('getAhaSvcsByIden')
|
|
76
|
+
|
|
77
|
+
with self.getTestDir() as dirn:
|
|
78
|
+
|
|
79
|
+
dirn00 = s_common.genpath(dirn, '00.cell')
|
|
80
|
+
dirn01 = s_common.genpath(dirn, '01.cell')
|
|
81
|
+
|
|
82
|
+
cell00 = await aha.enter_context(self.addSvcToAha(aha, '00.cell', s_cell.Cell, dirn=dirn00))
|
|
83
|
+
cell01 = await aha.enter_context(self.addSvcToAha(aha, '01.cell', s_cell.Cell, dirn=dirn01,
|
|
84
|
+
provinfo={'mirror': 'cell'}))
|
|
85
|
+
self.true(cell00.isactive)
|
|
86
|
+
self.false(cell01.isactive)
|
|
87
|
+
|
|
88
|
+
await cell01.sync()
|
|
89
|
+
|
|
90
|
+
# confirm that graceful shutdown with peers also demotes...
|
|
91
|
+
outp = self.getTestOutp()
|
|
92
|
+
argv = ['--url', cell00.getLocalUrl(), '--timeout', '12']
|
|
93
|
+
with self.getAsyncLoggerStream('synapse.daemon') as stream:
|
|
94
|
+
self.eq(1, await s_t_shutdown.main(argv, outp=outp))
|
|
95
|
+
stream.expect('AHA server does not support feature: getAhaSvcsByIden >= 1')
|
synapse/tests/test_utils.py
CHANGED
|
@@ -3,6 +3,7 @@ import time
|
|
|
3
3
|
import logging
|
|
4
4
|
import unittest
|
|
5
5
|
|
|
6
|
+
import synapse.exc as s_exc
|
|
6
7
|
import synapse.common as s_common
|
|
7
8
|
|
|
8
9
|
import synapse.lib.base as s_base
|
|
@@ -100,13 +101,33 @@ class TestUtils(s_t_utils.SynTest):
|
|
|
100
101
|
self.skipIfNoPath('newpDoesNotExist', mesg='hehe')
|
|
101
102
|
self.isin('newpDoesNotExist mesg=hehe', str(cm.exception))
|
|
102
103
|
|
|
103
|
-
def test_syntest_logstream(self):
|
|
104
|
+
async def test_syntest_logstream(self):
|
|
104
105
|
with self.getLoggerStream('synapse.tests.test_utils') as stream:
|
|
105
106
|
logger.error('ruh roh i am a error message')
|
|
107
|
+
|
|
108
|
+
stream.expect('error message')
|
|
109
|
+
with self.raises(s_exc.SynErr):
|
|
110
|
+
stream.expect('does not exist')
|
|
111
|
+
|
|
112
|
+
self.eq(str(stream), 'ruh roh i am a error message\n')
|
|
113
|
+
self.true(repr(stream).endswith('valu: ruh roh i am a error message>'))
|
|
114
|
+
self.true(repr(stream).startswith('<synapse.tests.utils.StreamEvent'))
|
|
115
|
+
|
|
106
116
|
stream.seek(0)
|
|
107
117
|
mesgs = stream.read()
|
|
108
118
|
self.isin('ruh roh', mesgs)
|
|
109
119
|
|
|
120
|
+
with self.getAsyncLoggerStream('synapse.tests.test_utils') as stream:
|
|
121
|
+
logger.error('ruh roh i am a new message')
|
|
122
|
+
|
|
123
|
+
stream.expect('new message')
|
|
124
|
+
with self.raises(s_exc.SynErr):
|
|
125
|
+
stream.expect('does not exist')
|
|
126
|
+
|
|
127
|
+
self.eq(str(stream), 'ruh roh i am a new message\n')
|
|
128
|
+
self.true(repr(stream).endswith('valu: ruh roh i am a new message>'))
|
|
129
|
+
self.true(repr(stream).startswith('<synapse.tests.utils.AsyncStreamEvent'))
|
|
130
|
+
|
|
110
131
|
def test_syntest_logstream_event(self):
|
|
111
132
|
|
|
112
133
|
@s_common.firethread
|
synapse/tests/utils.py
CHANGED
|
@@ -788,14 +788,9 @@ class CmdGenerator:
|
|
|
788
788
|
|
|
789
789
|
return retn
|
|
790
790
|
|
|
791
|
-
class
|
|
792
|
-
'''
|
|
793
|
-
A combination of a io.StringIO object and a threading.Event object.
|
|
794
|
-
'''
|
|
791
|
+
class _StreamIOMixin(io.StringIO):
|
|
795
792
|
def __init__(self, *args, **kwargs):
|
|
796
793
|
io.StringIO.__init__(self, *args, **kwargs)
|
|
797
|
-
threading.Event.__init__(self)
|
|
798
|
-
self.mesg = ''
|
|
799
794
|
|
|
800
795
|
def setMesg(self, mesg):
|
|
801
796
|
'''
|
|
@@ -810,6 +805,9 @@ class StreamEvent(io.StringIO, threading.Event):
|
|
|
810
805
|
self.mesg = mesg
|
|
811
806
|
self.clear()
|
|
812
807
|
|
|
808
|
+
def __str__(self):
|
|
809
|
+
return self.getvalue()
|
|
810
|
+
|
|
813
811
|
def write(self, s):
|
|
814
812
|
io.StringIO.write(self, s)
|
|
815
813
|
if self.mesg and self.mesg in s:
|
|
@@ -819,42 +817,59 @@ class StreamEvent(io.StringIO, threading.Event):
|
|
|
819
817
|
'''Get the messages as jsonlines. May throw Json errors if the captured stream is not jsonlines.'''
|
|
820
818
|
return jsonlines(self.getvalue())
|
|
821
819
|
|
|
822
|
-
|
|
820
|
+
def expect(self, substr: str):
|
|
821
|
+
'''
|
|
822
|
+
Check if a string is present in the messages captured by StreamEvent.
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
substr (str): String to check for the existence of.
|
|
826
|
+
'''
|
|
827
|
+
valu = self.getvalue()
|
|
828
|
+
if valu.find(substr) == -1:
|
|
829
|
+
mesg = '%s.expect(%s) not in %s' % (self.__class__.__name__, substr, valu)
|
|
830
|
+
raise s_exc.SynErr(mesg=mesg)
|
|
831
|
+
|
|
832
|
+
class StreamEvent(_StreamIOMixin, threading.Event):
|
|
823
833
|
'''
|
|
824
|
-
A combination of a io.StringIO object and
|
|
834
|
+
A combination of a io.StringIO object and a threading.Event object.
|
|
825
835
|
'''
|
|
826
836
|
def __init__(self, *args, **kwargs):
|
|
827
|
-
|
|
828
|
-
|
|
837
|
+
_StreamIOMixin.__init__(self, *args, **kwargs)
|
|
838
|
+
threading.Event.__init__(self)
|
|
829
839
|
self.mesg = ''
|
|
830
840
|
|
|
831
|
-
def
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
841
|
+
def __repr__(self):
|
|
842
|
+
cls = self.__class__
|
|
843
|
+
status = 'set' if self._flag else 'unset'
|
|
844
|
+
if valu := str(self):
|
|
845
|
+
valu = s_common.trimText(valu).strip()
|
|
846
|
+
status = f'{status}, valu: {valu}'
|
|
847
|
+
return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: {status}>"
|
|
837
848
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
849
|
+
class AsyncStreamEvent(_StreamIOMixin, asyncio.Event):
|
|
850
|
+
'''
|
|
851
|
+
A combination of a io.StringIO object and an asyncio.Event object.
|
|
852
|
+
'''
|
|
853
|
+
def __init__(self, *args, **kwargs):
|
|
854
|
+
_StreamIOMixin.__init__(self, *args, **kwargs)
|
|
855
|
+
asyncio.Event.__init__(self)
|
|
856
|
+
self.mesg = ''
|
|
843
857
|
|
|
844
|
-
def
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
858
|
+
def __repr__(self):
|
|
859
|
+
cls = self.__class__
|
|
860
|
+
status = 'set' if self._value else 'unset'
|
|
861
|
+
if self._waiters:
|
|
862
|
+
status = f'{status}, waiters:{len(self._waiters)}'
|
|
863
|
+
if valu := str(self):
|
|
864
|
+
valu = s_common.trimText(valu).strip()
|
|
865
|
+
status = f'{status}, valu: {valu}'
|
|
866
|
+
return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: {status}>"
|
|
848
867
|
|
|
849
868
|
async def wait(self, timeout=None):
|
|
850
869
|
if timeout is None:
|
|
851
870
|
return await asyncio.Event.wait(self)
|
|
852
871
|
return await s_coro.event_wait(self, timeout=timeout)
|
|
853
872
|
|
|
854
|
-
def jsonlines(self) -> typing.List[dict]:
|
|
855
|
-
'''Get the messages as jsonlines. May throw Json errors if the captured stream is not jsonlines.'''
|
|
856
|
-
return jsonlines(self.getvalue())
|
|
857
|
-
|
|
858
873
|
class HttpReflector(s_httpapi.Handler):
|
|
859
874
|
'''Test handler which reflects get/post data back to the caller'''
|
|
860
875
|
|
synapse/tools/aha/easycert.py
CHANGED
|
@@ -45,6 +45,7 @@ async def main(argv, outp=s_output.stdout):
|
|
|
45
45
|
cert = c_x509.load_pem_x509_certificate(certbyts.encode())
|
|
46
46
|
path = cdir._saveCertTo(cert, 'hosts', f'{name}.crt')
|
|
47
47
|
outp.printf(f'crt saved: {path}')
|
|
48
|
+
cdir.delHostCsr(name, outp=outp)
|
|
48
49
|
return 0
|
|
49
50
|
else:
|
|
50
51
|
csr = cdir.genUserCsr(name, outp=outp)
|
|
@@ -52,6 +53,7 @@ async def main(argv, outp=s_output.stdout):
|
|
|
52
53
|
cert = c_x509.load_pem_x509_certificate(certbyts.encode())
|
|
53
54
|
path = cdir._saveCertTo(cert, 'users', f'{name}.crt')
|
|
54
55
|
outp.printf(f'crt saved: {path}')
|
|
56
|
+
cdir.delUserCsr(name, outp=outp)
|
|
55
57
|
return 0
|
|
56
58
|
|
|
57
59
|
def getArgParser():
|
synapse/tools/aha/enroll.py
CHANGED
|
@@ -98,6 +98,9 @@ async def main(argv, outp=s_output.stdout):
|
|
|
98
98
|
teleyaml['aha:servers'] = servers
|
|
99
99
|
s_common.yamlsave(teleyaml, yamlpath)
|
|
100
100
|
|
|
101
|
+
# cleanup CSR
|
|
102
|
+
certdir.delUserCsr(username)
|
|
103
|
+
|
|
101
104
|
return 0
|
|
102
105
|
|
|
103
106
|
async def _main(argv, outp=s_output.stdout): # pragma: no cover
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tarfile
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
import synapse.exc as s_exc
|
|
6
|
+
import synapse.common as s_common
|
|
7
|
+
import synapse.telepath as s_telepath
|
|
8
|
+
|
|
9
|
+
import synapse.lib.cmd as s_cmd
|
|
10
|
+
import synapse.lib.const as s_const
|
|
11
|
+
import synapse.lib.output as s_output
|
|
12
|
+
|
|
13
|
+
descr = '''
|
|
14
|
+
Dump blobs from a Synapse Axon.
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
MAX_SPOOL_SIZE = s_const.mebibyte * 512
|
|
18
|
+
DEFAULT_ROTATE_SIZE = s_const.gigabyte * 4
|
|
19
|
+
|
|
20
|
+
def getTarName(celliden, start, end):
|
|
21
|
+
return f'{celliden}.{start:012d}-{end:012d}.tar.gz'
|
|
22
|
+
|
|
23
|
+
async def dumpBlobs(opts, outp):
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
async with await s_telepath.openurl(opts.url) as axon:
|
|
27
|
+
|
|
28
|
+
cellinfo = await axon.getCellInfo()
|
|
29
|
+
celltype = cellinfo['cell']['type']
|
|
30
|
+
if "axon" not in celltype.lower():
|
|
31
|
+
mesg = f'Axon dump tool only works on axons, not {celltype}'
|
|
32
|
+
raise s_exc.TypeMismatch(mesg=mesg)
|
|
33
|
+
celliden = cellinfo['cell']['iden']
|
|
34
|
+
|
|
35
|
+
if os.path.exists(opts.outdir) and not os.path.isdir(opts.outdir):
|
|
36
|
+
raise s_exc.BadDataValu(mesg=f'Specified output directory {opts.outdir} exists but is not a directory.')
|
|
37
|
+
os.makedirs(opts.outdir, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
if opts.statefile is None:
|
|
40
|
+
statefile_path = os.path.join(opts.outdir, f'{celliden}.yaml')
|
|
41
|
+
else:
|
|
42
|
+
statefile_path = opts.statefile
|
|
43
|
+
if os.path.isdir(statefile_path):
|
|
44
|
+
statefile_path = os.path.join(statefile_path, f'{celliden}.yaml')
|
|
45
|
+
state = {}
|
|
46
|
+
if os.path.isfile(statefile_path):
|
|
47
|
+
if (data := s_common.yamlload(statefile_path)) is not None:
|
|
48
|
+
state = data
|
|
49
|
+
opts.statefile = statefile_path
|
|
50
|
+
|
|
51
|
+
if opts.offset is not None:
|
|
52
|
+
start = opts.offset
|
|
53
|
+
else:
|
|
54
|
+
start = state.get('offset:next', 0)
|
|
55
|
+
outp.printf(f'Starting the dump from offs={start}')
|
|
56
|
+
|
|
57
|
+
rotate_size = opts.rotate_size
|
|
58
|
+
hashes_iter = axon.hashes(start)
|
|
59
|
+
last_offset = start
|
|
60
|
+
tar = None
|
|
61
|
+
tar_path = None
|
|
62
|
+
file_start = start
|
|
63
|
+
file_blobcount = 0
|
|
64
|
+
tar_size = 0
|
|
65
|
+
for_open = True
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
async for (offs, (sha256, size)) in hashes_iter:
|
|
69
|
+
if for_open:
|
|
70
|
+
tar_path = os.path.join(opts.outdir, getTarName(celliden, offs, -1))
|
|
71
|
+
tar = tarfile.open(tar_path, 'w:gz')
|
|
72
|
+
file_start = offs
|
|
73
|
+
tar_size = 0
|
|
74
|
+
file_blobcount = 0
|
|
75
|
+
for_open = False
|
|
76
|
+
|
|
77
|
+
last_offset = offs
|
|
78
|
+
file_blobcount += 1
|
|
79
|
+
sha2hex = s_common.ehex(sha256)
|
|
80
|
+
outp.printf(f'Dumping blob {sha2hex} (size={size}, offs={offs})')
|
|
81
|
+
|
|
82
|
+
with tempfile.SpooledTemporaryFile(max_size=MAX_SPOOL_SIZE, mode='w+b', dir=opts.outdir) as tmpf:
|
|
83
|
+
total = 0
|
|
84
|
+
async for byts in axon.get(sha256):
|
|
85
|
+
tmpf.write(byts)
|
|
86
|
+
total += len(byts)
|
|
87
|
+
if total != size:
|
|
88
|
+
raise s_exc.BadDataValu(mesg=f'Blob size mismatch for {sha2hex}: expected {size}, got {total}')
|
|
89
|
+
tmpf.flush()
|
|
90
|
+
tmpf.seek(0)
|
|
91
|
+
tarinfo = tarfile.TarInfo(name=f"{sha2hex}.blob")
|
|
92
|
+
tarinfo.size = size
|
|
93
|
+
tar.addfile(tarinfo, tmpf)
|
|
94
|
+
tar_size += size
|
|
95
|
+
|
|
96
|
+
if tar_size >= rotate_size:
|
|
97
|
+
outp.printf(f'Rotating to new .tar.gz file at offset {offs + 1}')
|
|
98
|
+
tar.close()
|
|
99
|
+
final_name = os.path.join(opts.outdir, getTarName(celliden, file_start, offs + 1))
|
|
100
|
+
os.rename(tar_path, final_name)
|
|
101
|
+
tar = None
|
|
102
|
+
tar_path = None
|
|
103
|
+
for_open = True
|
|
104
|
+
|
|
105
|
+
if tar is not None:
|
|
106
|
+
tar.close()
|
|
107
|
+
final_name = os.path.join(opts.outdir, getTarName(celliden, file_start, last_offset + 1))
|
|
108
|
+
os.rename(tar_path, final_name)
|
|
109
|
+
tar = None
|
|
110
|
+
tar_path = None
|
|
111
|
+
|
|
112
|
+
state['offset:next'] = last_offset + 1
|
|
113
|
+
s_common.yamlsave(state, statefile_path)
|
|
114
|
+
|
|
115
|
+
finally:
|
|
116
|
+
if tar is not None:
|
|
117
|
+
tar.close()
|
|
118
|
+
if tar_path and os.path.isfile(tar_path):
|
|
119
|
+
os.remove(tar_path)
|
|
120
|
+
|
|
121
|
+
except s_exc.SynErr as exc:
|
|
122
|
+
mesg = exc.get('mesg')
|
|
123
|
+
return (False, mesg)
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
mesg = f'Error {e} dumping blobs from Axon url: {opts.url}'
|
|
127
|
+
return (False, mesg)
|
|
128
|
+
|
|
129
|
+
return (True, None)
|
|
130
|
+
|
|
131
|
+
async def main(argv, outp=s_output.stdout):
|
|
132
|
+
pars = s_cmd.Parser(prog='synapse.tools.axon.dump', outp=outp, description=descr)
|
|
133
|
+
pars.add_argument('--url', default='cell:///vertex/storage', help='Telepath URL for the Axon.')
|
|
134
|
+
pars.add_argument('--offset', type=int, default=None, help='Starting offset in the Axon.')
|
|
135
|
+
pars.add_argument('--rotate-size', type=int, default=DEFAULT_ROTATE_SIZE,
|
|
136
|
+
help='Rotate to a new .blobs file after the current file exceeds this size in bytes (default: 4GB). '
|
|
137
|
+
'Note: files may exceed this size if a single blob is larger than the remaining space.')
|
|
138
|
+
pars.add_argument('--statefile', type=str, default=None,
|
|
139
|
+
help='Path to the state tracking file for the Axon dump.')
|
|
140
|
+
pars.add_argument('outdir', help='Directory to dump tar.gz files (required).')
|
|
141
|
+
|
|
142
|
+
opts = pars.parse_args(argv)
|
|
143
|
+
|
|
144
|
+
async with s_telepath.withTeleEnv():
|
|
145
|
+
(ok, mesg) = await dumpBlobs(opts, outp)
|
|
146
|
+
if not ok:
|
|
147
|
+
outp.printf(f'ERROR: {mesg}')
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
outp.printf('Successfully dumped blobs.')
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__': # pragma: no cover
|
|
155
|
+
s_cmd.exitmain(main)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import tarfile
|
|
2
|
+
|
|
3
|
+
import synapse.exc as s_exc
|
|
4
|
+
import synapse.common as s_common
|
|
5
|
+
import synapse.telepath as s_telepath
|
|
6
|
+
|
|
7
|
+
import synapse.lib.cmd as s_cmd
|
|
8
|
+
import synapse.lib.const as s_const
|
|
9
|
+
import synapse.lib.output as s_output
|
|
10
|
+
|
|
11
|
+
descr = '''
|
|
12
|
+
Load blobs into a Synapse Axon.
|
|
13
|
+
'''
|
|
14
|
+
|
|
15
|
+
DEFAULT_CHUNK_SIZE = s_const.mebibyte * 16
|
|
16
|
+
|
|
17
|
+
async def loadBlobs(opts, outp, tarfiles):
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
async with await s_telepath.openurl(opts.url) as axon:
|
|
21
|
+
|
|
22
|
+
cellinfo = await axon.getCellInfo()
|
|
23
|
+
celltype = cellinfo['cell']['type']
|
|
24
|
+
if "axon" not in celltype.lower():
|
|
25
|
+
mesg = f'Axon load tool only works on axons, not {celltype}'
|
|
26
|
+
raise s_exc.TypeMismatch(mesg=mesg)
|
|
27
|
+
|
|
28
|
+
for tarfile_path in tarfiles:
|
|
29
|
+
outp.printf(f'Processing tar archive: {tarfile_path}')
|
|
30
|
+
with tarfile.open(tarfile_path, 'r:gz') as tar:
|
|
31
|
+
for member in tar:
|
|
32
|
+
if not member.name.endswith('.blob'):
|
|
33
|
+
continue
|
|
34
|
+
sha2hex = member.name[:-5]
|
|
35
|
+
try:
|
|
36
|
+
sha256 = s_common.uhex(sha2hex)
|
|
37
|
+
except Exception:
|
|
38
|
+
outp.printf(f"Skipping invalid blob filename: {member.name}")
|
|
39
|
+
continue
|
|
40
|
+
if await axon.has(sha256):
|
|
41
|
+
outp.printf(f"Skipping existing blob {sha2hex}")
|
|
42
|
+
continue
|
|
43
|
+
outp.printf(f"Loading blob {sha2hex} (size={member.size})")
|
|
44
|
+
try:
|
|
45
|
+
fobj = tar.extractfile(member)
|
|
46
|
+
except OSError as e:
|
|
47
|
+
outp.printf(f"WARNING: Error extracting {member.name}: {e}")
|
|
48
|
+
continue
|
|
49
|
+
if fobj is None:
|
|
50
|
+
outp.printf(f"Failed to extract {member.name} from tar archive.")
|
|
51
|
+
continue
|
|
52
|
+
async with await axon.upload() as upfd:
|
|
53
|
+
while True:
|
|
54
|
+
chunk = fobj.read(DEFAULT_CHUNK_SIZE)
|
|
55
|
+
if not chunk:
|
|
56
|
+
break
|
|
57
|
+
await upfd.write(chunk)
|
|
58
|
+
await upfd.save()
|
|
59
|
+
|
|
60
|
+
except s_exc.SynErr as exc:
|
|
61
|
+
return (False, s_exc.reprexc(exc))
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
mesg = f'Error {e} loading blobs into Axon url: {opts.url}'
|
|
65
|
+
return (False, mesg)
|
|
66
|
+
|
|
67
|
+
return (True, None)
|
|
68
|
+
|
|
69
|
+
async def main(argv, outp=s_output.stdout):
|
|
70
|
+
pars = s_cmd.Parser(prog='synapse.tools.axon.load', outp=outp, description=descr)
|
|
71
|
+
pars.add_argument('--url', default='cell:///vertex/storage', help='Telepath URL for the Axon.')
|
|
72
|
+
pars.add_argument('files', nargs='+', help='List of .tar.gz files to import from.')
|
|
73
|
+
|
|
74
|
+
opts = pars.parse_args(argv)
|
|
75
|
+
|
|
76
|
+
tarfiles = sorted([f for f in opts.files if f.endswith('.tar.gz')])
|
|
77
|
+
|
|
78
|
+
async with s_telepath.withTeleEnv():
|
|
79
|
+
(ok, mesg) = await loadBlobs(opts, outp, tarfiles)
|
|
80
|
+
if not ok:
|
|
81
|
+
outp.printf(f'ERROR: {mesg}')
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
outp.printf('Successfully loaded blobs.')
|
|
85
|
+
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
if __name__ == '__main__': # pragma: no cover
|
|
89
|
+
s_cmd.exitmain(main)
|
|
File without changes
|
|
File without changes
|