ominfra 0.0.0.dev88__tar.gz → 0.0.0.dev90__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. {ominfra-0.0.0.dev88/ominfra.egg-info → ominfra-0.0.0.dev90}/PKG-INFO +3 -3
  2. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/auth.py +4 -4
  3. ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/journald/genmessages.py +54 -0
  4. ominfra-0.0.0.dev88/ominfra/clouds/aws/journald2aws/journald.py → ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/journald/messages.py +0 -1
  5. ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/journald/tailer.py +108 -0
  6. ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/main.py +317 -0
  7. ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/threadworker.py +64 -0
  8. ominfra-0.0.0.dev90/ominfra/deploy/poly/__init__.py +1 -0
  9. ominfra-0.0.0.dev90/ominfra/scripts/journald2aws.py +2172 -0
  10. ominfra-0.0.0.dev90/ominfra/supervisor/__init__.py +1 -0
  11. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90/ominfra.egg-info}/PKG-INFO +3 -3
  12. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra.egg-info/SOURCES.txt +7 -1
  13. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra.egg-info/requires.txt +2 -2
  14. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/pyproject.toml +3 -3
  15. ominfra-0.0.0.dev88/ominfra/tools/__init__.py +0 -0
  16. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/LICENSE +0 -0
  17. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/MANIFEST.in +0 -0
  18. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/README.rst +0 -0
  19. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/.manifests.json +0 -0
  20. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/__about__.py +0 -0
  21. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/__init__.py +0 -0
  22. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/__init__.py +0 -0
  23. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/__init__.py +0 -0
  24. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/__main__.py +0 -0
  25. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/cli.py +0 -0
  26. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/dataclasses.py +0 -0
  27. {ominfra-0.0.0.dev88/ominfra/deploy/executor → ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws}/__init__.py +0 -0
  28. {ominfra-0.0.0.dev88/ominfra/deploy/poly → ominfra-0.0.0.dev90/ominfra/clouds/aws/journald2aws/journald}/__init__.py +0 -0
  29. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/logs.py +0 -0
  30. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/clouds/aws/metadata.py +0 -0
  31. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/cmds.py +0 -0
  32. {ominfra-0.0.0.dev88/ominfra/clouds/aws/journald2aws → ominfra-0.0.0.dev90/ominfra/deploy}/__init__.py +0 -0
  33. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/_executor.py +0 -0
  34. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/configs.py +0 -0
  35. {ominfra-0.0.0.dev88/ominfra/supervisor → ominfra-0.0.0.dev90/ominfra/deploy/executor}/__init__.py +0 -0
  36. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/base.py +0 -0
  37. {ominfra-0.0.0.dev88/ominfra/deploy → ominfra-0.0.0.dev90/ominfra/deploy/executor/concerns}/__init__.py +0 -0
  38. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/dirs.py +0 -0
  39. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/nginx.py +0 -0
  40. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/repo.py +0 -0
  41. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/supervisor.py +0 -0
  42. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/systemd.py +0 -0
  43. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/user.py +0 -0
  44. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/concerns/venv.py +0 -0
  45. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/executor/main.py +0 -0
  46. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/_main.py +0 -0
  47. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/base.py +0 -0
  48. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/configs.py +0 -0
  49. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/deploy.py +0 -0
  50. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/main.py +0 -0
  51. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/nginx.py +0 -0
  52. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/repo.py +0 -0
  53. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/runtime.py +0 -0
  54. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/site.py +0 -0
  55. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/supervisor.py +0 -0
  56. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/poly/venv.py +0 -0
  57. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/deploy/remote.py +0 -0
  58. {ominfra-0.0.0.dev88/ominfra/deploy/executor/concerns → ominfra-0.0.0.dev90/ominfra/manage}/__init__.py +0 -0
  59. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/manage/manage.py +0 -0
  60. {ominfra-0.0.0.dev88/ominfra/manage → ominfra-0.0.0.dev90/ominfra/pyremote}/__init__.py +0 -0
  61. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/pyremote/_runcommands.py +0 -0
  62. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/pyremote/bootstrap.py +0 -0
  63. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/pyremote/runcommands.py +0 -0
  64. {ominfra-0.0.0.dev88/ominfra/pyremote → ominfra-0.0.0.dev90/ominfra/scripts}/__init__.py +0 -0
  65. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/scripts/supervisor.py +0 -0
  66. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/ssh.py +0 -0
  67. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/__main__.py +0 -0
  68. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/compat.py +0 -0
  69. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/configs.py +0 -0
  70. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/context.py +0 -0
  71. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/datatypes.py +0 -0
  72. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/dispatchers.py +0 -0
  73. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/events.py +0 -0
  74. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/exceptions.py +0 -0
  75. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/poller.py +0 -0
  76. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/process.py +0 -0
  77. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/states.py +0 -0
  78. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/supervisor.py +0 -0
  79. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/supervisor/types.py +0 -0
  80. {ominfra-0.0.0.dev88/ominfra/scripts → ominfra-0.0.0.dev90/ominfra/tailscale}/__init__.py +0 -0
  81. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/tailscale/cli.py +0 -0
  82. {ominfra-0.0.0.dev88/ominfra/tailscale → ominfra-0.0.0.dev90/ominfra/tools}/__init__.py +0 -0
  83. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra/tools/listresources.py +0 -0
  84. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra.egg-info/dependency_links.txt +0 -0
  85. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra.egg-info/entry_points.txt +0 -0
  86. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/ominfra.egg-info/top_level.txt +0 -0
  87. {ominfra-0.0.0.dev88 → ominfra-0.0.0.dev90}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev88
