ominfra 0.0.0.dev87__tar.gz → 0.0.0.dev89__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.dev87/ominfra.egg-info → ominfra-0.0.0.dev89}/PKG-INFO +3 -3
  2. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/auth.py +4 -4
  3. ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/journald/genmessages.py +54 -0
  4. ominfra-0.0.0.dev87/ominfra/clouds/aws/journald2aws/journald.py → ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/journald/messages.py +0 -1
  5. ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/journald/tailer.py +108 -0
  6. ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/main.py +311 -0
  7. ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/threadworker.py +64 -0
  8. ominfra-0.0.0.dev89/ominfra/deploy/poly/__init__.py +1 -0
  9. ominfra-0.0.0.dev89/ominfra/scripts/journald2aws.py +2166 -0
  10. ominfra-0.0.0.dev89/ominfra/supervisor/__init__.py +1 -0
  11. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89/ominfra.egg-info}/PKG-INFO +3 -3
  12. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra.egg-info/SOURCES.txt +7 -1
  13. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra.egg-info/requires.txt +2 -2
  14. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/pyproject.toml +3 -3
  15. ominfra-0.0.0.dev87/ominfra/tools/__init__.py +0 -0
  16. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/LICENSE +0 -0
  17. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/MANIFEST.in +0 -0
  18. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/README.rst +0 -0
  19. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/.manifests.json +0 -0
  20. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/__about__.py +0 -0
  21. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/__init__.py +0 -0
  22. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/__init__.py +0 -0
  23. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/__init__.py +0 -0
  24. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/__main__.py +0 -0
  25. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/cli.py +0 -0
  26. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/dataclasses.py +0 -0
  27. {ominfra-0.0.0.dev87/ominfra/deploy/executor → ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws}/__init__.py +0 -0
  28. {ominfra-0.0.0.dev87/ominfra/deploy/poly → ominfra-0.0.0.dev89/ominfra/clouds/aws/journald2aws/journald}/__init__.py +0 -0
  29. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/logs.py +0 -0
  30. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/clouds/aws/metadata.py +0 -0
  31. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/cmds.py +0 -0
  32. {ominfra-0.0.0.dev87/ominfra/clouds/aws/journald2aws → ominfra-0.0.0.dev89/ominfra/deploy}/__init__.py +0 -0
  33. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/_executor.py +0 -0
  34. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/configs.py +0 -0
  35. {ominfra-0.0.0.dev87/ominfra/supervisor → ominfra-0.0.0.dev89/ominfra/deploy/executor}/__init__.py +0 -0
  36. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/base.py +0 -0
  37. {ominfra-0.0.0.dev87/ominfra/deploy → ominfra-0.0.0.dev89/ominfra/deploy/executor/concerns}/__init__.py +0 -0
  38. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/dirs.py +0 -0
  39. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/nginx.py +0 -0
  40. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/repo.py +0 -0
  41. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/supervisor.py +0 -0
  42. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/systemd.py +0 -0
  43. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/user.py +0 -0
  44. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/concerns/venv.py +0 -0
  45. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/executor/main.py +0 -0
  46. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/_main.py +0 -0
  47. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/base.py +0 -0
  48. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/configs.py +0 -0
  49. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/deploy.py +0 -0
  50. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/main.py +0 -0
  51. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/nginx.py +0 -0
  52. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/repo.py +0 -0
  53. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/runtime.py +0 -0
  54. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/site.py +0 -0
  55. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/supervisor.py +0 -0
  56. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/poly/venv.py +0 -0
  57. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/deploy/remote.py +0 -0
  58. {ominfra-0.0.0.dev87/ominfra/deploy/executor/concerns → ominfra-0.0.0.dev89/ominfra/manage}/__init__.py +0 -0
  59. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/manage/manage.py +0 -0
  60. {ominfra-0.0.0.dev87/ominfra/manage → ominfra-0.0.0.dev89/ominfra/pyremote}/__init__.py +0 -0
  61. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/pyremote/_runcommands.py +0 -0
  62. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/pyremote/bootstrap.py +0 -0
  63. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/pyremote/runcommands.py +0 -0
  64. {ominfra-0.0.0.dev87/ominfra/pyremote → ominfra-0.0.0.dev89/ominfra/scripts}/__init__.py +0 -0
  65. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/scripts/supervisor.py +0 -0
  66. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/ssh.py +0 -0
  67. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/__main__.py +0 -0
  68. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/compat.py +0 -0
  69. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/configs.py +0 -0
  70. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/context.py +0 -0
  71. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/datatypes.py +0 -0
  72. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/dispatchers.py +0 -0
  73. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/events.py +0 -0
  74. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/exceptions.py +0 -0
  75. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/poller.py +0 -0
  76. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/process.py +0 -0
  77. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/states.py +0 -0
  78. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/supervisor.py +0 -0
  79. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/supervisor/types.py +0 -0
  80. {ominfra-0.0.0.dev87/ominfra/scripts → ominfra-0.0.0.dev89/ominfra/tailscale}/__init__.py +0 -0
  81. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/tailscale/cli.py +0 -0
  82. {ominfra-0.0.0.dev87/ominfra/tailscale → ominfra-0.0.0.dev89/ominfra/tools}/__init__.py +0 -0
  83. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra/tools/listresources.py +0 -0
  84. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra.egg-info/dependency_links.txt +0 -0
  85. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra.egg-info/entry_points.txt +0 -0
  86. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/ominfra.egg-info/top_level.txt +0 -0
  87. {ominfra-0.0.0.dev87 → ominfra-0.0.0.dev89}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev87
