remoteRF-server 1.1.0.dev28__py3-none-any.whl → 1.1.0.dev30__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.
- remoteRF_server/server/commands.py +1 -1
- remoteRF_server/server/federation/cli.py +27 -4
- remoteRF_server/server/federation/cluster.py +196 -92
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/METADATA +1 -1
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/RECORD +9 -9
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/WHEEL +0 -0
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/entry_points.txt +0 -0
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/licenses/LICENSE +0 -0
- {remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/top_level.txt +0 -0
|
@@ -148,7 +148,7 @@ def _table() -> list[Command]:
|
|
|
148
148
|
Command(("global verify",), "Redeem the emailed code [code]", _global("verify"), "Global"),
|
|
149
149
|
Command(("global leave",), "Stop participating", _global("leave"), "Global"),
|
|
150
150
|
# ---- clusters (sharing devices with other labs) ----
|
|
151
|
-
Command(("cluster list",
|
|
151
|
+
Command(("cluster list",), "Clusters shared with other labs", _federation("list"), "Clusters"),
|
|
152
152
|
Command(("cluster create",), "Start a cluster: cluster create <name>", _federation("create"), "Clusters"),
|
|
153
153
|
Command(("cluster invite",), "Invite code for a lab: cluster invite <name>", _federation("invite"), "Clusters"),
|
|
154
154
|
Command(("cluster join",), "Join with a code: cluster join <code>", _federation("join"), "Clusters"),
|
|
@@ -32,6 +32,17 @@ def _print(text: str, style=None) -> None:
|
|
|
32
32
|
printf(text, style or Sty.DEFAULT)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _lab(party: dict) -> str:
|
|
36
|
+
short = party["deployment_id"][:8]
|
|
37
|
+
return f"{party['display_name']} ({short}…)" if party.get("display_name") else party["deployment_id"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _day(epoch) -> str:
|
|
41
|
+
import datetime
|
|
42
|
+
|
|
43
|
+
return datetime.datetime.fromtimestamp(int(epoch)).strftime("%Y-%m-%d")
|
|
44
|
+
|
|
45
|
+
|
|
35
46
|
def cmd_clusters() -> int:
|
|
36
47
|
from ...common.utils.panel import marker, print_panel
|
|
37
48
|
from . import cluster
|
|
@@ -48,10 +59,22 @@ def cmd_clusters() -> int:
|
|
|
48
59
|
items = cluster.list_clusters()
|
|
49
60
|
if not items:
|
|
50
61
|
rows.append("No clusters. Start one with `cluster create <name>`.")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
for index, item in enumerate(items):
|
|
63
|
+
if index:
|
|
64
|
+
rows.append("")
|
|
65
|
+
if item["role"] == "owner":
|
|
66
|
+
count = len(item["members"])
|
|
67
|
+
rows.append(f"{item['name']} (yours) {count} member lab{'s' if count != 1 else ''}"
|
|
68
|
+
+ (f", {item['open_invites']} open invite{'s' if item['open_invites'] != 1 else ''}"
|
|
69
|
+
if item["open_invites"] else ""))
|
|
70
|
+
for member in item["members"]:
|
|
71
|
+
rows.append(f" {_lab(member)} {member['state'] or 'joining'}"
|
|
72
|
+
+ (f" since {_day(member['joined_at'])}" if member.get("joined_at") else ""))
|
|
73
|
+
if not item["members"] and not item["open_invites"]:
|
|
74
|
+
rows.append(f" nobody yet: `cluster invite {item['name']}`")
|
|
75
|
+
else:
|
|
76
|
+
owner = _lab({"display_name": item.get("peer_name"), "deployment_id": item["peer"]})
|
|
77
|
+
rows.append(f"{item['name']} owned by {owner} {item['state']}")
|
|
55
78
|
print()
|
|
56
79
|
print_panel("Clusters", rows, literal=True)
|
|
57
80
|
print()
|
|
@@ -66,9 +66,39 @@ def _state_path() -> Path:
|
|
|
66
66
|
|
|
67
67
|
def load() -> dict:
|
|
68
68
|
try:
|
|
69
|
-
|
|
69
|
+
clusters = json.loads(_state_path().read_text())
|
|
70
70
|
except FileNotFoundError:
|
|
71
71
|
return {}
|
|
72
|
+
for cluster in clusters.values():
|
|
73
|
+
_upgrade(cluster)
|
|
74
|
+
return clusters
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _upgrade(cluster: dict) -> None:
|
|
78
|
+
"""First shape held one partner per owner cluster; now it holds a star."""
|
|
79
|
+
if cluster.get("role") != "owner" or "members" in cluster:
|
|
80
|
+
return
|
|
81
|
+
members, invites = {}, {}
|
|
82
|
+
if cluster.get("peer"):
|
|
83
|
+
members[cluster["peer"]] = {
|
|
84
|
+
"display_name": None, "contract_id": cluster.get("contract_id"),
|
|
85
|
+
"state": cluster.get("state"), "published": cluster.get("published", []),
|
|
86
|
+
"joined_at": cluster.get("created_at"),
|
|
87
|
+
}
|
|
88
|
+
if cluster.get("joiner"):
|
|
89
|
+
members[cluster["peer"]]["joiner"] = cluster["joiner"]
|
|
90
|
+
elif cluster.get("joiner"):
|
|
91
|
+
joiner = cluster["joiner"]
|
|
92
|
+
members[joiner["deployment_id"]] = {
|
|
93
|
+
"display_name": None, "contract_id": None, "state": "pending",
|
|
94
|
+
"published": [], "joined_at": cluster.get("created_at"), "joiner": joiner,
|
|
95
|
+
}
|
|
96
|
+
elif cluster.get("secret") and cluster.get("invite_id"):
|
|
97
|
+
invites[cluster["invite_id"]] = {"secret": cluster["secret"], "auto": cluster.get("auto", True),
|
|
98
|
+
"created_at": cluster.get("created_at")}
|
|
99
|
+
for key in ("peer", "contract_id", "state", "published", "joiner", "secret", "invite_id", "auto"):
|
|
100
|
+
cluster.pop(key, None)
|
|
101
|
+
cluster["members"], cluster["invites"] = members, invites
|
|
72
102
|
|
|
73
103
|
|
|
74
104
|
def save(clusters: dict) -> None:
|
|
@@ -186,18 +216,19 @@ def create(name: str) -> dict:
|
|
|
186
216
|
clusters = load()
|
|
187
217
|
if name in clusters:
|
|
188
218
|
raise ClusterError(f"cluster {name} already exists")
|
|
189
|
-
clusters[name] = {"role": "owner", "
|
|
219
|
+
clusters[name] = {"role": "owner", "created_at": int(time.time()), "members": {}, "invites": {}}
|
|
190
220
|
save(clusters)
|
|
191
221
|
return clusters[name]
|
|
192
222
|
|
|
193
223
|
|
|
194
224
|
def invite(name: str, *, auto: bool = True) -> str:
|
|
195
|
-
"""The code is the approval: whoever redeems it is
|
|
196
|
-
without a further step here (`auto=False` keeps a
|
|
225
|
+
"""One code admits one lab. The code is the approval: whoever redeems it is
|
|
226
|
+
pinned and contracted without a further step here (`auto=False` keeps a
|
|
227
|
+
manual `accept`). Several codes may be open at once."""
|
|
197
228
|
clusters = load()
|
|
198
229
|
cluster = _find(clusters, name)
|
|
199
230
|
if cluster["role"] != "owner":
|
|
200
|
-
raise ClusterError(f"{name} is owned by another deployment")
|
|
231
|
+
raise ClusterError(f"{name} is owned by another deployment; only the owner invites")
|
|
201
232
|
me = _pin_self()
|
|
202
233
|
invite_id, secret = secrets.token_hex(5), secrets.token_bytes(20)
|
|
203
234
|
g.call("POST", "/v2/broker/invites", {
|
|
@@ -208,7 +239,7 @@ def invite(name: str, *, auto: bool = True) -> str:
|
|
|
208
239
|
"identity": me,
|
|
209
240
|
"tag": _tag(secret, invite_id, me),
|
|
210
241
|
})
|
|
211
|
-
cluster
|
|
242
|
+
cluster["invites"][invite_id] = {"secret": secret.hex(), "auto": auto, "created_at": int(time.time())}
|
|
212
243
|
save(clusters)
|
|
213
244
|
return encode_code(invite_id, secret)
|
|
214
245
|
|
|
@@ -230,10 +261,13 @@ def join(code: str) -> str:
|
|
|
230
261
|
_pin(owner["identity"], source="cluster_invite")
|
|
231
262
|
name = view["cluster_name"]
|
|
232
263
|
clusters = load()
|
|
264
|
+
if name in clusters and clusters[name]["role"] == "owner":
|
|
265
|
+
raise ClusterError(f"{name} is your own cluster")
|
|
233
266
|
clusters[name] = {
|
|
234
267
|
"role": "member",
|
|
235
268
|
"state": "joined",
|
|
236
269
|
"peer": owner["identity"]["deployment_id"],
|
|
270
|
+
"peer_name": owner.get("display_name"),
|
|
237
271
|
"invite_id": invite_id,
|
|
238
272
|
"created_at": int(time.time()),
|
|
239
273
|
}
|
|
@@ -241,13 +275,9 @@ def join(code: str) -> str:
|
|
|
241
275
|
return name
|
|
242
276
|
|
|
243
277
|
|
|
244
|
-
def
|
|
245
|
-
"""Owner: pin the joiner and propose
|
|
246
|
-
|
|
247
|
-
cluster = _find(clusters, name)
|
|
248
|
-
if cluster.get("state") != "pending":
|
|
249
|
-
raise ClusterError(f"{name} has no join waiting for approval")
|
|
250
|
-
joiner = cluster["joiner"]
|
|
278
|
+
def _propose(cluster: dict, member: dict, peer: str) -> None:
|
|
279
|
+
"""Owner: pin the joiner and propose its bilateral contract."""
|
|
280
|
+
joiner = member.pop("joiner")
|
|
251
281
|
_pin(joiner, source="cluster_invite")
|
|
252
282
|
me = _identity()
|
|
253
283
|
a, b = sorted([me, joiner], key=lambda item: item["deployment_id"])
|
|
@@ -267,29 +297,49 @@ def accept(name: str) -> dict:
|
|
|
267
297
|
"supersedes_version": None,
|
|
268
298
|
}
|
|
269
299
|
_contracts().propose(terms, actor_admin_ref="cluster")
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
member.update(state="proposed", contract_id=terms["contract_id"])
|
|
301
|
+
_publish(member)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def accept(name: str, peer: str | None = None) -> dict:
|
|
305
|
+
"""Owner, manual invites only: approve a join that is waiting."""
|
|
306
|
+
clusters = load()
|
|
307
|
+
cluster = _find(clusters, name)
|
|
308
|
+
pending = {pid: m for pid, m in cluster.get("members", {}).items() if m.get("state") == "pending"}
|
|
309
|
+
if not pending:
|
|
310
|
+
raise ClusterError(f"{name} has no join waiting for approval")
|
|
311
|
+
if peer is None and len(pending) > 1:
|
|
312
|
+
raise ClusterError(f"{name} has several joins waiting; name one: " + ", ".join(sorted(pending)))
|
|
313
|
+
peer = peer or next(iter(pending))
|
|
314
|
+
if peer not in pending:
|
|
315
|
+
raise ClusterError(f"{peer} is not waiting to join {name}")
|
|
316
|
+
_propose(cluster, pending[peer], peer)
|
|
273
317
|
save(clusters)
|
|
274
|
-
_publish(cluster)
|
|
275
318
|
return cluster
|
|
276
319
|
|
|
277
320
|
|
|
321
|
+
def _revoke(contract_id: str | None) -> None:
|
|
322
|
+
if not contract_id:
|
|
323
|
+
return
|
|
324
|
+
try:
|
|
325
|
+
_contracts().revoke(contract_id, 1, actor_admin_ref="cluster")
|
|
326
|
+
except ContractError as exc:
|
|
327
|
+
if exc.code not in ("contract_revoked", "contract_terminal", "invalid_transition"):
|
|
328
|
+
raise
|
|
329
|
+
|
|
330
|
+
|
|
278
331
|
def remove(name: str) -> str:
|
|
279
|
-
"""Leave or disband: revoke
|
|
280
|
-
the
|
|
332
|
+
"""Leave (member) or disband (owner): revoke every contract -- a terminal,
|
|
333
|
+
signed event the other side imports on its next sync -- and forget the
|
|
334
|
+
cluster locally."""
|
|
281
335
|
clusters = load()
|
|
282
336
|
cluster = _find(clusters, name)
|
|
283
337
|
outcome = "removed"
|
|
284
|
-
|
|
285
|
-
|
|
338
|
+
parties = list(cluster["members"].values()) if cluster["role"] == "owner" else [cluster]
|
|
339
|
+
for party in parties:
|
|
340
|
+
_revoke(party.get("contract_id"))
|
|
286
341
|
try:
|
|
287
|
-
|
|
288
|
-
except ContractError as exc:
|
|
289
|
-
if exc.code not in ("contract_revoked", "contract_terminal", "invalid_transition"):
|
|
290
|
-
raise
|
|
291
|
-
try:
|
|
292
|
-
_publish(cluster) # the other lab imports the revocation on its next sync
|
|
342
|
+
_publish(party)
|
|
293
343
|
except g.GlobalError:
|
|
294
344
|
outcome = "removed locally; could not reach Global to tell the other lab"
|
|
295
345
|
del clusters[name]
|
|
@@ -298,30 +348,48 @@ def remove(name: str) -> str:
|
|
|
298
348
|
|
|
299
349
|
|
|
300
350
|
def list_clusters() -> list[dict]:
|
|
351
|
+
"""Owner rows carry `members` and `open_invites`; member rows carry `peer`."""
|
|
301
352
|
store = None
|
|
302
353
|
out = []
|
|
303
354
|
for name, cluster in sorted(load().items()):
|
|
304
|
-
item = {"name": name,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
355
|
+
item = {"name": name, "role": cluster["role"], "created_at": cluster.get("created_at")}
|
|
356
|
+
if cluster["role"] == "owner":
|
|
357
|
+
members = []
|
|
358
|
+
for peer, member in sorted(cluster["members"].items()):
|
|
359
|
+
state = member.get("state")
|
|
360
|
+
if member.get("contract_id"):
|
|
361
|
+
try:
|
|
362
|
+
store = store or _contracts()
|
|
363
|
+
state = store.show(member["contract_id"], 1)["state"]
|
|
364
|
+
except ContractError:
|
|
365
|
+
pass
|
|
366
|
+
members.append({"deployment_id": peer, "display_name": member.get("display_name"),
|
|
367
|
+
"state": state, "joined_at": member.get("joined_at")})
|
|
368
|
+
item.update(members=members, open_invites=len(cluster["invites"]))
|
|
369
|
+
else:
|
|
370
|
+
state = cluster.get("state")
|
|
371
|
+
if cluster.get("contract_id"):
|
|
372
|
+
try:
|
|
373
|
+
store = store or _contracts()
|
|
374
|
+
state = store.show(cluster["contract_id"], 1)["state"]
|
|
375
|
+
except ContractError:
|
|
376
|
+
pass
|
|
377
|
+
item.update(peer=cluster["peer"], peer_name=cluster.get("peer_name"), state=state)
|
|
312
378
|
out.append(item)
|
|
313
379
|
return out
|
|
314
380
|
|
|
315
381
|
|
|
316
382
|
# -----------------------------
|
|
317
|
-
# Agent
|
|
383
|
+
# Agent: one pass per Global liveness tick
|
|
318
384
|
# -----------------------------
|
|
319
385
|
|
|
320
|
-
def _publish(
|
|
321
|
-
"""Send our own signed events for
|
|
386
|
+
def _publish(party: dict) -> None:
|
|
387
|
+
"""Send our own signed events for this party's contract to the broker."""
|
|
388
|
+
if not party.get("contract_id"):
|
|
389
|
+
return
|
|
322
390
|
store = _contracts()
|
|
323
|
-
shown = store.show(
|
|
324
|
-
done = set(
|
|
391
|
+
shown = store.show(party["contract_id"], 1, include_signed_events=True)
|
|
392
|
+
done = set(party.setdefault("published", []))
|
|
325
393
|
for event in shown["events"]:
|
|
326
394
|
if event["actor_deployment_id"] != store.local_deployment_id or event["event_id"] in done:
|
|
327
395
|
continue
|
|
@@ -329,75 +397,111 @@ def _publish(cluster: dict) -> None:
|
|
|
329
397
|
"protocol_version": "2", "terms": shown["terms"], "signed_event": event["signed_event"],
|
|
330
398
|
})
|
|
331
399
|
done.add(event["event_id"])
|
|
332
|
-
|
|
400
|
+
party["published"] = sorted(done)
|
|
333
401
|
|
|
334
402
|
|
|
335
|
-
def _sync_contract(
|
|
336
|
-
"""Import the peer's events for this
|
|
403
|
+
def _sync_contract(party: dict, peer: str, directory: list[dict]) -> str:
|
|
404
|
+
"""Import the peer's events for this party's contract; return the local state."""
|
|
337
405
|
store = _contracts()
|
|
338
|
-
if not
|
|
339
|
-
# Member: the owner's proposal names us
|
|
406
|
+
if not party.get("contract_id"):
|
|
407
|
+
# Member: adopt the owner's proposal that names both of us -- the
|
|
408
|
+
# newest one that is not already revoked (an earlier cluster's may be).
|
|
409
|
+
candidates = []
|
|
340
410
|
for item in directory:
|
|
341
411
|
terms = item.get("terms") or {}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
412
|
+
parties = {terms.get("deployment_a"), terms.get("deployment_b")}
|
|
413
|
+
if parties != {peer, store.local_deployment_id} or item["version"] != 1 or not item.get("events"):
|
|
414
|
+
continue
|
|
415
|
+
store.import_bundle(canonical_json({
|
|
416
|
+
"protocol_version": "2", "terms": terms, "events": item["events"],
|
|
417
|
+
}))
|
|
418
|
+
if store.show(item["contract_id"], 1)["state"] != "revoked":
|
|
419
|
+
candidates.append((terms["valid_from"], item["contract_id"]))
|
|
420
|
+
if not candidates:
|
|
421
|
+
return party["state"]
|
|
422
|
+
party["contract_id"] = max(candidates)[1]
|
|
347
423
|
for item in directory:
|
|
348
|
-
if item["contract_id"] ==
|
|
424
|
+
if item["contract_id"] == party["contract_id"] and item.get("terms"):
|
|
349
425
|
store.import_bundle(canonical_json({
|
|
350
426
|
"protocol_version": "2", "terms": item["terms"], "events": item["events"],
|
|
351
427
|
}))
|
|
352
|
-
return store.show(
|
|
428
|
+
return store.show(party["contract_id"], 1)["state"]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _advance(party: dict, peer: str, directory: list[dict], *, label: str, notify, is_member: bool) -> None:
|
|
432
|
+
"""Move one bilateral contract one step; notify on active / revoked."""
|
|
433
|
+
before = party.get("state")
|
|
434
|
+
if before in ("pending", "revoked"):
|
|
435
|
+
return
|
|
436
|
+
state = _sync_contract(party, peer, directory)
|
|
437
|
+
store = _contracts()
|
|
438
|
+
if state == "proposed" and is_member:
|
|
439
|
+
store.accept(party["contract_id"], 1, actor_admin_ref="cluster")
|
|
440
|
+
state = store.activate(party["contract_id"], 1, actor_admin_ref="cluster")["state"]
|
|
441
|
+
elif state == "accepted":
|
|
442
|
+
mine = {e["actor_deployment_id"] for e in store.show(party["contract_id"], 1)["events"]
|
|
443
|
+
if e["event_type"] == "activate"}
|
|
444
|
+
if store.local_deployment_id not in mine:
|
|
445
|
+
state = store.activate(party["contract_id"], 1, actor_admin_ref="cluster")["state"]
|
|
446
|
+
_publish(party)
|
|
447
|
+
if party.get("contract_id"):
|
|
448
|
+
party["state"] = state
|
|
449
|
+
if state == "active" and before != "active":
|
|
450
|
+
notify(f"[cluster] {label} is active")
|
|
451
|
+
if state == "revoked" and before != "revoked":
|
|
452
|
+
notify(f"[cluster] {label} was ended by the other lab")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _collect_joins(name: str, cluster: dict, broker_invites: list[dict], notify) -> None:
|
|
456
|
+
"""Owner: turn redeemed invites into members; drop invites Global no longer holds."""
|
|
457
|
+
seen = {item["invite_id"]: item for item in broker_invites}
|
|
458
|
+
for invite_id, invite in list(cluster["invites"].items()):
|
|
459
|
+
item = seen.get(invite_id)
|
|
460
|
+
if item is None:
|
|
461
|
+
del cluster["invites"][invite_id] # expired or consumed elsewhere
|
|
462
|
+
continue
|
|
463
|
+
if not item.get("joiner"):
|
|
464
|
+
continue
|
|
465
|
+
joiner = item["joiner"]
|
|
466
|
+
secret = bytes.fromhex(invite["secret"])
|
|
467
|
+
if not hmac.compare_digest(joiner["tag"], _tag(secret, invite_id, joiner["identity"])):
|
|
468
|
+
notify(f"[cluster] {name}: a join arrived without the invite secret; ignored")
|
|
469
|
+
del cluster["invites"][invite_id]
|
|
470
|
+
continue
|
|
471
|
+
peer = joiner["identity"]["deployment_id"]
|
|
472
|
+
member = cluster["members"].setdefault(peer, {"published": [], "joined_at": int(time.time())})
|
|
473
|
+
member.update(display_name=joiner.get("display_name"), joiner=joiner["identity"], state="pending")
|
|
474
|
+
del cluster["invites"][invite_id]
|
|
475
|
+
if invite.get("auto", True):
|
|
476
|
+
_propose(cluster, member, peer)
|
|
477
|
+
else:
|
|
478
|
+
notify(f"[cluster] {peer} wants to join {name}: run `cluster accept {name}`")
|
|
353
479
|
|
|
354
480
|
|
|
355
481
|
def tick(notify=None) -> None:
|
|
356
|
-
"""One agent pass: move every cluster one step closer to active."""
|
|
482
|
+
"""One agent pass: move every cluster and member one step closer to active."""
|
|
357
483
|
notify = notify or (lambda text: None)
|
|
358
484
|
clusters = load()
|
|
359
485
|
if not clusters:
|
|
360
486
|
return
|
|
361
487
|
directory = None
|
|
488
|
+
broker_invites = None
|
|
362
489
|
for name, cluster in clusters.items():
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if item["invite_id"] != cluster["invite_id"] or not item.get("joiner"):
|
|
369
|
-
continue
|
|
370
|
-
joiner = item["joiner"]
|
|
371
|
-
secret = bytes.fromhex(cluster["secret"])
|
|
372
|
-
if not hmac.compare_digest(joiner["tag"], _tag(secret, cluster["invite_id"], joiner["identity"])):
|
|
373
|
-
notify(f"[cluster] {name}: a join arrived without the invite secret; ignored")
|
|
374
|
-
continue
|
|
375
|
-
cluster.update(state="pending", joiner=joiner["identity"])
|
|
490
|
+
if cluster["role"] == "owner":
|
|
491
|
+
if cluster["invites"]:
|
|
492
|
+
if broker_invites is None:
|
|
493
|
+
broker_invites = g.call("GET", "/v2/broker/invites")["invites"]
|
|
494
|
+
_collect_joins(name, cluster, broker_invites, notify)
|
|
376
495
|
save(clusters)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
directory
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if contract_state == "proposed" and cluster["role"] == "member":
|
|
389
|
-
store.accept(cluster["contract_id"], 1, actor_admin_ref="cluster")
|
|
390
|
-
contract_state = store.activate(cluster["contract_id"], 1, actor_admin_ref="cluster")["state"]
|
|
391
|
-
elif contract_state == "accepted":
|
|
392
|
-
mine = {e["actor_deployment_id"] for e in store.show(cluster["contract_id"], 1)["events"]
|
|
393
|
-
if e["event_type"] == "activate"}
|
|
394
|
-
if store.local_deployment_id not in mine:
|
|
395
|
-
contract_state = store.activate(cluster["contract_id"], 1, actor_admin_ref="cluster")["state"]
|
|
396
|
-
if cluster.get("contract_id"):
|
|
397
|
-
_publish(cluster)
|
|
398
|
-
if contract_state == "active" and state != "active":
|
|
399
|
-
notify(f"[cluster] {name} is active")
|
|
400
|
-
if contract_state == "revoked" and state != "revoked":
|
|
401
|
-
notify(f"[cluster] {name} was ended by the other lab; `cluster remove {name}` forgets it")
|
|
402
|
-
cluster["state"] = contract_state if cluster.get("contract_id") else state
|
|
496
|
+
if not cluster["members"]:
|
|
497
|
+
continue
|
|
498
|
+
if directory is None:
|
|
499
|
+
directory = g.call("GET", "/v2/broker/contracts")["contracts"]
|
|
500
|
+
for peer, member in cluster["members"].items():
|
|
501
|
+
_advance(member, peer, directory, label=f"{name}: {member.get('display_name') or peer}",
|
|
502
|
+
notify=notify, is_member=False)
|
|
503
|
+
else:
|
|
504
|
+
if directory is None:
|
|
505
|
+
directory = g.call("GET", "/v2/broker/contracts")["contracts"]
|
|
506
|
+
_advance(cluster, cluster["peer"], directory, label=name, notify=notify, is_member=True)
|
|
403
507
|
save(clusters)
|
|
@@ -52,7 +52,7 @@ remoteRF_server/host/host_tunnel_server.py,sha256=sz06xOtFbaDZSy1UQfw6faKhJmuOy5
|
|
|
52
52
|
remoteRF_server/server/__init__.py,sha256=-1h_xQjiE66oiWRO-vnLGJ4smpgUNhFuV-ZreW9_tD4,667
|
|
53
53
|
remoteRF_server/server/acc_perms.py,sha256=2loCvstVu8uvsb2uCv2Sk7iNQ5GyO_sBIkPQFzqOaRI,13334
|
|
54
54
|
remoteRF_server/server/cert_provider.py,sha256=CRDrZL4DSf-eL6wzPJzwucrzPLOr2PvNRq5V--oZtr0,6272
|
|
55
|
-
remoteRF_server/server/commands.py,sha256=
|
|
55
|
+
remoteRF_server/server/commands.py,sha256=dH19PK86cUiZhTnN9jC1RdM1t_t6dPuL6x-bdACxkSY,17008
|
|
56
56
|
remoteRF_server/server/console.py,sha256=J91LfwPVmBgielOxfE740NOYduekggyZkqi-47y_A4w,4472
|
|
57
57
|
remoteRF_server/server/control.py,sha256=Uq1Fk0zIoHHX_EXfz3OuNle8urvO_UhAiB5Lk5cs2e4,5177
|
|
58
58
|
remoteRF_server/server/device_manager.py,sha256=nRTvk_jeZFU3QnlM6qf3DIckU2V2e_gyDeD5gW14KQI,38802
|
|
@@ -70,8 +70,8 @@ remoteRF_server/server/deployment/__init__.py,sha256=YV86uafd8XiGD7sLaqNfO5xxmt5
|
|
|
70
70
|
remoteRF_server/server/deployment/capabilities.py,sha256=20RxmGG03VoQQZnV3jGhc-3i9B5hOl5fCopnsErFjPk,872
|
|
71
71
|
remoteRF_server/server/federation/__init__.py,sha256=MZLqqh0Lzzh1t59VXrkS_bdpMO5znk1UVBjRGPvnUCg,735
|
|
72
72
|
remoteRF_server/server/federation/admin.py,sha256=t3F17FIytekzclPICmaFJEA4-F5OuWcDx3rgmFga17A,9187
|
|
73
|
-
remoteRF_server/server/federation/cli.py,sha256=
|
|
74
|
-
remoteRF_server/server/federation/cluster.py,sha256=
|
|
73
|
+
remoteRF_server/server/federation/cli.py,sha256=Ax0tQw7_giLUP5lX1Y6x3sS1cbUbU2PxYur5k_5f3Po,7028
|
|
74
|
+
remoteRF_server/server/federation/cluster.py,sha256=2C8ys2BwUSxCLlYGvNjWU840q8e6H2ShGfl380yzA0I,19970
|
|
75
75
|
remoteRF_server/server/federation/contracts.py,sha256=NYh7lb_9etJHFfzXwevjC4HfSBUypLxg5xGeBgydLF4,29305
|
|
76
76
|
remoteRF_server/server/federation/destination.py,sha256=XawmGCiVbL_MfNNn8xwV9_bYxUDW1L7efCpL11K2JAc,35909
|
|
77
77
|
remoteRF_server/server/federation/devices.py,sha256=n5MvnD0UZyl4sFpiekG9ProanWXTYizYq6YchCflLgY,18061
|
|
@@ -90,9 +90,9 @@ remoteRF_server/tools/gen_certs.py,sha256=Vbyu8JjRu__UYPVmyCRXt5SOw0Za1HV7g1md3G
|
|
|
90
90
|
remoteRF_server/tools/gist_status.py,sha256=SFv2iQgQrIKT9gxKrU-_Dr7S0p0rE4wQM6J3OC6FcoM,4649
|
|
91
91
|
remoteRF_server/tools/gist_status_testing.py,sha256=IYw7UkPjAUpKDTtIlnYGb5g6SJkwz4TRGUfJJ6f4LhQ,2690
|
|
92
92
|
remoteRF_server/tools/introspect_uhd.py,sha256=vUpPb0rM9tsHPxWadS3EfMDE-gMd-TvNV0TjrZOQqBY,4531
|
|
93
|
-
remoterf_server-1.1.0.
|
|
94
|
-
remoterf_server-1.1.0.
|
|
95
|
-
remoterf_server-1.1.0.
|
|
96
|
-
remoterf_server-1.1.0.
|
|
97
|
-
remoterf_server-1.1.0.
|
|
98
|
-
remoterf_server-1.1.0.
|
|
93
|
+
remoterf_server-1.1.0.dev30.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
94
|
+
remoterf_server-1.1.0.dev30.dist-info/METADATA,sha256=HAEJaoA51VNwScm3KxXvq650DbitxyOWXy1ChAXpD3s,42069
|
|
95
|
+
remoterf_server-1.1.0.dev30.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
96
|
+
remoterf_server-1.1.0.dev30.dist-info/entry_points.txt,sha256=_bbKk65PfYi-n6zUjqOiOwcalf_Ws1TCGATIe1TukxE,136
|
|
97
|
+
remoterf_server-1.1.0.dev30.dist-info/top_level.txt,sha256=NuYWe8dSx3qdbxshWBwOgx68EHmJ9I8Zvxam4ARsQ_I,30
|
|
98
|
+
remoterf_server-1.1.0.dev30.dist-info/RECORD,,
|
|
File without changes
|
{remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{remoterf_server-1.1.0.dev28.dist-info → remoterf_server-1.1.0.dev30.dist-info}/top_level.txt
RENAMED
|
File without changes
|