moat-kv 0.70.23__py3-none-any.whl → 0.71.0__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.
Files changed (177) hide show
  1. build/lib/docs/source/conf.py +201 -0
  2. build/lib/examples/pathify.py +45 -0
  3. build/lib/moat/kv/__init__.py +19 -0
  4. build/lib/moat/kv/_cfg.yaml +93 -0
  5. build/lib/moat/kv/_main.py +91 -0
  6. build/lib/moat/kv/actor/__init__.py +98 -0
  7. build/lib/moat/kv/actor/deletor.py +139 -0
  8. build/lib/moat/kv/auth/__init__.py +444 -0
  9. build/lib/moat/kv/auth/_test.py +166 -0
  10. build/lib/moat/kv/auth/password.py +234 -0
  11. build/lib/moat/kv/auth/root.py +58 -0
  12. build/lib/moat/kv/backend/__init__.py +67 -0
  13. build/lib/moat/kv/backend/mqtt.py +71 -0
  14. build/lib/moat/kv/client.py +1025 -0
  15. build/lib/moat/kv/code.py +236 -0
  16. build/lib/moat/kv/codec.py +11 -0
  17. build/lib/moat/kv/command/__init__.py +1 -0
  18. build/lib/moat/kv/command/acl.py +180 -0
  19. build/lib/moat/kv/command/auth.py +261 -0
  20. build/lib/moat/kv/command/code.py +293 -0
  21. build/lib/moat/kv/command/codec.py +186 -0
  22. build/lib/moat/kv/command/data.py +265 -0
  23. build/lib/moat/kv/command/dump/__init__.py +143 -0
  24. build/lib/moat/kv/command/error.py +149 -0
  25. build/lib/moat/kv/command/internal.py +248 -0
  26. build/lib/moat/kv/command/job.py +433 -0
  27. build/lib/moat/kv/command/log.py +53 -0
  28. build/lib/moat/kv/command/server.py +114 -0
  29. build/lib/moat/kv/command/type.py +201 -0
  30. build/lib/moat/kv/config.py +46 -0
  31. build/lib/moat/kv/data.py +216 -0
  32. build/lib/moat/kv/errors.py +561 -0
  33. build/lib/moat/kv/exceptions.py +126 -0
  34. build/lib/moat/kv/mock/__init__.py +101 -0
  35. build/lib/moat/kv/mock/mqtt.py +159 -0
  36. build/lib/moat/kv/mock/tracer.py +63 -0
  37. build/lib/moat/kv/model.py +1069 -0
  38. build/lib/moat/kv/obj/__init__.py +646 -0
  39. build/lib/moat/kv/obj/command.py +241 -0
  40. build/lib/moat/kv/runner.py +1347 -0
  41. build/lib/moat/kv/server.py +2809 -0
  42. build/lib/moat/kv/types.py +513 -0
  43. ci/rtd-requirements.txt +4 -0
  44. ci/test-requirements.txt +7 -0
  45. ci/travis.sh +96 -0
  46. debian/.gitignore +7 -0
  47. debian/changelog +1435 -0
  48. debian/control +43 -0
  49. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
  50. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
  51. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
  52. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +93 -0
  53. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
  54. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
  55. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
  56. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
  57. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
  58. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
  59. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
  60. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
  61. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +71 -0
  62. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
  63. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
  64. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
  65. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
  66. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
  67. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
  68. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
  69. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
  70. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
  71. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
  72. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
  73. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
  74. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
  75. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
  76. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
  77. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
  91. debian/moat-kv.postinst +3 -0
  92. debian/rules +20 -0
  93. debian/source/format +1 -0
  94. debian/watch +4 -0
  95. docs/Makefile +20 -0
  96. docs/make.bat +36 -0
  97. docs/source/TODO.rst +61 -0
  98. docs/source/_static/.gitkeep +0 -0
  99. docs/source/acls.rst +80 -0
  100. docs/source/auth.rst +84 -0
  101. docs/source/client_protocol.rst +456 -0
  102. docs/source/code.rst +341 -0
  103. docs/source/command_line.rst +1187 -0
  104. docs/source/common_protocol.rst +47 -0
  105. docs/source/conf.py +201 -0
  106. docs/source/debugging.rst +70 -0
  107. docs/source/extend.rst +37 -0
  108. docs/source/history.rst +36 -0
  109. docs/source/index.rst +75 -0
  110. docs/source/model.rst +54 -0
  111. docs/source/overview.rst +83 -0
  112. docs/source/related.rst +89 -0
  113. docs/source/server_protocol.rst +450 -0
  114. docs/source/startup.rst +31 -0
  115. docs/source/translator.rst +244 -0
  116. docs/source/tutorial.rst +711 -0
  117. docs/source/v3.rst +168 -0
  118. examples/code/transform.scale.yml +21 -0
  119. examples/code/transform.switch.yml +82 -0
  120. examples/code/transform.timeslot.yml +63 -0
  121. examples/pathify.py +45 -0
  122. moat/kv/_cfg.yaml +2 -6
  123. moat/kv/actor/__init__.py +98 -0
  124. moat/kv/actor/deletor.py +139 -0
  125. moat/kv/auth/__init__.py +444 -0
  126. moat/kv/auth/_test.py +166 -0
  127. moat/kv/auth/password.py +234 -0
  128. moat/kv/auth/root.py +58 -0
  129. moat/kv/backend/__init__.py +67 -0
  130. moat/kv/backend/mqtt.py +71 -0
  131. moat/kv/command/__init__.py +1 -0
  132. moat/kv/command/acl.py +180 -0
  133. moat/kv/command/auth.py +261 -0
  134. moat/kv/command/code.py +293 -0
  135. moat/kv/command/codec.py +186 -0
  136. moat/kv/command/data.py +265 -0
  137. moat/kv/command/dump/__init__.py +143 -0
  138. moat/kv/command/error.py +149 -0
  139. moat/kv/command/internal.py +248 -0
  140. moat/kv/command/job.py +433 -0
  141. moat/kv/command/log.py +53 -0
  142. moat/kv/command/server.py +114 -0
  143. moat/kv/command/type.py +201 -0
  144. moat/kv/mock/__init__.py +101 -0
  145. moat/kv/mock/mqtt.py +159 -0
  146. moat/kv/mock/tracer.py +63 -0
  147. moat/kv/obj/__init__.py +646 -0
  148. moat/kv/obj/command.py +241 -0
  149. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/METADATA +2 -5
  150. moat_kv-0.71.0.dist-info/RECORD +188 -0
  151. moat_kv-0.71.0.dist-info/top_level.txt +9 -0
  152. scripts/current +15 -0
  153. scripts/env +8 -0
  154. scripts/init +39 -0
  155. scripts/recover +17 -0
  156. scripts/rotate +33 -0
  157. scripts/run +29 -0
  158. scripts/run-all +10 -0
  159. scripts/run-any +10 -0
  160. scripts/run-single +15 -0
  161. scripts/success +4 -0
  162. systemd/moat-kv-recover.service +21 -0
  163. systemd/moat-kv-rotate.service +20 -0
  164. systemd/moat-kv-rotate.timer +10 -0
  165. systemd/moat-kv-run-all.service +26 -0
  166. systemd/moat-kv-run-all@.service +25 -0
  167. systemd/moat-kv-run-any.service +26 -0
  168. systemd/moat-kv-run-any@.service +25 -0
  169. systemd/moat-kv-run-single.service +26 -0
  170. systemd/moat-kv-run-single@.service +25 -0
  171. systemd/moat-kv.service +27 -0
  172. systemd/postinst +7 -0
  173. systemd/sysusers +3 -0
  174. moat_kv-0.70.23.dist-info/RECORD +0 -19
  175. moat_kv-0.70.23.dist-info/top_level.txt +0 -1
  176. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/WHEEL +0 -0
  177. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.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)