3
+ Version: 0.0.0.dev89
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.dev87
16
- Requires-Dist: omlish==0.0.0.dev87
15
+ Requires-Dist: omdev==0.0.0.dev89
16
+ Requires-Dist: omlish==0.0.0.dev89
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,311 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: UP007
3
+ # @omlish-amalg ../../../scripts/journald2aws.py
4
+ """
5
+ https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
6
+
7
+ journalctl:
8
+ -o json
9
+ --show-cursor
10
+
11
+ --since "2012-10-30 18:17:16"
12
+ --until "2012-10-30 18:17:16"
13
+
14
+ --after-cursor <cursor>
15
+
16
+ ==
17
+
18
+ https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
19
+
20
+ ==
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class Journald2AwsConfig:
24
+ log_group_name: str
25
+ log_stream_name: str
26
+
27
+ aws_batch_size: int = 1_000
28
+ aws_flush_interval_s: float = 1.
29
+ """
30
+ import argparse
31
+ import contextlib
32
+ import dataclasses as dc
33
+ import json
34
+ import os.path
35
+ import queue
36
+ import sys
37
+ import time
38
+ import typing as ta
39
+ import urllib.request
40
+
41
+ from omlish.lite.cached import cached_nullary
42
+ from omlish.lite.check import check_non_empty_str
43
+ from omlish.lite.check import check_not_none
44
+ from omlish.lite.logs import configure_standard_logging
45
+ from omlish.lite.logs import log
46
+ from omlish.lite.marshal import unmarshal_obj
47
+ from omlish.lite.pidfile import Pidfile
48
+ from omlish.lite.runtime import is_debugger_attached
49
+
50
+ from ..auth import AwsSigner
51
+ from ..logs import AwsLogMessagePoster
52
+ from ..logs import AwsPutLogEventsResponse
53
+ from .journald.messages import JournalctlMessage # noqa
54
+ from .journald.tailer import JournalctlTailerWorker
55
+
56
+
57
+ @dc.dataclass(frozen=True)
58
+ class JournalctlOpts:
59
+ after_cursor: ta.Optional[str] = None
60
+
61
+ since: ta.Optional[str] = None
62
+ until: ta.Optional[str] = None
63
+
64
+
65
+ class JournalctlToAws:
66
+ @dc.dataclass(frozen=True)
67
+ class Config:
68
+ pid_file: ta.Optional[str] = None
69
+
70
+ cursor_file: ta.Optional[str] = None
71
+
72
+ #
73
+
74
+ aws_log_group_name: str = 'omlish'
75
+ aws_log_stream_name: ta.Optional[str] = None
76
+
77
+ aws_access_key_id: ta.Optional[str] = None
78
+ aws_secret_access_key: ta.Optional[str] = dc.field(default=None, repr=False)
79
+
80
+ aws_region_name: str = 'us-west-1'
81
+
82
+ #
83
+
84
+ journalctl_cmd: ta.Optional[ta.Sequence[str]] = None
85
+
86
+ journalctl_after_cursor: ta.Optional[str] = None
87
+ journalctl_since: ta.Optional[str] = None
88
+
89
+ #
90
+
91
+ dry_run: bool = False
92
+
93
+ def __init__(self, config: Config) -> None:
94
+ super().__init__()
95
+ self._config = config
96
+
97
+ #
98
+
99
+ _es: contextlib.ExitStack
100
+
101
+ def __enter__(self) -> 'JournalctlToAws':
102
+ self._es = contextlib.ExitStack().__enter__()
103
+ return self
104
+
105
+ def __exit__(self, exc_type, exc_val, exc_tb):
106
+ return self._es.__exit__(exc_type, exc_val, exc_tb)
107
+
108
+ #
109
+
110
+ @cached_nullary
111
+ def _pidfile(self) -> ta.Optional[Pidfile]:
112
+ if self._config.pid_file is None:
113
+ return None
114
+
115
+ pfp = os.path.expanduser(self._config.pid_file)
116
+
117
+ log.info('Opening pidfile %s', pfp)
118
+
119
+ pf = self._es.enter_context(Pidfile(pfp))
120
+ pf.write()
121
+ return pf
122
+
123
+ def _ensure_locked(self) -> None:
124
+ if (pf := self._pidfile()) is not None:
125
+ pf.ensure_locked()
126
+
127
+ #
128
+
129
+ def _read_cursor_file(self) -> ta.Optional[str]:
130
+ self._ensure_locked()
131
+
132
+ if not (cf := self._config.cursor_file):
133
+ return None
134
+ cf = os.path.expanduser(cf)
135
+
136
+ try:
137
+ with open(cf) as f:
138
+ return f.read().strip()
139
+ except FileNotFoundError:
140
+ return None
141
+
142
+ def _write_cursor_file(self, cursor: str) -> None:
143
+ self._ensure_locked()
144
+
145
+ if not (cf := self._config.cursor_file):
146
+ return
147
+ cf = os.path.expanduser(cf)
148
+
149
+ log.info('Writing cursor file %s : %s', cf, cursor)
150
+ with open(ncf := cf + '.next', 'w') as f:
151
+ f.write(cursor)
152
+
153
+ os.rename(ncf, cf)
154
+
155
+ #
156
+
157
+ @cached_nullary
158
+ def _aws_credentials(self) -> AwsSigner.Credentials:
159
+ return AwsSigner.Credentials(
160
+ access_key_id=check_non_empty_str(self._config.aws_access_key_id),
161
+ secret_access_key=check_non_empty_str(self._config.aws_secret_access_key),
162
+ )
163
+
164
+ @cached_nullary
165
+ def _aws_log_message_poster(self) -> AwsLogMessagePoster:
166
+ return AwsLogMessagePoster(
167
+ log_group_name=self._config.aws_log_group_name,
168
+ log_stream_name=check_non_empty_str(self._config.aws_log_stream_name),
169
+ region_name=self._config.aws_region_name,
170
+ credentials=check_not_none(self._aws_credentials()),
171
+ )
172
+
173
+ #
174
+
175
+ @cached_nullary
176
+ def _journalctl_message_queue(self): # type: () -> queue.Queue[ta.Sequence[JournalctlMessage]]
177
+ return queue.Queue()
178
+
179
+ @cached_nullary
180
+ def _journalctl_tailer_worker(self) -> JournalctlTailerWorker:
181
+ ac: ta.Optional[str] = self._config.journalctl_after_cursor
182
+ if ac is None:
183
+ ac = self._read_cursor_file()
184
+ if ac is not None:
185
+ log.info('Starting from cursor %s', ac)
186
+
187
+ if (since := self._config.journalctl_since):
188
+ log.info('Starting since %s', since)
189
+
190
+ return JournalctlTailerWorker(
191
+ self._journalctl_message_queue(),
192
+
193
+ since=since,
194
+ after_cursor=ac,
195
+
196
+ cmd=self._config.journalctl_cmd,
197
+ shell_wrap=is_debugger_attached(),
198
+ )
199
+
200
+ #
201
+
202
+ def run(self) -> None:
203
+ self._ensure_locked()
204
+
205
+ q = self._journalctl_message_queue()
206
+ jtw = self._journalctl_tailer_worker()
207
+ mp = self._aws_log_message_poster()
208
+
209
+ jtw.start()
210
+
211
+ last_cursor: ta.Optional[str] = None # noqa
212
+ while True:
213
+ if not jtw.is_alive():
214
+ log.critical('Journalctl tailer worker died')
215
+ break
216
+
217
+ msgs: ta.Sequence[JournalctlMessage] = q.get()
218
+ log.debug('%r', msgs)
219
+
220
+ cur_cursor: ta.Optional[str] = None
221
+ for m in reversed(msgs):
222
+ if m.cursor is not None:
223
+ cur_cursor = m.cursor
224
+ break
225
+
226
+ if not msgs:
227
+ log.warning('Empty queue chunk')
228
+ continue
229
+
230
+ [post] = mp.feed([mp.Message(
231
+ message=json.dumps(m.dct),
232
+ ts_ms=int(time.time() * 1000.),
233
+ ) for m in msgs])
234
+ log.debug('%r', post)
235
+
236
+ if not self._config.dry_run:
237
+ with urllib.request.urlopen(urllib.request.Request( # noqa
238
+ post.url,
239
+ method='POST',
240
+ headers=dict(post.headers),
241
+ data=post.data,
242
+ )) as resp:
243
+ response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
244
+ log.debug('%r', response)
245
+
246
+ if cur_cursor is not None:
247
+ self._write_cursor_file(cur_cursor)
248
+ last_cursor = cur_cursor # noqa
249
+
250
+
251
+ def _main() -> None:
252
+ parser = argparse.ArgumentParser()
253
+
254
+ parser.add_argument('--config-file')
255
+ parser.add_argument('-v', '--verbose', action='store_true')
256
+
257
+ parser.add_argument('--after-cursor', nargs='?')
258
+ parser.add_argument('--since', nargs='?')
259
+ parser.add_argument('--dry-run', action='store_true')
260
+
261
+ parser.add_argument('--message', nargs='?')
262
+ parser.add_argument('--real', action='store_true')
263
+
264
+ args = parser.parse_args()
265
+
266
+ #
267
+
268
+ configure_standard_logging('DEBUG' if args.verbose else 'INFO')
269
+
270
+ #
271
+
272
+ config: JournalctlToAws.Config
273
+ if args.config_file:
274
+ with open(os.path.expanduser(args.config_file)) as cf:
275
+ config_dct = json.load(cf)
276
+ config = unmarshal_obj(config_dct, JournalctlToAws.Config)
277
+ else:
278
+ config = JournalctlToAws.Config()
279
+
280
+ #
281
+
282
+ for k in ['aws_access_key_id', 'aws_secret_access_key']:
283
+ if not getattr(config, k) and k.upper() in os.environ:
284
+ config = dc.replace(config, **{k: os.environ.get(k.upper())}) # type: ignore
285
+
286
+ #
287
+
288
+ if not args.real:
289
+ config = dc.replace(config, journalctl_cmd=[
290
+ sys.executable,
291
+ os.path.join(os.path.dirname(__file__), 'journald', 'genmessages.py'),
292
+ '--sleep-n', '2',
293
+ '--sleep-s', '.5',
294
+ *(['--message', args.message] if args.message else []),
295
+ '100000',
296
+ ])
297
+
298
+ #
299
+
300
+ for a in ['after_cursor', 'since', 'dry_run']:
301
+ if (pa := getattr(args, a)):
302
+ config = dc.replace(config, **{a: pa})
303
+
304
+ #
305
+
306
+ with JournalctlToAws(config) as jta:
307
+ jta.run()
308
+
309
+
310
+ if __name__ == '__main__':
311
+ _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