3
+ Version: 0.0.0.dev90
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: ~=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev88
16
- Requires-Dist: omlish==0.0.0.dev88
15
+ Requires-Dist: omdev==0.0.0.dev90
16
+ Requires-Dist: omlish==0.0.0.dev90
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"
@@ -41,8 +41,8 @@ class AwsSigner:
41
41
 
42
42
  @dc.dataclass(frozen=True)
43
43
  class Credentials:
44
- access_key: str
45
- secret_key: str = dc.field(repr=False)
44
+ access_key_id: str
45
+ secret_access_key: str = dc.field(repr=False)
46
46
 
47
47
  @dc.dataclass(frozen=True)
48
48
  class Request:
@@ -198,7 +198,7 @@ class V4AwsSigner(AwsSigner):
198
198
 
199
199
  #
200
200
 
201
- key = self._creds.secret_key
201
+ key = self._creds.secret_access_key
202
202
  key_date = self._sha256_sign(f'AWS4{key}'.encode('utf-8'), req_dt[:8]) # noqa
203
203
  key_region = self._sha256_sign(key_date, self._region_name)
204
204
  key_service = self._sha256_sign(key_region, self._service_name)
@@ -208,7 +208,7 @@ class V4AwsSigner(AwsSigner):
208
208
  #
209
209
 
210
210
  cred_scope = '/'.join([
211
- self._creds.access_key,
211
+ self._creds.access_key_id,
212
212
  *scope_parts,
213
213
  ])
