moat-kv 0.70.23__py3-none-any.whl → 0.70.24__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.
- build/lib/docs/source/conf.py +201 -0
- build/lib/examples/pathify.py +45 -0
- build/lib/moat/kv/__init__.py +19 -0
- build/lib/moat/kv/_cfg.yaml +97 -0
- build/lib/moat/kv/_main.py +91 -0
- build/lib/moat/kv/actor/__init__.py +98 -0
- build/lib/moat/kv/actor/deletor.py +139 -0
- build/lib/moat/kv/auth/__init__.py +444 -0
- build/lib/moat/kv/auth/_test.py +166 -0
- build/lib/moat/kv/auth/password.py +234 -0
- build/lib/moat/kv/auth/root.py +58 -0
- build/lib/moat/kv/backend/__init__.py +67 -0
- build/lib/moat/kv/backend/mqtt.py +74 -0
- build/lib/moat/kv/backend/serf.py +45 -0
- build/lib/moat/kv/client.py +1025 -0
- build/lib/moat/kv/code.py +236 -0
- build/lib/moat/kv/codec.py +11 -0
- build/lib/moat/kv/command/__init__.py +1 -0
- build/lib/moat/kv/command/acl.py +180 -0
- build/lib/moat/kv/command/auth.py +261 -0
- build/lib/moat/kv/command/code.py +293 -0
- build/lib/moat/kv/command/codec.py +186 -0
- build/lib/moat/kv/command/data.py +265 -0
- build/lib/moat/kv/command/dump/__init__.py +143 -0
- build/lib/moat/kv/command/error.py +149 -0
- build/lib/moat/kv/command/internal.py +248 -0
- build/lib/moat/kv/command/job.py +433 -0
- build/lib/moat/kv/command/log.py +53 -0
- build/lib/moat/kv/command/server.py +114 -0
- build/lib/moat/kv/command/type.py +201 -0
- build/lib/moat/kv/config.py +46 -0
- build/lib/moat/kv/data.py +216 -0
- build/lib/moat/kv/errors.py +561 -0
- build/lib/moat/kv/exceptions.py +126 -0
- build/lib/moat/kv/mock/__init__.py +101 -0
- build/lib/moat/kv/mock/mqtt.py +159 -0
- build/lib/moat/kv/mock/serf.py +250 -0
- build/lib/moat/kv/mock/tracer.py +63 -0
- build/lib/moat/kv/model.py +1069 -0
- build/lib/moat/kv/obj/__init__.py +646 -0
- build/lib/moat/kv/obj/command.py +241 -0
- build/lib/moat/kv/runner.py +1347 -0
- build/lib/moat/kv/server.py +2809 -0
- build/lib/moat/kv/types.py +513 -0
- debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
- debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +97 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +74 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/serf.py +45 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/serf.py +250 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
- docs/source/conf.py +201 -0
- examples/pathify.py +45 -0
- moat/kv/actor/__init__.py +98 -0
- moat/kv/actor/deletor.py +139 -0
- moat/kv/auth/__init__.py +444 -0
- moat/kv/auth/_test.py +166 -0
- moat/kv/auth/password.py +234 -0
- moat/kv/auth/root.py +58 -0
- moat/kv/backend/__init__.py +67 -0
- moat/kv/backend/mqtt.py +74 -0
- moat/kv/backend/serf.py +45 -0
- moat/kv/command/__init__.py +1 -0
- moat/kv/command/acl.py +180 -0
- moat/kv/command/auth.py +261 -0
- moat/kv/command/code.py +293 -0
- moat/kv/command/codec.py +186 -0
- moat/kv/command/data.py +265 -0
- moat/kv/command/dump/__init__.py +143 -0
- moat/kv/command/error.py +149 -0
- moat/kv/command/internal.py +248 -0
- moat/kv/command/job.py +433 -0
- moat/kv/command/log.py +53 -0
- moat/kv/command/server.py +114 -0
- moat/kv/command/type.py +201 -0
- moat/kv/mock/__init__.py +101 -0
- moat/kv/mock/mqtt.py +159 -0
- moat/kv/mock/serf.py +250 -0
- moat/kv/mock/tracer.py +63 -0
- moat/kv/obj/__init__.py +646 -0
- moat/kv/obj/command.py +241 -0
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/METADATA +2 -2
- moat_kv-0.70.24.dist-info/RECORD +137 -0
- moat_kv-0.70.24.dist-info/top_level.txt +9 -0
- moat_kv-0.70.23.dist-info/RECORD +0 -19
- moat_kv-0.70.23.dist-info/top_level.txt +0 -1
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/WHEEL +0 -0
- {moat_kv-0.70.23.dist-info → moat_kv-0.70.24.dist-info}/licenses/LICENSE.txt +0 -0
moat/kv/command/job.py
ADDED
@@ -0,0 +1,433 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import sys
|
5
|
+
import time
|
6
|
+
from functools import partial
|
7
|
+
|
8
|
+
import anyio
|
9
|
+
import asyncclick as click
|
10
|
+
from moat.util import P, Path, attr_args, attrdict, process_args, yprint
|
11
|
+
|
12
|
+
from moat.kv.code import CodeRoot
|
13
|
+
from moat.kv.data import add_dates, data_get
|
14
|
+
from moat.kv.runner import AllRunnerRoot, AnyRunnerRoot, SingleRunnerRoot
|
15
|
+
|
16
|
+
|
17
|
+
@click.group() # pylint: disable=undefined-variable
|
18
|
+
@click.option("-n", "--node", help="node to run this code on. Empty: any one node, '-': all nodes")
|
19
|
+
@click.option("-g", "--group", help="group to run this code on. Empty: default")
|
20
|
+
@click.pass_context
|
21
|
+
async def cli(ctx, node, group):
|
22
|
+
"""Run code stored in MoaT-KV.
|
23
|
+
|
24
|
+
\b
|
25
|
+
The option '-n' is somewhat special:
|
26
|
+
-n - Jobs for all hosts
|
27
|
+
-n XXX Jobs for host XXX
|
28
|
+
(no -n) Jobs for any host
|
29
|
+
|
30
|
+
The default group is 'default'.
|
31
|
+
"""
|
32
|
+
obj = ctx.obj
|
33
|
+
if group is None:
|
34
|
+
group = "default"
|
35
|
+
if group == "-":
|
36
|
+
if node is not None:
|
37
|
+
raise click.UsageError("'-g -' doesn't make sense with '-n'")
|
38
|
+
if ctx.invoked_subcommand != "info":
|
39
|
+
raise click.UsageError("'-g -' only makes sense with the 'info' command")
|
40
|
+
obj.runner_root = SingleRunnerRoot
|
41
|
+
subpath = (None,)
|
42
|
+
elif not node:
|
43
|
+
obj.runner_root = AnyRunnerRoot
|
44
|
+
subpath = (group,)
|
45
|
+
elif node == "-":
|
46
|
+
obj.runner_root = AllRunnerRoot
|
47
|
+
subpath = (group,)
|
48
|
+
else:
|
49
|
+
obj.runner_root = SingleRunnerRoot
|
50
|
+
subpath = (node, group)
|
51
|
+
|
52
|
+
cfg = obj.cfg["kv"]["runner"]
|
53
|
+
obj.subpath = Path(cfg["sub"][obj.runner_root.SUB]) + subpath
|
54
|
+
obj.path = cfg["prefix"] + obj.subpath
|
55
|
+
obj.statepath = cfg["state"] + obj.subpath
|
56
|
+
|
57
|
+
|
58
|
+
@cli.group("at", short_help="path of the job to operate on", invoke_without_command=True)
|
59
|
+
@click.argument("path", nargs=1, type=P)
|
60
|
+
@click.pass_context
|
61
|
+
async def at_cli(ctx, path):
|
62
|
+
"""
|
63
|
+
Add, list, modify, delete jobs at/under this path.
|
64
|
+
"""
|
65
|
+
obj = ctx.obj
|
66
|
+
obj.jobpath = path
|
67
|
+
|
68
|
+
if ctx.invoked_subcommand is None:
|
69
|
+
res = await obj.client.get(obj.path + path, nchain=obj.meta)
|
70
|
+
yprint(
|
71
|
+
res if obj.meta else res.value if "value" in res else None,
|
72
|
+
stream=obj.stdout,
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
@cli.command("info")
|
77
|
+
@click.pass_obj
|
78
|
+
async def info_(obj):
|
79
|
+
"""
|
80
|
+
List available groups for the node in question.
|
81
|
+
|
82
|
+
\b
|
83
|
+
Options (between 'job' and 'info')
|
84
|
+
(none) list groups with jobs for any host
|
85
|
+
-n - list groups with jobs for all hosts
|
86
|
+
-g - list hosts that have specific jobs
|
87
|
+
-n XXX list groups with jobs for a specific host
|
88
|
+
"""
|
89
|
+
path = obj.path[:-1]
|
90
|
+
async for r in obj.client.get_tree(path=path, min_depth=1, max_depth=1, empty=True):
|
91
|
+
print(r.path[-1], file=obj.stdout)
|
92
|
+
|
93
|
+
|
94
|
+
@at_cli.command("--help", hidden=True)
|
95
|
+
@click.pass_context
|
96
|
+
def help_(ctx): # pylint:disable=unused-variable # oh boy
|
97
|
+
print(at_cli.get_help(ctx))
|
98
|
+
|
99
|
+
|
100
|
+
@at_cli.command("path")
|
101
|
+
@click.pass_obj
|
102
|
+
async def path__(obj):
|
103
|
+
"""
|
104
|
+
Emit the full path leading to the specified runner object.
|
105
|
+
|
106
|
+
Useful for copying or for state monitoring.
|
107
|
+
|
108
|
+
NEVER directly write to the state object. It's controlled by the
|
109
|
+
runner. You'll confuse it if you do that.
|
110
|
+
|
111
|
+
Updating the control object will cancel any running code.
|
112
|
+
"""
|
113
|
+
path = obj.jobpath
|
114
|
+
res = dict(command=obj.path + path, state=obj.statepath + path)
|
115
|
+
yprint(res, stream=obj.stdout)
|
116
|
+
|
117
|
+
|
118
|
+
@cli.command("run")
|
119
|
+
@click.option(
|
120
|
+
"-n",
|
121
|
+
"--nodes",
|
122
|
+
type=int,
|
123
|
+
default=0,
|
124
|
+
help="Size of the group (not for single-node runners)",
|
125
|
+
)
|
126
|
+
@click.pass_obj
|
127
|
+
async def run(obj, nodes):
|
128
|
+
"""
|
129
|
+
Run code that needs to run.
|
130
|
+
|
131
|
+
This does not return.
|
132
|
+
"""
|
133
|
+
from moat.util import as_service
|
134
|
+
|
135
|
+
if obj.subpath[-1] == "-":
|
136
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
137
|
+
if nodes and obj.runner_root is SingleRunnerRoot:
|
138
|
+
raise click.UsageError("A single-site runner doesn't have a size.")
|
139
|
+
|
140
|
+
async with as_service(obj) as evt:
|
141
|
+
c = obj.client
|
142
|
+
cr = await CodeRoot.as_handler(c)
|
143
|
+
await obj.runner_root.as_handler(
|
144
|
+
c,
|
145
|
+
subpath=obj.subpath,
|
146
|
+
code=cr,
|
147
|
+
**({"nodes": nodes} if nodes else {}),
|
148
|
+
)
|
149
|
+
evt.set()
|
150
|
+
await anyio.sleep_forever()
|
151
|
+
|
152
|
+
|
153
|
+
async def _state_fix(obj, state, state_only, path, r):
|
154
|
+
try:
|
155
|
+
val = r.value
|
156
|
+
except AttributeError:
|
157
|
+
return
|
158
|
+
if state:
|
159
|
+
rs = await obj.client._request(
|
160
|
+
action="get_value",
|
161
|
+
path=state + r.path,
|
162
|
+
iter=False,
|
163
|
+
nchain=obj.meta,
|
164
|
+
)
|
165
|
+
if state_only:
|
166
|
+
r.value = rs
|
167
|
+
else:
|
168
|
+
if obj.meta:
|
169
|
+
val["state"] = rs
|
170
|
+
elif "value" in rs:
|
171
|
+
val["state"] = rs.value
|
172
|
+
if "value" in rs:
|
173
|
+
add_dates(rs.value)
|
174
|
+
if not state_only:
|
175
|
+
if path:
|
176
|
+
r.path = path + r.path
|
177
|
+
add_dates(val)
|
178
|
+
|
179
|
+
return r
|
180
|
+
|
181
|
+
|
182
|
+
@at_cli.command("list")
|
183
|
+
@click.option("-s", "--state", is_flag=True, help="Add state data")
|
184
|
+
@click.option("-S", "--state-only", is_flag=True, help="Only output state data")
|
185
|
+
@click.option("-t", "--table", is_flag=True, help="one-line output")
|
186
|
+
@click.option(
|
187
|
+
"-d",
|
188
|
+
"--as-dict",
|
189
|
+
default=None,
|
190
|
+
help="Structure as dictionary. The argument is the key to use "
|
191
|
+
"for values. Default: return as list",
|
192
|
+
)
|
193
|
+
@click.pass_obj
|
194
|
+
async def list_(obj, state, state_only, table, as_dict):
|
195
|
+
"""List run entries."""
|
196
|
+
if table and state:
|
197
|
+
raise click.UsageError("'--table' and '--state' are mutually exclusive")
|
198
|
+
|
199
|
+
path = obj.jobpath
|
200
|
+
|
201
|
+
if state or state_only or table:
|
202
|
+
state = obj.statepath + path
|
203
|
+
|
204
|
+
if table:
|
205
|
+
from moat.kv.errors import ErrorRoot
|
206
|
+
|
207
|
+
err = await ErrorRoot.as_handler(obj.client)
|
208
|
+
|
209
|
+
async for r in obj.client.get_tree(obj.path + path):
|
210
|
+
p = path + r.path
|
211
|
+
s = await obj.client.get(state + r.path)
|
212
|
+
if "value" not in s:
|
213
|
+
st = "-never-"
|
214
|
+
elif s.value.started > s.value.stopped:
|
215
|
+
st = s.value.node
|
216
|
+
else:
|
217
|
+
try:
|
218
|
+
e = await err.get_error_record("run", obj.path + p, create=False)
|
219
|
+
except KeyError:
|
220
|
+
st = "-stopped-"
|
221
|
+
else:
|
222
|
+
if e is None or e.resolved:
|
223
|
+
st = "-stopped-"
|
224
|
+
else:
|
225
|
+
st = " | ".join(
|
226
|
+
"%s %s"
|
227
|
+
% (
|
228
|
+
Path.build(e.subpath)
|
229
|
+
if e._path[-2] == ee._path[-1]
|
230
|
+
else Path.build(ee.subpath),
|
231
|
+
getattr(ee, "message", None)
|
232
|
+
or getattr(ee, "comment", None)
|
233
|
+
or "-",
|
234
|
+
)
|
235
|
+
for ee in e
|
236
|
+
)
|
237
|
+
print(p, r.value.code, st, file=obj.stdout)
|
238
|
+
|
239
|
+
else:
|
240
|
+
await data_get(
|
241
|
+
obj,
|
242
|
+
obj.path + path,
|
243
|
+
as_dict=as_dict,
|
244
|
+
item_mangle=partial(_state_fix, obj, state, state_only, None if as_dict else path),
|
245
|
+
)
|
246
|
+
|
247
|
+
|
248
|
+
@at_cli.command("state")
|
249
|
+
@click.option("-r", "--result", is_flag=True, help="Just print the actual result.")
|
250
|
+
@click.pass_obj
|
251
|
+
async def state_(obj, result):
|
252
|
+
"""Get the status of a runner entry."""
|
253
|
+
path = obj.jobpath
|
254
|
+
|
255
|
+
if obj.subpath[-1] == "-":
|
256
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
257
|
+
if result and obj.meta:
|
258
|
+
raise click.UsageError("You can't use '-v' and '-r' at the same time.")
|
259
|
+
if not len(path):
|
260
|
+
raise click.UsageError("You need a non-empty path.")
|
261
|
+
path = obj.statepath + obj.jobpath
|
262
|
+
|
263
|
+
res = await obj.client.get(path, nchain=obj.meta)
|
264
|
+
if "value" not in res:
|
265
|
+
if obj.debug:
|
266
|
+
print("Not found (yet?)", file=sys.stderr)
|
267
|
+
sys.exit(1)
|
268
|
+
|
269
|
+
add_dates(res.value)
|
270
|
+
if not obj.meta:
|
271
|
+
res = res.value
|
272
|
+
yprint(res, stream=obj.stdout)
|
273
|
+
|
274
|
+
|
275
|
+
@at_cli.command()
|
276
|
+
@click.option("-s", "--state", is_flag=True, help="Add state data")
|
277
|
+
@click.pass_obj
|
278
|
+
async def get(obj, state):
|
279
|
+
"""Read a runner entry"""
|
280
|
+
path = obj.jobpath
|
281
|
+
if obj.subpath[-1] == "-":
|
282
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
283
|
+
if not path:
|
284
|
+
raise click.UsageError("You need a non-empty path.")
|
285
|
+
|
286
|
+
res = await obj.client._request(
|
287
|
+
action="get_value",
|
288
|
+
path=obj.path + path,
|
289
|
+
iter=False,
|
290
|
+
nchain=obj.meta,
|
291
|
+
)
|
292
|
+
if "value" not in res:
|
293
|
+
print("Not found.", file=sys.stderr)
|
294
|
+
return
|
295
|
+
res.path = path
|
296
|
+
if state:
|
297
|
+
state = obj.statepath
|
298
|
+
await _state_fix(obj, state, False, path, res)
|
299
|
+
if not obj.meta:
|
300
|
+
res = res.value
|
301
|
+
|
302
|
+
yprint(res, stream=obj.stdout)
|
303
|
+
|
304
|
+
|
305
|
+
@at_cli.command()
|
306
|
+
@click.option("-f", "--force", is_flag=True, help="Force deletion even if messy")
|
307
|
+
@click.pass_obj
|
308
|
+
async def delete(obj, force):
|
309
|
+
"""Remove a runner entry"""
|
310
|
+
path = obj.jobpath
|
311
|
+
|
312
|
+
if obj.subpath[-1] == "-":
|
313
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
314
|
+
if not path:
|
315
|
+
raise click.UsageError("You need a non-empty path.")
|
316
|
+
|
317
|
+
res = await obj.client.get(obj.path + path, nchain=3)
|
318
|
+
if "value" not in res:
|
319
|
+
res.info = "Does not exist."
|
320
|
+
else:
|
321
|
+
val = res.value
|
322
|
+
if "target" not in val:
|
323
|
+
val.target = None
|
324
|
+
if val.target is not None:
|
325
|
+
val.target = None
|
326
|
+
res = await obj.client.set(obj.path + path, value=val, nchain=3, chain=res.chain)
|
327
|
+
if not force:
|
328
|
+
res.info = "'target' was set: cleared but not deleted."
|
329
|
+
if force or val.target is None:
|
330
|
+
sres = await obj.client.get(obj.statepath + path, nchain=3)
|
331
|
+
if not force and "value" in sres and sres.value.stopped < sres.value.started:
|
332
|
+
res.info = "Still running, not deleted."
|
333
|
+
else:
|
334
|
+
sres = await obj.client.delete(obj.statepath + path, chain=sres.chain)
|
335
|
+
res = await obj.client.delete(obj.path + path, chain=res.chain)
|
336
|
+
if "value" in res and res.value.stopped < res.value.started:
|
337
|
+
res.info = "Deleted (unclean!)."
|
338
|
+
else:
|
339
|
+
res.info = "Deleted."
|
340
|
+
|
341
|
+
if obj.meta:
|
342
|
+
yprint(res, stream=obj.stdout)
|
343
|
+
elif obj.debug:
|
344
|
+
print(res.info)
|
345
|
+
|
346
|
+
|
347
|
+
@at_cli.command("set")
|
348
|
+
@click.option("-c", "--code", help="Path to the code that should run.")
|
349
|
+
@click.option("-C", "--copy", help="Use this entry as a template.")
|
350
|
+
@click.option("-t", "--time", "tm", help="time the code should next run at. '-':not")
|
351
|
+
@click.option("-r", "--repeat", type=int, help="Seconds the code should re-run after")
|
352
|
+
@click.option("-k", "--ok", type=float, help="Code is OK if it ran this many seconds")
|
353
|
+
@click.option("-b", "--backoff", type=float, help="Back-off factor. Default: 1.4")
|
354
|
+
@click.option("-d", "--delay", type=int, help="Seconds the code should retry after (w/ backoff)")
|
355
|
+
@click.option("-i", "--info", help="Short human-readable information")
|
356
|
+
@attr_args
|
357
|
+
@click.pass_obj
|
358
|
+
async def set_(obj, code, tm, info, ok, repeat, delay, backoff, copy, **kw):
|
359
|
+
"""Add or modify a runner.
|
360
|
+
|
361
|
+
Code typically requires some input parameters.
|
362
|
+
|
363
|
+
You should use '-v NAME VALUE' for string values, '-p NAME VALUE' for
|
364
|
+
paths, and '-e NAME VALUE' for other data. '-e NAME -' deletes an item.
|
365
|
+
"""
|
366
|
+
path = obj.jobpath
|
367
|
+
|
368
|
+
if obj.subpath[-1] == "-":
|
369
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
370
|
+
|
371
|
+
if code is not None:
|
372
|
+
code = P(code)
|
373
|
+
if copy:
|
374
|
+
copy = P(copy)
|
375
|
+
path = obj.path + P(path)
|
376
|
+
|
377
|
+
res = await obj.client._request(action="get_value", path=copy or path, iter=False, nchain=3)
|
378
|
+
if "value" not in res:
|
379
|
+
if copy:
|
380
|
+
raise click.UsageError("--copy: use the complete path to an existing entry")
|
381
|
+
elif code is None:
|
382
|
+
raise click.UsageError("New entry, need code")
|
383
|
+
res = {}
|
384
|
+
chain = None
|
385
|
+
else:
|
386
|
+
chain = None if copy else res["chain"]
|
387
|
+
res = res["value"]
|
388
|
+
if copy and "code" not in res:
|
389
|
+
raise click.UsageError("'--copy' needs a runner entry")
|
390
|
+
|
391
|
+
vl = attrdict(**res.setdefault("data", {}))
|
392
|
+
vl = process_args(vl, **kw)
|
393
|
+
res["data"] = vl
|
394
|
+
|
395
|
+
if code is not None:
|
396
|
+
res["code"] = code
|
397
|
+
if ok is not None:
|
398
|
+
res["ok_after"] = ok
|
399
|
+
if info is not None:
|
400
|
+
res["info"] = info
|
401
|
+
if backoff is not None:
|
402
|
+
res["backoff"] = backoff
|
403
|
+
if delay is not None:
|
404
|
+
res["delay"] = delay
|
405
|
+
if repeat is not None:
|
406
|
+
res["repeat"] = repeat
|
407
|
+
if tm is not None:
|
408
|
+
if tm == "-":
|
409
|
+
res["target"] = None
|
410
|
+
else:
|
411
|
+
res["target"] = time.time() + float(tm)
|
412
|
+
|
413
|
+
res = await obj.client.set(path, value=res, nchain=3, chain=chain)
|
414
|
+
if obj.meta:
|
415
|
+
yprint(res, stream=obj.stdout)
|
416
|
+
|
417
|
+
|
418
|
+
@cli.command(short_help="Show runners' keepalive messages")
|
419
|
+
@click.pass_obj
|
420
|
+
async def monitor(obj):
|
421
|
+
"""
|
422
|
+
Runners periodically send a keepalive message. Show them.
|
423
|
+
"""
|
424
|
+
|
425
|
+
# TODO this does not watch changes in MoaT-KV.
|
426
|
+
# It also should watch individual jobs' state changes.
|
427
|
+
if obj.subpath[-1] == "-":
|
428
|
+
raise click.UsageError("Group '-' can only be used for listing.")
|
429
|
+
|
430
|
+
async with obj.client.msg_monitor("run") as cl:
|
431
|
+
async for msg in cl:
|
432
|
+
yprint(msg, stream=obj.stdout)
|
433
|
+
print("---", file=obj.stdout)
|
moat/kv/command/log.py
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import asyncclick as click
|
5
|
+
from moat.util import yprint
|
6
|
+
|
7
|
+
|
8
|
+
@click.group(short_help="Manage logging.") # pylint: disable=undefined-variable
|
9
|
+
async def cli():
|
10
|
+
"""
|
11
|
+
This subcommand controls a server's logging.
|
12
|
+
"""
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
@cli.command()
|
17
|
+
@click.option("-i", "--incremental", is_flag=True, help="Don't write the initial state")
|
18
|
+
@click.argument("path", nargs=1)
|
19
|
+
@click.pass_obj
|
20
|
+
async def dest(obj, path, incremental):
|
21
|
+
"""
|
22
|
+
Log changes to a file.
|
23
|
+
|
24
|
+
Any previously open log (on the server you talk to) is closed as soon
|
25
|
+
as the new one is opened and ready.
|
26
|
+
"""
|
27
|
+
res = await obj.client._request("log", path=path, fetch=not incremental)
|
28
|
+
if obj.meta:
|
29
|
+
yprint(res, stream=obj.stdout)
|
30
|
+
|
31
|
+
|
32
|
+
@cli.command()
|
33
|
+
@click.option("-f", "--full", is_flag=1, help="Also dump internal state")
|
34
|
+
@click.argument("path", nargs=1)
|
35
|
+
@click.pass_obj
|
36
|
+
async def save(obj, path, full):
|
37
|
+
"""
|
38
|
+
Write the server's current state to a file.
|
39
|
+
"""
|
40
|
+
res = await obj.client._request("save", path=path, full=full)
|
41
|
+
if obj.meta:
|
42
|
+
yprint(res, stream=obj.stdout)
|
43
|
+
|
44
|
+
|
45
|
+
@cli.command()
|
46
|
+
@click.pass_obj
|
47
|
+
async def stop(obj):
|
48
|
+
"""
|
49
|
+
Stop logging changes.
|
50
|
+
"""
|
51
|
+
res = await obj.client._request("log") # no path == stop
|
52
|
+
if obj.meta:
|
53
|
+
yprint(res, stream=obj.stdout)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# command line interface
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import asyncclick as click
|
5
|
+
|
6
|
+
from moat.kv.server import Server
|
7
|
+
|
8
|
+
|
9
|
+
@click.command(short_help="Run the MoaT-KV server.") # pylint: disable=undefined-variable
|
10
|
+
@click.option(
|
11
|
+
"-l",
|
12
|
+
"--load",
|
13
|
+
type=click.Path(readable=True, exists=True, allow_dash=False),
|
14
|
+
default=None,
|
15
|
+
help="Event log to preload.",
|
16
|
+
)
|
17
|
+
@click.option(
|
18
|
+
"-s",
|
19
|
+
"--save",
|
20
|
+
type=click.Path(writable=True, allow_dash=False),
|
21
|
+
default=None,
|
22
|
+
help="Event log to write to.",
|
23
|
+
hidden=True,
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"-i",
|
27
|
+
"--incremental",
|
28
|
+
default=None,
|
29
|
+
help="Save incremental changes, not the complete state",
|
30
|
+
hidden=True,
|
31
|
+
)
|
32
|
+
@click.option(
|
33
|
+
"-I",
|
34
|
+
"--init",
|
35
|
+
default=None,
|
36
|
+
help="Initial value to set the root to. Use only when setting up "
|
37
|
+
"a cluster for the first time!",
|
38
|
+
hidden=True,
|
39
|
+
)
|
40
|
+
@click.option(
|
41
|
+
"-e",
|
42
|
+
"--eval",
|
43
|
+
"eval_",
|
44
|
+
is_flag=True,
|
45
|
+
help="The 'init' value shall be evaluated.",
|
46
|
+
hidden=True,
|
47
|
+
)
|
48
|
+
@click.option(
|
49
|
+
"-a",
|
50
|
+
"--auth",
|
51
|
+
"--authoritative",
|
52
|
+
is_flag=True,
|
53
|
+
help="Data in this file is complete: mark anything missing as known even if not.",
|
54
|
+
)
|
55
|
+
@click.option(
|
56
|
+
"-f",
|
57
|
+
"--force",
|
58
|
+
is_flag=True,
|
59
|
+
help="Force 'successful' startup even if data are missing.",
|
60
|
+
)
|
61
|
+
@click.argument("name", nargs=1)
|
62
|
+
@click.argument("nodes", nargs=-1)
|
63
|
+
@click.pass_obj
|
64
|
+
async def cli(obj, name, load, save, init, incremental, eval_, auth, force, nodes):
|
65
|
+
"""
|
66
|
+
This command starts a MoaT-KV server. It defaults to connecting to the local Serf
|
67
|
+
agent.
|
68
|
+
|
69
|
+
All MoaT-KV servers must have a unique name. Its uniqueness cannot be
|
70
|
+
verified reliably.
|
71
|
+
|
72
|
+
One server in your network needs either an initial datum, or a copy of
|
73
|
+
a previously-saved MoaT-KV state. Otherwise, no client connections will
|
74
|
+
be accepted until synchronization with the other servers in your MoaT-KV
|
75
|
+
network is complete.
|
76
|
+
|
77
|
+
This command requires a unique NAME argument. The name identifies this
|
78
|
+
server on the network. Never start two servers with the same name!
|
79
|
+
|
80
|
+
You can force the server to fetch its data from a specific node, in
|
81
|
+
case some data are corrupted. (This should never be necessary.)
|
82
|
+
|
83
|
+
A server will refuse to start up as long as it knows about missing
|
84
|
+
entries. Use the 'force' flag to disable that. You should disable
|
85
|
+
any clients which use this server until the situation is resolved!
|
86
|
+
|
87
|
+
An auhthoritative server doesn't have missing data in its storage by
|
88
|
+
definition. This flag is used in the 'run' script when loading from a
|
89
|
+
file.
|
90
|
+
"""
|
91
|
+
|
92
|
+
kw = {}
|
93
|
+
if eval_:
|
94
|
+
kw["init"] = eval(init) # pylint: disable=eval-used
|
95
|
+
elif init == "-":
|
96
|
+
kw["init"] = None
|
97
|
+
elif init is not None:
|
98
|
+
kw["init"] = init
|
99
|
+
|
100
|
+
from moat.util import as_service
|
101
|
+
|
102
|
+
if load and nodes:
|
103
|
+
raise click.UsageError("Either read from a file or fetch from a node. Not both.")
|
104
|
+
if auth and force:
|
105
|
+
raise click.UsageError("Using both '-a' and '-f' is redundant. Choose one.")
|
106
|
+
|
107
|
+
async with as_service(obj) as evt:
|
108
|
+
s = Server(name, cfg=obj.cfg["kv"], **kw)
|
109
|
+
if load is not None:
|
110
|
+
await s.load(path=load, local=True, authoritative=auth)
|
111
|
+
if nodes:
|
112
|
+
await s.fetch_data(nodes, authoritative=auth)
|
113
|
+
|
114
|
+
await s.serve(log_path=save, log_inc=incremental, force=force, ready_evt=evt)
|