ominfra 0.0.0.dev76__py3-none-any.whl → 0.0.0.dev78__py3-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.
- ominfra/clouds/aws/auth.py +97 -92
- ominfra/clouds/aws/dataclasses.py +149 -0
- ominfra/clouds/aws/journald2aws/__init__.py +0 -0
- ominfra/clouds/aws/journald2aws/journald.py +67 -0
- ominfra/clouds/aws/logs.py +173 -0
- ominfra/deploy/_executor.py +17 -0
- ominfra/pyremote/_runcommands.py +17 -0
- ominfra/scripts/__init__.py +0 -0
- ominfra/scripts/supervisor.py +3323 -0
- ominfra/supervisor/__init__.py +1 -0
- ominfra/supervisor/__main__.py +4 -0
- ominfra/supervisor/compat.py +208 -0
- ominfra/supervisor/configs.py +110 -0
- ominfra/supervisor/context.py +405 -0
- ominfra/supervisor/datatypes.py +171 -0
- ominfra/supervisor/dispatchers.py +307 -0
- ominfra/supervisor/events.py +304 -0
- ominfra/supervisor/exceptions.py +22 -0
- ominfra/supervisor/poller.py +232 -0
- ominfra/supervisor/process.py +782 -0
- ominfra/supervisor/states.py +78 -0
- ominfra/supervisor/supervisor.py +390 -0
- ominfra/supervisor/types.py +49 -0
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/RECORD +29 -9
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev76.dist-info → ominfra-0.0.0.dev78.dist-info}/top_level.txt +0 -0
ominfra/clouds/aws/auth.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
1
3
|
"""
|
2
4
|
https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
|
3
5
|
|
@@ -6,6 +8,7 @@ TODO:
|
|
6
8
|
- boto / s3transfer upload_fileobj doesn't stream either lol - eagerly calcs Content-MD5
|
7
9
|
- sts tokens
|
8
10
|
- !! fix canonical_qs - sort params
|
11
|
+
- secrets
|
9
12
|
"""
|
10
13
|
import dataclasses as dc
|
11
14
|
import datetime
|
@@ -14,123 +17,125 @@ import hmac
|
|
14
17
|
import typing as ta
|
15
18
|
import urllib.parse
|
16
19
|
|
17
|
-
from omlish import
|
18
|
-
from omlish import
|
20
|
+
from omlish.lite.check import check_equal
|
21
|
+
from omlish.lite.check import check_non_empty_str
|
22
|
+
from omlish.lite.check import check_not_isinstance
|
19
23
|
|
20
24
|
|
21
25
|
##
|
22
26
|
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@dc.dataclass(frozen=True)
|
37
|
-
class Credentials:
|
38
|
-
access_key: str
|
39
|
-
secret_key: str = dc.field(repr=False)
|
40
|
-
|
28
|
+
class AwsSigner:
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
creds: 'AwsSigner.Credentials',
|
32
|
+
region_name: str,
|
33
|
+
service_name: str,
|
34
|
+
) -> None:
|
35
|
+
super().__init__()
|
36
|
+
self._creds = creds
|
37
|
+
self._region_name = region_name
|
38
|
+
self._service_name = service_name
|
41
39
|
|
42
|
-
|
43
|
-
class Request:
|
44
|
-
method: str
|
45
|
-
url: str
|
46
|
-
headers: HttpMap = dc.field(default_factory=dict)
|
47
|
-
payload: bytes = b''
|
40
|
+
#
|
48
41
|
|
42
|
+
@dc.dataclass(frozen=True)
|
43
|
+
class Credentials:
|
44
|
+
access_key: str
|
45
|
+
secret_key: str = dc.field(repr=False)
|
49
46
|
|
50
|
-
|
47
|
+
@dc.dataclass(frozen=True)
|
48
|
+
class Request:
|
49
|
+
method: str
|
50
|
+
url: str
|
51
|
+
headers: ta.Mapping[str, ta.Sequence[str]] = dc.field(default_factory=dict)
|
52
|
+
payload: bytes = b''
|
51
53
|
|
54
|
+
#
|
52
55
|
|
53
|
-
|
54
|
-
url_parts = urllib.parse.urlsplit(url)
|
55
|
-
host = check.non_empty_str(url_parts.hostname)
|
56
|
-
default_ports = {
|
57
|
-
'http': 80,
|
58
|
-
'https': 443,
|
59
|
-
}
|
60
|
-
if url_parts.port is not None:
|
61
|
-
if url_parts.port != default_ports.get(url_parts.scheme):
|
62
|
-
host = '%s:%d' % (host, url_parts.port)
|
63
|
-
return host
|
56
|
+
ISO8601 = '%Y%m%dT%H%M%SZ'
|
64
57
|
|
58
|
+
#
|
65
59
|
|
66
|
-
|
67
|
-
|
60
|
+
@staticmethod
|
61
|
+
def _host_from_url(url: str) -> str:
|
62
|
+
url_parts = urllib.parse.urlsplit(url)
|
63
|
+
host = check_non_empty_str(url_parts.hostname)
|
64
|
+
default_ports = {
|
65
|
+
'http': 80,
|
66
|
+
'https': 443,
|
67
|
+
}
|
68
|
+
if url_parts.port is not None:
|
69
|
+
if url_parts.port != default_ports.get(url_parts.scheme):
|
70
|
+
host = '%s:%d' % (host, url_parts.port)
|
71
|
+
return host
|
68
72
|
|
73
|
+
@staticmethod
|
74
|
+
def _lower_case_http_map(d: ta.Mapping[str, ta.Sequence[str]]) -> ta.Mapping[str, ta.Sequence[str]]:
|
75
|
+
o: ta.Dict[str, ta.List[str]] = {}
|
76
|
+
for k, vs in d.items():
|
77
|
+
o.setdefault(k.lower(), []).extend(check_not_isinstance(vs, str))
|
78
|
+
return o
|
69
79
|
|
70
|
-
|
71
|
-
return hashlib.sha256(_as_bytes(data)).hexdigest()
|
80
|
+
#
|
72
81
|
|
82
|
+
@staticmethod
|
83
|
+
def _as_bytes(data: ta.Union[str, bytes]) -> bytes:
|
84
|
+
return data if isinstance(data, bytes) else data.encode('utf-8')
|
73
85
|
|
74
|
-
|
75
|
-
|
86
|
+
@staticmethod
|
87
|
+
def _sha256(data: ta.Union[str, bytes]) -> str:
|
88
|
+
return hashlib.sha256(AwsSigner._as_bytes(data)).hexdigest()
|
76
89
|
|
90
|
+
@staticmethod
|
91
|
+
def _sha256_sign(key: bytes, msg: ta.Union[str, bytes]) -> bytes:
|
92
|
+
return hmac.new(key, AwsSigner._as_bytes(msg), hashlib.sha256).digest()
|
77
93
|
|
78
|
-
|
79
|
-
|
94
|
+
@staticmethod
|
95
|
+
def _sha256_sign_hex(key: bytes, msg: ta.Union[str, bytes]) -> str:
|
96
|
+
return hmac.new(key, AwsSigner._as_bytes(msg), hashlib.sha256).hexdigest()
|
80
97
|
|
98
|
+
_EMPTY_SHA256: str
|
81
99
|
|
82
|
-
|
100
|
+
#
|
83
101
|
|
84
|
-
|
102
|
+
_SIGNED_HEADERS_BLACKLIST = frozenset([
|
103
|
+
'authorization',
|
104
|
+
'expect',
|
105
|
+
'user-agent',
|
106
|
+
'x-amzn-trace-id',
|
107
|
+
])
|
85
108
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
109
|
+
def _validate_request(self, req: Request) -> None:
|
110
|
+
check_non_empty_str(req.method)
|
111
|
+
check_equal(req.method.upper(), req.method)
|
112
|
+
for k, vs in req.headers.items():
|
113
|
+
check_equal(k.strip(), k)
|
114
|
+
for v in vs:
|
115
|
+
check_equal(v.strip(), v)
|
91
116
|
|
92
117
|
|
93
|
-
|
94
|
-
o: dict[str, list[str]] = {}
|
95
|
-
for k, vs in d.items():
|
96
|
-
o.setdefault(k.lower(), []).extend(vs)
|
97
|
-
return o
|
118
|
+
AwsSigner._EMPTY_SHA256 = AwsSigner._sha256(b'') # noqa
|
98
119
|
|
99
120
|
|
100
|
-
|
101
|
-
def __init__(
|
102
|
-
self,
|
103
|
-
creds: Credentials,
|
104
|
-
region_name: str,
|
105
|
-
service_name: str,
|
106
|
-
) -> None:
|
107
|
-
super().__init__()
|
108
|
-
self._creds = creds
|
109
|
-
self._region_name = region_name
|
110
|
-
self._service_name = service_name
|
121
|
+
##
|
111
122
|
|
112
|
-
def _validate_request(self, req: Request) -> None:
|
113
|
-
check.non_empty_str(req.method)
|
114
|
-
check.equal(req.method.upper(), req.method)
|
115
|
-
for k, vs in req.headers.items():
|
116
|
-
check.equal(k.strip(), k)
|
117
|
-
for v in vs:
|
118
|
-
check.equal(v.strip(), v)
|
119
123
|
|
124
|
+
class V4AwsSigner(AwsSigner):
|
120
125
|
def sign(
|
121
126
|
self,
|
122
|
-
req: Request,
|
127
|
+
req: AwsSigner.Request,
|
123
128
|
*,
|
124
129
|
sign_payload: bool = False,
|
125
|
-
utcnow: datetime.datetime
|
126
|
-
) ->
|
130
|
+
utcnow: ta.Optional[datetime.datetime] = None,
|
131
|
+
) -> ta.Mapping[str, ta.Sequence[str]]:
|
127
132
|
self._validate_request(req)
|
128
133
|
|
129
134
|
#
|
130
135
|
|
131
136
|
if utcnow is None:
|
132
|
-
utcnow =
|
133
|
-
req_dt = utcnow.strftime(
|
137
|
+
utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
|
138
|
+
req_dt = utcnow.strftime(self.ISO8601)
|
134
139
|
|
135
140
|
#
|
136
141
|
|
@@ -140,18 +145,18 @@ class V4AwsSigner:
|
|
140
145
|
|
141
146
|
#
|
142
147
|
|
143
|
-
headers_to_sign = {
|
144
|
-
k: v
|
145
|
-
for k, v in _lower_case_http_map(req.headers).items()
|
146
|
-
if k not in _SIGNED_HEADERS_BLACKLIST
|
148
|
+
headers_to_sign: ta.Dict[str, ta.List[str]] = {
|
149
|
+
k: list(v)
|
150
|
+
for k, v in self._lower_case_http_map(req.headers).items()
|
151
|
+
if k not in self._SIGNED_HEADERS_BLACKLIST
|
147
152
|
}
|
148
153
|
|
149
154
|
if 'host' not in headers_to_sign:
|
150
|
-
headers_to_sign['host'] = [_host_from_url(req.url)]
|
155
|
+
headers_to_sign['host'] = [self._host_from_url(req.url)]
|
151
156
|
|
152
157
|
headers_to_sign['x-amz-date'] = [req_dt]
|
153
158
|
|
154
|
-
hashed_payload = _sha256(req.payload) if req.payload else _EMPTY_SHA256
|
159
|
+
hashed_payload = self._sha256(req.payload) if req.payload else self._EMPTY_SHA256
|
155
160
|
if sign_payload:
|
156
161
|
headers_to_sign['x-amz-content-sha256'] = [hashed_payload]
|
157
162
|
|
@@ -183,7 +188,7 @@ class V4AwsSigner:
|
|
183
188
|
'aws4_request',
|
184
189
|
]
|
185
190
|
scope = '/'.join(scope_parts)
|
186
|
-
hashed_canon_req = _sha256(canon_req)
|
191
|
+
hashed_canon_req = self._sha256(canon_req)
|
187
192
|
string_to_sign = '\n'.join([
|
188
193
|
algorithm,
|
189
194
|
req_dt,
|
@@ -194,11 +199,11 @@ class V4AwsSigner:
|
|
194
199
|
#
|
195
200
|
|
196
201
|
key = self._creds.secret_key
|
197
|
-
key_date = _sha256_sign(f'AWS4{key}'.encode('utf-8'), req_dt[:8]) # noqa
|
198
|
-
key_region = _sha256_sign(key_date, self._region_name)
|
199
|
-
key_service = _sha256_sign(key_region, self._service_name)
|
200
|
-
key_signing = _sha256_sign(key_service, 'aws4_request')
|
201
|
-
sig = _sha256_sign_hex(key_signing, string_to_sign)
|
202
|
+
key_date = self._sha256_sign(f'AWS4{key}'.encode('utf-8'), req_dt[:8]) # noqa
|
203
|
+
key_region = self._sha256_sign(key_date, self._region_name)
|
204
|
+
key_service = self._sha256_sign(key_region, self._service_name)
|
205
|
+
key_signing = self._sha256_sign(key_service, 'aws4_request')
|
206
|
+
sig = self._sha256_sign_hex(key_signing, string_to_sign)
|
202
207
|
|
203
208
|
#
|
204
209
|
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import collections.abc
|
4
|
+
import dataclasses as dc
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.cached import cached_nullary
|
8
|
+
from omlish.lite.reflect import get_optional_alias_arg
|
9
|
+
from omlish.lite.reflect import is_generic_alias
|
10
|
+
from omlish.lite.reflect import is_optional_alias
|
11
|
+
from omlish.lite.strings import camel_case
|
12
|
+
|
13
|
+
|
14
|
+
class AwsDataclass:
|
15
|
+
class Raw(dict):
|
16
|
+
pass
|
17
|
+
|
18
|
+
#
|
19
|
+
|
20
|
+
_aws_meta: ta.ClassVar[ta.Optional['AwsDataclassMeta']] = None
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def _get_aws_meta(cls) -> 'AwsDataclassMeta':
|
24
|
+
try:
|
25
|
+
return cls.__dict__['_aws_meta']
|
26
|
+
except KeyError:
|
27
|
+
pass
|
28
|
+
ret = cls._aws_meta = AwsDataclassMeta(cls)
|
29
|
+
return ret
|
30
|
+
|
31
|
+
#
|
32
|
+
|
33
|
+
def to_aws(self) -> ta.Mapping[str, ta.Any]:
|
34
|
+
return self._get_aws_meta().converters().d2a(self)
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def from_aws(cls, v: ta.Mapping[str, ta.Any]) -> 'AwsDataclass':
|
38
|
+
return cls._get_aws_meta().converters().a2d(v)
|
39
|
+
|
40
|
+
|
41
|
+
@dc.dataclass(frozen=True)
|
42
|
+
class AwsDataclassMeta:
|
43
|
+
cls: ta.Type['AwsDataclass']
|
44
|
+
|
45
|
+
#
|
46
|
+
|
47
|
+
class Field(ta.NamedTuple):
|
48
|
+
d_name: str
|
49
|
+
a_name: str
|
50
|
+
is_opt: bool
|
51
|
+
is_seq: bool
|
52
|
+
dc_cls: ta.Optional[ta.Type['AwsDataclass']]
|
53
|
+
|
54
|
+
@cached_nullary
|
55
|
+
def fields(self) -> ta.Sequence[Field]:
|
56
|
+
fs = []
|
57
|
+
for f in dc.fields(self.cls): # type: ignore # noqa
|
58
|
+
d_name = f.name
|
59
|
+
a_name = camel_case(d_name, lower=True)
|
60
|
+
|
61
|
+
is_opt = False
|
62
|
+
is_seq = False
|
63
|
+
dc_cls = None
|
64
|
+
|
65
|
+
c = f.type
|
66
|
+
if c is AwsDataclass.Raw:
|
67
|
+
continue
|
68
|
+
|
69
|
+
if is_optional_alias(c):
|
70
|
+
is_opt = True
|
71
|
+
c = get_optional_alias_arg(c)
|
72
|
+
|
73
|
+
if is_generic_alias(c) and ta.get_origin(c) is collections.abc.Sequence:
|
74
|
+
is_seq = True
|
75
|
+
[c] = ta.get_args(c)
|
76
|
+
|
77
|
+
if is_generic_alias(c):
|
78
|
+
raise TypeError(c)
|
79
|
+
|
80
|
+
if isinstance(c, type) and issubclass(c, AwsDataclass):
|
81
|
+
dc_cls = c
|
82
|
+
|
83
|
+
fs.append(AwsDataclassMeta.Field(
|
84
|
+
d_name=d_name,
|
85
|
+
a_name=a_name,
|
86
|
+
is_opt=is_opt,
|
87
|
+
is_seq=is_seq,
|
88
|
+
dc_cls=dc_cls,
|
89
|
+
))
|
90
|
+
|
91
|
+
return fs
|
92
|
+
|
93
|
+
#
|
94
|
+
|
95
|
+
class Converters(ta.NamedTuple):
|
96
|
+
d2a: ta.Callable
|
97
|
+
a2d: ta.Callable
|
98
|
+
|
99
|
+
@cached_nullary
|
100
|
+
def converters(self) -> Converters:
|
101
|
+
for df in dc.fields(self.cls): # type: ignore # noqa
|
102
|
+
c = df.type
|
103
|
+
|
104
|
+
if is_optional_alias(c):
|
105
|
+
c = get_optional_alias_arg(c)
|
106
|
+
|
107
|
+
if c is AwsDataclass.Raw:
|
108
|
+
rf = df.name
|
109
|
+
break
|
110
|
+
|
111
|
+
else:
|
112
|
+
rf = None
|
113
|
+
|
114
|
+
fs = [
|
115
|
+
(f, f.dc_cls._get_aws_meta().converters() if f.dc_cls is not None else None) # noqa
|
116
|
+
for f in self.fields()
|
117
|
+
]
|
118
|
+
|
119
|
+
def d2a(o):
|
120
|
+
dct = {}
|
121
|
+
for f, cs in fs:
|
122
|
+
x = getattr(o, f.d_name)
|
123
|
+
if x is None:
|
124
|
+
continue
|
125
|
+
if cs is not None:
|
126
|
+
if f.is_seq:
|
127
|
+
x = list(map(cs.d2a, x))
|
128
|
+
else:
|
129
|
+
x = cs.d2a(x)
|
130
|
+
dct[f.a_name] = x
|
131
|
+
return dct
|
132
|
+
|
133
|
+
def a2d(v):
|
134
|
+
dct = {}
|
135
|
+
for f, cs in fs:
|
136
|
+
x = v.get(f.a_name)
|
137
|
+
if x is None:
|
138
|
+
continue
|
139
|
+
if cs is not None:
|
140
|
+
if f.is_seq:
|
141
|
+
x = list(map(cs.a2d, x))
|
142
|
+
else:
|
143
|
+
x = cs.a2d(x)
|
144
|
+
dct[f.d_name] = x
|
145
|
+
if rf is not None:
|
146
|
+
dct[rf] = self.cls.Raw(v)
|
147
|
+
return self.cls(**dct)
|
148
|
+
|
149
|
+
return AwsDataclassMeta.Converters(d2a, a2d)
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import dataclasses as dc
|
4
|
+
import json
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.check import check_isinstance
|
8
|
+
from omlish.lite.io import DelimitingBuffer
|
9
|
+
from omlish.lite.logs import log
|
10
|
+
|
11
|
+
|
12
|
+
@dc.dataclass(frozen=True)
|
13
|
+
class JournalctlMessage:
|
14
|
+
raw: bytes
|
15
|
+
dct: ta.Optional[ta.Mapping[str, ta.Any]] = None
|
16
|
+
cursor: ta.Optional[str] = None
|
17
|
+
ts_us: ta.Optional[int] = None # microseconds UTC
|
18
|
+
|
19
|
+
|
20
|
+
class JournalctlMessageBuilder:
|
21
|
+
def __init__(self) -> None:
|
22
|
+
super().__init__()
|
23
|
+
|
24
|
+
self._buf = DelimitingBuffer(b'\n')
|
25
|
+
|
26
|
+
_cursor_field = '__CURSOR'
|
27
|
+
_timestamp_field = '_SOURCE_REALTIME_TIMESTAMP'
|
28
|
+
|
29
|
+
def _make_message(self, raw: bytes) -> JournalctlMessage:
|
30
|
+
dct = None
|
31
|
+
cursor = None
|
32
|
+
ts = None
|
33
|
+
|
34
|
+
try:
|
35
|
+
dct = json.loads(raw.decode('utf-8', 'replace'))
|
36
|
+
except Exception: # noqa
|
37
|
+
log.exception('Failed to parse raw message: %r', raw)
|
38
|
+
|
39
|
+
else:
|
40
|
+
cursor = dct.get(self._cursor_field)
|
41
|
+
|
42
|
+
if tsv := dct.get(self._timestamp_field):
|
43
|
+
if isinstance(tsv, str):
|
44
|
+
try:
|
45
|
+
ts = int(tsv)
|
46
|
+
except ValueError:
|
47
|
+
try:
|
48
|
+
ts = int(float(tsv))
|
49
|
+
except ValueError:
|
50
|
+
log.exception('Failed to parse timestamp: %r', tsv)
|
51
|
+
elif isinstance(tsv, (int, float)):
|
52
|
+
ts = int(tsv)
|
53
|
+
else:
|
54
|
+
log.exception('Invalid timestamp: %r', tsv)
|
55
|
+
|
56
|
+
return JournalctlMessage(
|
57
|
+
raw=raw,
|
58
|
+
dct=dct,
|
59
|
+
cursor=cursor,
|
60
|
+
ts_us=ts,
|
61
|
+
)
|
62
|
+
|
63
|
+
def feed(self, data: bytes) -> ta.Sequence[JournalctlMessage]:
|
64
|
+
ret: ta.List[JournalctlMessage] = []
|
65
|
+
for line in self._buf.feed(data):
|
66
|
+
ret.append(self._make_message(check_isinstance(line, bytes))) # type: ignore
|
67
|
+
return ret
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# @omlish-lite
|
2
|
+
# ruff: noqa: UP007
|
3
|
+
"""
|
4
|
+
https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html :
|
5
|
+
- The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26
|
6
|
+
bytes for each log event.
|
7
|
+
- None of the log events in the batch can be more than 2 hours in the future.
|
8
|
+
- None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from
|
9
|
+
earlier than the retention period of the log group.
|
10
|
+
- The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the
|
11
|
+
event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In AWS Tools for PowerShell
|
12
|
+
and the AWS SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example,
|
13
|
+
2017-09-15T13:45:30.)
|
14
|
+
- A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
|
15
|
+
- Each log event can be no larger than 256 KB.
|
16
|
+
- The maximum number of log events in a batch is 10,000.
|
17
|
+
"""
|
18
|
+
import dataclasses as dc
|
19
|
+
import json
|
20
|
+
import typing as ta
|
21
|
+
|
22
|
+
from omlish.lite.check import check_non_empty_str
|
23
|
+
from omlish.lite.check import check_single
|
24
|
+
|
25
|
+
from .auth import AwsSigner
|
26
|
+
from .auth import V4AwsSigner
|
27
|
+
from .dataclasses import AwsDataclass
|
28
|
+
|
29
|
+
|
30
|
+
##
|
31
|
+
|
32
|
+
|
33
|
+
@dc.dataclass(frozen=True)
|
34
|
+
class AwsLogEvent(AwsDataclass):
|
35
|
+
message: str
|
36
|
+
timestamp: int # milliseconds UTC
|
37
|
+
|
38
|
+
|
39
|
+
@dc.dataclass(frozen=True)
|
40
|
+
class AwsPutLogEventsRequest(AwsDataclass):
|
41
|
+
log_group_name: str
|
42
|
+
log_stream_name: str
|
43
|
+
log_events: ta.Sequence[AwsLogEvent]
|
44
|
+
sequence_token: ta.Optional[str] = None
|
45
|
+
|
46
|
+
|
47
|
+
@dc.dataclass(frozen=True)
|
48
|
+
class AwsRejectedLogEventsInfo(AwsDataclass):
|
49
|
+
expired_log_event_end_index: ta.Optional[int] = None
|
50
|
+
too_new_log_event_start_index: ta.Optional[int] = None
|
51
|
+
too_old_log_event_end_index: ta.Optional[int] = None
|
52
|
+
|
53
|
+
|
54
|
+
@dc.dataclass(frozen=True)
|
55
|
+
class AwsPutLogEventsResponse(AwsDataclass):
|
56
|
+
next_sequence_token: ta.Optional[str] = None
|
57
|
+
rejected_log_events_info: ta.Optional[AwsRejectedLogEventsInfo] = None
|
58
|
+
|
59
|
+
raw: ta.Optional[AwsDataclass.Raw] = None
|
60
|
+
|
61
|
+
|
62
|
+
##
|
63
|
+
|
64
|
+
|
65
|
+
class AwsLogMessagePoster:
|
66
|
+
"""
|
67
|
+
TODO:
|
68
|
+
- max_items
|
69
|
+
- max_bytes - manually build body
|
70
|
+
- flush_interval
|
71
|
+
- !! sort by timestamp
|
72
|
+
"""
|
73
|
+
|
74
|
+
DEFAULT_URL = 'https://logs.{region_name}.amazonaws.com/' # noqa
|
75
|
+
|
76
|
+
DEFAULT_SERVICE_NAME = 'logs'
|
77
|
+
|
78
|
+
DEFAULT_TARGET = 'Logs_20140328.PutLogEvents'
|
79
|
+
DEFAULT_CONTENT_TYPE = 'application/x-amz-json-1.1'
|
80
|
+
|
81
|
+
DEFAULT_HEADERS: ta.Mapping[str, str] = {
|
82
|
+
'X-Amz-Target': DEFAULT_TARGET,
|
83
|
+
'Content-Type': DEFAULT_CONTENT_TYPE,
|
84
|
+
}
|
85
|
+
|
86
|
+
def __init__(
|
87
|
+
self,
|
88
|
+
log_group_name: str,
|
89
|
+
log_stream_name: str,
|
90
|
+
region_name: str,
|
91
|
+
credentials: AwsSigner.Credentials,
|
92
|
+
|
93
|
+
url: ta.Optional[str] = None,
|
94
|
+
service_name: str = DEFAULT_SERVICE_NAME,
|
95
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
96
|
+
extra_headers: ta.Optional[ta.Mapping[str, str]] = None,
|
97
|
+
) -> None:
|
98
|
+
super().__init__()
|
99
|
+
|
100
|
+
self._log_group_name = check_non_empty_str(log_group_name)
|
101
|
+
self._log_stream_name = check_non_empty_str(log_stream_name)
|
102
|
+
|
103
|
+
if url is None:
|
104
|
+
url = self.DEFAULT_URL.format(region_name=region_name)
|
105
|
+
self._url = url
|
106
|
+
|
107
|
+
if headers is None:
|
108
|
+
headers = self.DEFAULT_HEADERS
|
109
|
+
if extra_headers is not None:
|
110
|
+
headers = {**headers, **extra_headers}
|
111
|
+
self._headers = {k: [v] for k, v in headers.items()}
|
112
|
+
|
113
|
+
self._signer = V4AwsSigner(
|
114
|
+
credentials,
|
115
|
+
region_name,
|
116
|
+
service_name,
|
117
|
+
)
|
118
|
+
|
119
|
+
#
|
120
|
+
|
121
|
+
@dc.dataclass(frozen=True)
|
122
|
+
class Message:
|
123
|
+
message: str
|
124
|
+
ts_ms: int # milliseconds UTC
|
125
|
+
|
126
|
+
@dc.dataclass(frozen=True)
|
127
|
+
class Post:
|
128
|
+
url: str
|
129
|
+
headers: ta.Mapping[str, str]
|
130
|
+
data: bytes
|
131
|
+
|
132
|
+
def feed(self, messages: ta.Sequence[Message]) -> ta.Sequence[Post]:
|
133
|
+
if not messages:
|
134
|
+
return []
|
135
|
+
|
136
|
+
payload = AwsPutLogEventsRequest(
|
137
|
+
log_group_name=self._log_group_name,
|
138
|
+
log_stream_name=self._log_stream_name,
|
139
|
+
log_events=[
|
140
|
+
AwsLogEvent(
|
141
|
+
message=m.message,
|
142
|
+
timestamp=m.ts_ms,
|
143
|
+
)
|
144
|
+
for m in messages
|
145
|
+
],
|
146
|
+
)
|
147
|
+
|
148
|
+
body = json.dumps(
|
149
|
+
payload.to_aws(),
|
150
|
+
indent=None,
|
151
|
+
separators=(',', ':'),
|
152
|
+
).encode('utf-8')
|
153
|
+
|
154
|
+
sig_req = V4AwsSigner.Request(
|
155
|
+
method='POST',
|
156
|
+
url=self._url,
|
157
|
+
headers=self._headers,
|
158
|
+
payload=body,
|
159
|
+
)
|
160
|
+
|
161
|
+
sig_headers = self._signer.sign(
|
162
|
+
sig_req,
|
163
|
+
sign_payload=False,
|
164
|
+
)
|
165
|
+
sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
|
166
|
+
|
167
|
+
post = AwsLogMessagePoster.Post(
|
168
|
+
url=self._url,
|
169
|
+
headers={k: check_single(v) for k, v in sig_req.headers.items()},
|
170
|
+
data=sig_req.payload,
|
171
|
+
)
|
172
|
+
|
173
|
+
return [post]
|
ominfra/deploy/_executor.py
CHANGED
@@ -168,6 +168,23 @@ def check_state(v: bool, msg: str = 'Illegal state') -> None:
|
|
168
168
|
raise ValueError(msg)
|
169
169
|
|
170
170
|
|
171
|
+
def check_equal(l: T, r: T) -> T:
|
172
|
+
if l != r:
|
173
|
+
raise ValueError(l, r)
|
174
|
+
return l
|
175
|
+
|
176
|
+
|
177
|
+
def check_not_equal(l: T, r: T) -> T:
|
178
|
+
if l == r:
|
179
|
+
raise ValueError(l, r)
|
180
|
+
return l
|
181
|
+
|
182
|
+
|
183
|
+
def check_single(vs: ta.Iterable[T]) -> T:
|
184
|
+
[v] = vs
|
185
|
+
return v
|
186
|
+
|
187
|
+
|
171
188
|
########################################
|
172
189
|
# ../../../../omlish/lite/json.py
|
173
190
|
|