214
214
  auth = f'{algorithm} ' + ', '.join([
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ # @omlish-script
3
+ import argparse
4
+ import datetime
5
+ import json
6
+ import re
7
+ import sys
8
+ import time
9
+ import uuid
10
+
11
+
12
+ def _main() -> None:
13
+ parser = argparse.ArgumentParser()
14
+
15
+ parser.add_argument('n', type=int, default=10, nargs='?')
16
+ parser.add_argument('--message', default='message {i}', nargs='?')
17
+ parser.add_argument('--sleep-s', type=float, nargs='?')
18
+ parser.add_argument('--sleep-n', type=int, nargs='?')
19
+
20
+ parser.add_argument('--after-cursor', nargs='?')
21
+
22
+ # Ignored
23
+ parser.add_argument('--output', nargs='?')
24
+ parser.add_argument('--follow', action='store_true')
25
+ parser.add_argument('--show-cursor', action='store_true')
26
+ parser.add_argument('--since', nargs='?')
27
+
28
+ args = parser.parse_args()
29
+
30
+ if (ac := args.after_cursor) is not None:
31
+ if not (m := re.fullmatch(r'cursor:(?P<n>\d+)', ac)):
32
+ raise ValueError(ac)
33
+ start = int(m.groupdict()['n'])
34
+ else:
35
+ start = 0
36
+
37
+ for i in range(start, args.n):
38
+ if args.sleep_s:
39
+ if not args.sleep_n or (i and i % args.sleep_n == 0):
40
+ time.sleep(args.sleep_s)
41
+
42
+ ts_us = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() * 1_000_000 # noqa
43
+ dct = {
44
+ 'MESSAGE': args.message.format(i=i),
45
+ 'MESSAGE_ID': uuid.uuid4().hex,
46
+ '__CURSOR': f'cursor:{i}',
47
+ '_SOURCE_REALTIME_TIMESTAMP': str(int(ts_us)),
48
+ }
49
+ print(json.dumps(dct, indent=None, separators=(',', ':')))
50
+ sys.stdout.flush()
51
+
52
+
53
+ if __name__ == '__main__':
54
+ _main()
@@ -1,5 +1,4 @@
1
1
  # ruff: noqa: UP006 UP007
2
- # @omlish-lite
3
2
  import dataclasses as dc
4
3
  import json
5
4
  import typing as ta
@@ -0,0 +1,108 @@
1
+ # ruff: noqa: UP007
2
+ import fcntl
3
+ import os.path
4
+ import queue # noqa
5
+ import subprocess
6
+ import time
7
+ import typing as ta
8
+
9
+ from omlish.lite.cached import cached_nullary
10
+ from omlish.lite.check import check_not_none
11
+ from omlish.lite.logs import log
12
+ from omlish.lite.subprocesses import subprocess_shell_wrap_exec
13
+
14
+ from ..threadworker import ThreadWorker
15
+ from .messages import JournalctlMessage # noqa
16
+ from .messages import JournalctlMessageBuilder
17
+
18
+
19
+ class JournalctlTailerWorker(ThreadWorker):
20
+ DEFAULT_CMD: ta.ClassVar[ta.Sequence[str]] = ['journalctl']
21
+
22
+ def __init__(
23
+ self,
24
+ output, # type: queue.Queue[ta.Sequence[JournalctlMessage]]
25
+ *,
26
+ since: ta.Optional[str] = None,
27
+ after_cursor: ta.Optional[str] = None,
28
+
29
+ cmd: ta.Optional[ta.Sequence[str]] = None,
30
+ shell_wrap: bool = False,
31
+
32
+ read_size: int = 0x4000,
33
+ sleep_s: float = 1.,
34
+
35
+ **kwargs: ta.Any,
36
+ ) -> None:
37
+ super().__init__(**kwargs)
38
+
39
+ self._output = output
40
+
41
+ self._since = since
42
+ self._after_cursor = after_cursor
43
+
44
+ self._cmd = cmd or self.DEFAULT_CMD
45
+ self._shell_wrap = shell_wrap
46
+
47
+ self._read_size = read_size
48
+ self._sleep_s = sleep_s
49
+
50
+ self._mb = JournalctlMessageBuilder()
51
+
52
+ self._proc: ta.Optional[subprocess.Popen] = None
53
+
54
+ @cached_nullary
55
+ def _full_cmd(self) -> ta.Sequence[str]:
56
+ cmd = [
57
+ *self._cmd,
58
+ '--output', 'json',
59
+ '--show-cursor',
60
+ '--follow',
61
+ ]
62
+
63
+ if self._since is not None:
64
+ cmd.extend(['--since', self._since])
65
+
66
+ if self._after_cursor is not None:
67
+ cmd.extend(['--after-cursor', self._after_cursor])
68
+
69
+ if self._shell_wrap:
70
+ cmd = list(subprocess_shell_wrap_exec(*cmd))
71
+
72
+ return cmd
73
+
74
+ def _run(self) -> None:
75
+ with subprocess.Popen(
76
+ self._full_cmd(),
77
+ stdout=subprocess.PIPE,
78
+ ) as self._proc:
79
+ stdout = check_not_none(self._proc.stdout)
80
+
81
+ fd = stdout.fileno()
82
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
83
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
84
+
85
+ while True:
86
+ if not self._heartbeat():
87
+ break
88
+
89
+ while stdout.readable():
90
+ if not self._heartbeat():
91
+ break
92
+
93
+ buf = stdout.read(self._read_size)
94
+ if not buf:
95
+ log.debug('Journalctl empty read')
96
+ break
97
+
98
+ log.debug('Journalctl read buffer: %r', buf)
99
+ msgs = self._mb.feed(buf)
100
+ if msgs:
101
+ self._output.put(msgs)
102
+
103
+ if self._proc.poll() is not None:
104
+ log.critical('Journalctl process terminated')
105
+ break
106
+
107
+ log.debug('Journalctl readable')
108
+ time.sleep(self._sleep_s)
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: UP007
3
+ # @omlish-amalg ../../../scripts/journald2aws.py
4
+ """
5
+ TODO:
6
+ - create log group
7
+ - log stats - chunk sizes etc
8
+
9
+ ==
10
+
11
+ https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
12
+
13
+ journalctl:
14
+ -o json
15
+ --show-cursor
16
+
17
+ --since "2012-10-30 18:17:16"
18
+ --until "2012-10-30 18:17:16"
19
+
20
+ --after-cursor <cursor>
21
+
22
+ ==
23
+
24
+ https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
25
+
26
+ ==
27
+
28
+ @dc.dataclass(frozen=True)
29
+ class Journald2AwsConfig:
30
+ log_group_name: str
31
+ log_stream_name: str
32
+
33
+ aws_batch_size: int = 1_000
34
+ aws_flush_interval_s: float = 1.
35
+ """
36
+ import argparse
37
+ import contextlib
38
+ import dataclasses as dc
39
+ import json
40
+ import os.path
41
+ import queue
42
+ import sys
43
+ import time
44
+ import typing as ta
45
+ import urllib.request
46
+
47
+ from omlish.lite.cached import cached_nullary
48
+ from omlish.lite.check import check_non_empty_str
49
+ from omlish.lite.check import check_not_none
50
+ from omlish.lite.logs import configure_standard_logging
51
+ from omlish.lite.logs import log
52
+ from omlish.lite.marshal import unmarshal_obj
53
+ from omlish.lite.pidfile import Pidfile
54
+ from omlish.lite.runtime import is_debugger_attached
55
+
56
+ from ..auth import AwsSigner
57
+ from ..logs import AwsLogMessagePoster
58
+ from ..logs import AwsPutLogEventsResponse
59
+ from .journald.messages import JournalctlMessage # noqa
60
+ from .journald.tailer import JournalctlTailerWorker
61
+
62
+
63
+ @dc.dataclass(frozen=True)
64
+ class JournalctlOpts:
65
+ after_cursor: ta.Optional[str] = None
66
+
67
+ since: ta.Optional[str] = None
68
+ until: ta.Optional[str] = None
69
+
70
+
71
+ class JournalctlToAws:
72
+ @dc.dataclass(frozen=True)
73
+ class Config:
74
+ pid_file: ta.Optional[str] = None
75
+
76
+ cursor_file: ta.Optional[str] = None
77
+
78
+ #
79
+
80
+ aws_log_group_name: str = 'omlish'
81
+ aws_log_stream_name: ta.Optional[str] = None
82
+
83
+ aws_access_key_id: ta.Optional[str] = None
84
+ aws_secret_access_key: ta.Optional[str] = dc.field(default=None, repr=False)
85
+
86
+ aws_region_name: str = 'us-west-1'
87
+
88
+ #
89
+
90
+ journalctl_cmd: ta.Optional[ta.Sequence[str]] = None
91
+
92
+ journalctl_after_cursor: ta.Optional[str] = None
93
+ journalctl_since: ta.Optional[str] = None
94
+
95
+ #
96
+
97
+ dry_run: bool = False
98
+
99
+ def __init__(self, config: Config) -> None:
100
+ super().__init__()
101
+ self._config = config
102
+
103
+ #
104
+
105
+ _es: contextlib.ExitStack
106
+
107
+ def __enter__(self) -> 'JournalctlToAws':
108
+ self._es = contextlib.ExitStack().__enter__()
109
+ return self
110
+
111
+ def __exit__(self, exc_type, exc_val, exc_tb):
112
+ return self._es.__exit__(exc_type, exc_val, exc_tb)
113
+
114
+ #
115
+
116
+ @cached_nullary
117
+ def _pidfile(self) -> ta.Optional[Pidfile]:
118
+ if self._config.pid_file is None:
119
+ return None
120
+
121
+ pfp = os.path.expanduser(self._config.pid_file)
122
+
123
+ log.info('Opening pidfile %s', pfp)
124
+
125
+ pf = self._es.enter_context(Pidfile(pfp))
126
+ pf.write()
127
+ return pf
128
+
129
+ def _ensure_locked(self) -> None:
130
+ if (pf := self._pidfile()) is not None:
131
+ pf.ensure_locked()
132
+
133
+ #
134
+
135
+ def _read_cursor_file(self) -> ta.Optional[str]:
136
+ self._ensure_locked()
137
+
138
+ if not (cf := self._config.cursor_file):
139
+ return None
140
+ cf = os.path.expanduser(cf)
141
+
142
+ try:
143
+ with open(cf) as f:
144
+ return f.read().strip()
145
+ except FileNotFoundError:
146
+ return None
147
+
148
+ def _write_cursor_file(self, cursor: str) -> None:
149
+ self._ensure_locked()
150
+
151
+ if not (cf := self._config.cursor_file):
152
+ return
153
+ cf = os.path.expanduser(cf)
154
+
155
+ log.info('Writing cursor file %s : %s', cf, cursor)
156
+ with open(ncf := cf + '.next', 'w') as f:
157
+ f.write(cursor)
158
+
159
+ os.rename(ncf, cf)
160
+
161
+ #
162
+
163
+ @cached_nullary
164
+ def _aws_credentials(self) -> AwsSigner.Credentials:
165
+ return AwsSigner.Credentials(
166
+ access_key_id=check_non_empty_str(self._config.aws_access_key_id),
167
+ secret_access_key=check_non_empty_str(self._config.aws_secret_access_key),
168
+ )
169
+
170
+ @cached_nullary
171
+ def _aws_log_message_poster(self) -> AwsLogMessagePoster:
172
+ return AwsLogMessagePoster(
173
+ log_group_name=self._config.aws_log_group_name,
174
+ log_stream_name=check_non_empty_str(self._config.aws_log_stream_name),
175
+ region_name=self._config.aws_region_name,
176
+ credentials=check_not_none(self._aws_credentials()),
177
+ )
178
+
179
+ #
180
+
181
+ @cached_nullary
182
+ def _journalctl_message_queue(self): # type: () -> queue.Queue[ta.Sequence[JournalctlMessage]]
183
+ return queue.Queue()
184
+
185
+ @cached_nullary
186
+ def _journalctl_tailer_worker(self) -> JournalctlTailerWorker:
187
+ ac: ta.Optional[str] = self._config.journalctl_after_cursor
188
+ if ac is None:
189
+ ac = self._read_cursor_file()
190
+ if ac is not None:
191
+ log.info('Starting from cursor %s', ac)
192
+
193
+ if (since := self._config.journalctl_since):
194
+ log.info('Starting since %s', since)
195
+
196
+ return JournalctlTailerWorker(
197
+ self._journalctl_message_queue(),
198
+
199
+ since=since,
200
+ after_cursor=ac,
201
+
202
+ cmd=self._config.journalctl_cmd,
203
+ shell_wrap=is_debugger_attached(),
204
+ )
205
+
206
+ #
207
+
208
+ def run(self) -> None:
209
+ self._ensure_locked()
210
+
211
+ q = self._journalctl_message_queue()
212
+ jtw = self._journalctl_tailer_worker()
213
+ mp = self._aws_log_message_poster()
214
+
215
+ jtw.start()
216
+
217
+ last_cursor: ta.Optional[str] = None # noqa
218
+ while True:
219
+ if not jtw.is_alive():
220
+ log.critical('Journalctl tailer worker died')
221
+ break
222
+
223
+ msgs: ta.Sequence[JournalctlMessage] = q.get()
224
+ log.debug('%r', msgs)
225
+
226
+ cur_cursor: ta.Optional[str] = None
227
+ for m in reversed(msgs):
228
+ if m.cursor is not None:
229
+ cur_cursor = m.cursor
230
+ break
231
+
232
+ if not msgs:
233
+ log.warning('Empty queue chunk')
234
+ continue
235
+
236
+ [post] = mp.feed([mp.Message(
237
+ message=json.dumps(m.dct),
238
+ ts_ms=int(time.time() * 1000.),
239
+ ) for m in msgs])
240
+ log.debug('%r', post)
241
+
242
+ if not self._config.dry_run:
243
+ with urllib.request.urlopen(urllib.request.Request( # noqa
244
+ post.url,
245
+ method='POST',
246
+ headers=dict(post.headers),
247
+ data=post.data,
248
+ )) as resp:
249
+ response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
250
+ log.debug('%r', response)
251
+
252
+ if cur_cursor is not None:
253
+ self._write_cursor_file(cur_cursor)
254
+ last_cursor = cur_cursor # noqa
255
+
256
+
257
+ def _main() -> None:
258
+ parser = argparse.ArgumentParser()
259
+
260
+ parser.add_argument('--config-file')
261
+ parser.add_argument('-v', '--verbose', action='store_true')
262
+
263
+ parser.add_argument('--after-cursor', nargs='?')
264
+ parser.add_argument('--since', nargs='?')
265
+ parser.add_argument('--dry-run', action='store_true')
266
+
267
+ parser.add_argument('--message', nargs='?')
268
+ parser.add_argument('--real', action='store_true')
269
+
270
+ args = parser.parse_args()
271
+
272
+ #
273
+
274
+ configure_standard_logging('DEBUG' if args.verbose else 'INFO')
275
+
276
+ #
277
+
278
+ config: JournalctlToAws.Config
279
+ if args.config_file:
280
+ with open(os.path.expanduser(args.config_file)) as cf:
281
+ config_dct = json.load(cf)
282
+ config = unmarshal_obj(config_dct, JournalctlToAws.Config)
283
+ else:
284
+ config = JournalctlToAws.Config()
285
+
286
+ #
287
+
288
+ for k in ['aws_access_key_id', 'aws_secret_access_key']:
289
+ if not getattr(config, k) and k.upper() in os.environ:
290
+ config = dc.replace(config, **{k: os.environ.get(k.upper())}) # type: ignore
291
+
292
+ #
293
+
294
+ if not args.real:
295
+ config = dc.replace(config, journalctl_cmd=[
296
+ sys.executable,
297
+ os.path.join(os.path.dirname(__file__), 'journald', 'genmessages.py'),
298
+ '--sleep-n', '2',
299
+ '--sleep-s', '.5',
300
+ *(['--message', args.message] if args.message else []),
301
+ '100000',
302
+ ])
303
+
304
+ #
305
+
306
+ for a in ['after_cursor', 'since', 'dry_run']:
307
+ if (pa := getattr(args, a)):
308
+ config = dc.replace(config, **{a: pa})
309
+
310
+ #
311
+
312
+ with JournalctlToAws(config) as jta:
313
+ jta.run()
314
+
315
+
316
+ if __name__ == '__main__':
317
+ _main()
@@ -0,0 +1,64 @@
1
+ # ruff: noqa: UP007
2
+ import abc
3
+ import threading
4
+ import time
5
+ import typing as ta
6
+
7
+ from omlish.lite.logs import log
8
+
9
+
10
+ class ThreadWorker(abc.ABC):
11
+ def __init__(
12
+ self,
13
+ *,
14
+ stop_event: ta.Optional[threading.Event] = None,
15
+ ) -> None:
16
+ super().__init__()
17
+
18
+ if stop_event is None:
19
+ stop_event = threading.Event()
20
+ self._stop_event = stop_event
21
+
22
+ self._thread: ta.Optional[threading.Thread] = None
23
+
24
+ self._last_heartbeat: ta.Optional[float] = None
25
+
26
+ #
27
+
28
+ def should_stop(self) -> bool:
29
+ return self._stop_event.is_set()
30
+
31
+ #
32
+
33
+ @property
34
+ def last_heartbeat(self) -> ta.Optional[float]:
35
+ return self._last_heartbeat
36
+
37
+ def _heartbeat(self) -> bool:
38
+ self._last_heartbeat = time.time()
39
+
40
+ if self.should_stop():
41
+ log.info('Stopping: %s', self)
42
+ return False
43
+
44
+ return True
45
+
46
+ #
47
+
48
+ def is_alive(self) -> bool:
49
+ return (thr := self._thread) is not None and thr.is_alive()
50
+
51
+ def start(self) -> None:
52
+ thr = threading.Thread(target=self._run)
53
+ self._thread = thr
54
+ thr.start()
55
+
56
+ @abc.abstractmethod
57
+ def _run(self) -> None:
58
+ raise NotImplementedError
59
+
60
+ def stop(self) -> None:
61
+ raise NotImplementedError
62
+
63
+ def cleanup(self) -> None: # noqa
64
+ pass
@@ -0,0 +1 @@
1
+ # @omlish-lite