flwr-nightly 1.23.0.dev20251029__py3-none-any.whl → 1.23.0.dev20251031__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.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/new/new.py +187 -35
- flwr/client/grpc_adapter_client/connection.py +4 -6
- flwr/client/grpc_rere_client/connection.py +47 -24
- flwr/client/grpc_rere_client/grpc_adapter.py +0 -16
- flwr/client/rest_client/connection.py +70 -33
- flwr/common/constant.py +3 -1
- flwr/proto/fleet_pb2.py +31 -39
- flwr/proto/fleet_pb2.pyi +0 -48
- flwr/proto/fleet_pb2_grpc.py +0 -66
- flwr/proto/fleet_pb2_grpc.pyi +0 -20
- flwr/server/app.py +30 -16
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +0 -6
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +92 -124
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +14 -5
- flwr/server/superlink/fleet/message_handler/message_handler.py +66 -23
- flwr/server/superlink/fleet/rest_rere/rest_api.py +20 -28
- flwr/server/superlink/fleet/vce/vce_api.py +3 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +4 -3
- flwr/server/superlink/linkstate/sqlite_linkstate.py +4 -4
- flwr/supercore/constant.py +4 -0
- flwr/supernode/cli/flower_supernode.py +7 -0
- flwr/supernode/start_client_internal.py +3 -9
- {flwr_nightly-1.23.0.dev20251029.dist-info → flwr_nightly-1.23.0.dev20251031.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251029.dist-info → flwr_nightly-1.23.0.dev20251031.dist-info}/RECORD +26 -26
- {flwr_nightly-1.23.0.dev20251029.dist-info → flwr_nightly-1.23.0.dev20251031.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251029.dist-info → flwr_nightly-1.23.0.dev20251031.dist-info}/entry_points.txt +0 -0
flwr/cli/new/new.py
CHANGED
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
"""Flower command line interface `new` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
18
20
|
import re
|
|
21
|
+
import zipfile
|
|
19
22
|
from enum import Enum
|
|
20
23
|
from pathlib import Path
|
|
21
24
|
from string import Template
|
|
22
25
|
from typing import Annotated, Optional
|
|
23
26
|
|
|
27
|
+
import requests
|
|
24
28
|
import typer
|
|
25
29
|
|
|
30
|
+
from flwr.supercore.constant import APP_ID_PATTERN, PLATFORM_API_URL
|
|
31
|
+
|
|
26
32
|
from ..utils import (
|
|
27
33
|
is_valid_project_name,
|
|
28
34
|
prompt_options,
|
|
@@ -93,6 +99,180 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
|
|
|
93
99
|
create_file(file_path, content)
|
|
94
100
|
|
|
95
101
|
|
|
102
|
+
def print_success_prompt(
|
|
103
|
+
package_name: str, llm_challenge_str: Optional[str] = None
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Print styled setup instructions for running a new Flower App after creation."""
|
|
106
|
+
prompt = typer.style(
|
|
107
|
+
"🎊 Flower App creation successful.\n\n"
|
|
108
|
+
"To run your Flower App, first install its dependencies:\n\n",
|
|
109
|
+
fg=typer.colors.GREEN,
|
|
110
|
+
bold=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
114
|
+
|
|
115
|
+
prompt += typer.style(
|
|
116
|
+
f" cd {package_name} && pip install -e .\n" + _add + "\n",
|
|
117
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
118
|
+
bold=True,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
prompt += typer.style(
|
|
122
|
+
"then, run the app:\n\n ",
|
|
123
|
+
fg=typer.colors.GREEN,
|
|
124
|
+
bold=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
prompt += typer.style(
|
|
128
|
+
"\tflwr run .\n\n",
|
|
129
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
130
|
+
bold=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
prompt += typer.style(
|
|
134
|
+
"💡 Check the README in your app directory to learn how to\n"
|
|
135
|
+
"customize it and how to run it using the Deployment Runtime.\n",
|
|
136
|
+
fg=typer.colors.GREEN,
|
|
137
|
+
bold=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
print(prompt)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Security: prevent zip-slip
|
|
144
|
+
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
|
|
145
|
+
"""Extract ZIP file into destination directory."""
|
|
146
|
+
dest_dir = dest_dir.resolve()
|
|
147
|
+
|
|
148
|
+
def _is_within_directory(base: Path, target: Path) -> bool:
|
|
149
|
+
try:
|
|
150
|
+
target.relative_to(base)
|
|
151
|
+
return True
|
|
152
|
+
except ValueError:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
for member in zf.infolist():
|
|
156
|
+
# Skip directory placeholders;
|
|
157
|
+
# ZipInfo can represent them as names ending with '/'.
|
|
158
|
+
if member.is_dir():
|
|
159
|
+
target_path = (dest_dir / member.filename).resolve()
|
|
160
|
+
if not _is_within_directory(dest_dir, target_path):
|
|
161
|
+
raise ValueError(f"Unsafe path in zip: {member.filename}")
|
|
162
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Files
|
|
166
|
+
target_path = (dest_dir / member.filename).resolve()
|
|
167
|
+
if not _is_within_directory(dest_dir, target_path):
|
|
168
|
+
raise ValueError(f"Unsafe path in zip: {member.filename}")
|
|
169
|
+
|
|
170
|
+
# Ensure parent exists
|
|
171
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
# Extract
|
|
174
|
+
with zf.open(member, "r") as src, open(target_path, "wb") as dst:
|
|
175
|
+
dst.write(src.read())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
179
|
+
"""Download ZIP file from Platform API to memory."""
|
|
180
|
+
try:
|
|
181
|
+
r = requests.get(presigned_url, timeout=60)
|
|
182
|
+
r.raise_for_status()
|
|
183
|
+
except requests.RequestException as e:
|
|
184
|
+
raise typer.BadParameter(f"ZIP download failed: {e}") from e
|
|
185
|
+
|
|
186
|
+
buf = io.BytesIO(r.content)
|
|
187
|
+
# Validate it's a zip
|
|
188
|
+
if not zipfile.is_zipfile(buf):
|
|
189
|
+
raise typer.BadParameter("Downloaded file is not a valid ZIP")
|
|
190
|
+
buf.seek(0)
|
|
191
|
+
return buf
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _request_download_link(identifier: str) -> str:
|
|
195
|
+
"""Request download link from Flower platform API."""
|
|
196
|
+
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
197
|
+
headers = {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Accept": "application/json",
|
|
200
|
+
}
|
|
201
|
+
body = {
|
|
202
|
+
"identifier": identifier, # send raw string of identifier
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
resp = requests.post(url, headers=headers, data=json.dumps(body), timeout=20)
|
|
207
|
+
except requests.RequestException as e:
|
|
208
|
+
raise typer.BadParameter(f"Unable to connect to Platform API: {e}") from e
|
|
209
|
+
|
|
210
|
+
if resp.status_code == 404:
|
|
211
|
+
raise typer.BadParameter(f"'{identifier}' not found in Platform API")
|
|
212
|
+
if not resp.ok:
|
|
213
|
+
raise typer.BadParameter(
|
|
214
|
+
f"Platform API request failed with "
|
|
215
|
+
f"status {resp.status_code}. Details: {resp.text}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
data = resp.json()
|
|
219
|
+
if "zip_url" not in data:
|
|
220
|
+
raise typer.BadParameter("Invalid response from Platform API")
|
|
221
|
+
return str(data["zip_url"])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def download_remote_app_via_api(identifier: str) -> None:
|
|
225
|
+
"""Download App from Platform API."""
|
|
226
|
+
# Parse @user/app just to derive local dir name
|
|
227
|
+
m = re.match(APP_ID_PATTERN, identifier)
|
|
228
|
+
if not m:
|
|
229
|
+
raise typer.BadParameter(
|
|
230
|
+
"Invalid remote app ID. Expected format: '@user_name/app_name'."
|
|
231
|
+
)
|
|
232
|
+
app_name = m.group("app")
|
|
233
|
+
|
|
234
|
+
project_dir = Path.cwd() / app_name
|
|
235
|
+
if project_dir.exists():
|
|
236
|
+
if not typer.confirm(
|
|
237
|
+
typer.style(
|
|
238
|
+
f"\n💬 {app_name} already exists, do you want to override it?",
|
|
239
|
+
fg=typer.colors.MAGENTA,
|
|
240
|
+
bold=True,
|
|
241
|
+
)
|
|
242
|
+
):
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
print(
|
|
246
|
+
typer.style(
|
|
247
|
+
f"\n🔗 Requesting download link for {identifier}...",
|
|
248
|
+
fg=typer.colors.GREEN,
|
|
249
|
+
bold=True,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
presigned_url = _request_download_link(identifier)
|
|
253
|
+
|
|
254
|
+
print(
|
|
255
|
+
typer.style(
|
|
256
|
+
"⬇️ Downloading ZIP into memory...",
|
|
257
|
+
fg=typer.colors.GREEN,
|
|
258
|
+
bold=True,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
zip_buf = _download_zip_to_memory(presigned_url)
|
|
262
|
+
|
|
263
|
+
print(
|
|
264
|
+
typer.style(
|
|
265
|
+
f"📦 Unpacking into {project_dir}...",
|
|
266
|
+
fg=typer.colors.GREEN,
|
|
267
|
+
bold=True,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
with zipfile.ZipFile(zip_buf) as zf:
|
|
271
|
+
_safe_extract_zip(zf, Path.cwd())
|
|
272
|
+
|
|
273
|
+
print_success_prompt(app_name)
|
|
274
|
+
|
|
275
|
+
|
|
96
276
|
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
97
277
|
def new(
|
|
98
278
|
app_name: Annotated[
|
|
@@ -111,6 +291,12 @@ def new(
|
|
|
111
291
|
"""Create new Flower App."""
|
|
112
292
|
if app_name is None:
|
|
113
293
|
app_name = prompt_text("Please provide the app name")
|
|
294
|
+
|
|
295
|
+
# Download remote app
|
|
296
|
+
if app_name and app_name.startswith("@"):
|
|
297
|
+
download_remote_app_via_api(app_name)
|
|
298
|
+
return
|
|
299
|
+
|
|
114
300
|
if not is_valid_project_name(app_name):
|
|
115
301
|
app_name = prompt_text(
|
|
116
302
|
"Please provide a name that only contains "
|
|
@@ -282,38 +468,4 @@ def new(
|
|
|
282
468
|
context=context,
|
|
283
469
|
)
|
|
284
470
|
|
|
285
|
-
|
|
286
|
-
"🎊 Flower App creation successful.\n\n"
|
|
287
|
-
"To run your Flower App, first install its dependencies:\n\n",
|
|
288
|
-
fg=typer.colors.GREEN,
|
|
289
|
-
bold=True,
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
293
|
-
|
|
294
|
-
prompt += typer.style(
|
|
295
|
-
f" cd {package_name} && pip install -e .\n" + _add + "\n",
|
|
296
|
-
fg=typer.colors.BRIGHT_CYAN,
|
|
297
|
-
bold=True,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
prompt += typer.style(
|
|
301
|
-
"then, run the app:\n\n ",
|
|
302
|
-
fg=typer.colors.GREEN,
|
|
303
|
-
bold=True,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
prompt += typer.style(
|
|
307
|
-
"\tflwr run .\n\n",
|
|
308
|
-
fg=typer.colors.BRIGHT_CYAN,
|
|
309
|
-
bold=True,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
prompt += typer.style(
|
|
313
|
-
"💡 Check the README in your app directory to learn how to\n"
|
|
314
|
-
"customize it and how to run it using the Deployment Runtime.\n",
|
|
315
|
-
fg=typer.colors.GREEN,
|
|
316
|
-
bold=True,
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
print(prompt)
|
|
471
|
+
print_success_prompt(package_name, llm_challenge_str)
|
|
@@ -44,10 +44,9 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
|
|
|
44
44
|
] = None,
|
|
45
45
|
) -> Iterator[
|
|
46
46
|
tuple[
|
|
47
|
+
int,
|
|
47
48
|
Callable[[], Optional[tuple[Message, ObjectTree]]],
|
|
48
49
|
Callable[[Message, ObjectTree], set[str]],
|
|
49
|
-
Callable[[], Optional[int]],
|
|
50
|
-
Callable[[], None],
|
|
51
50
|
Callable[[int], Run],
|
|
52
51
|
Callable[[str, int], Fab],
|
|
53
52
|
Callable[[int, str], bytes],
|
|
@@ -81,12 +80,11 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
|
|
|
81
80
|
|
|
82
81
|
Returns
|
|
83
82
|
-------
|
|
83
|
+
node_id : int
|
|
84
84
|
receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
|
|
85
85
|
send : Callable[[Message, ObjectTree], set[str]]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
get_run : Optional[Callable]
|
|
89
|
-
get_fab : Optional[Callable]
|
|
86
|
+
get_run : Callable[[int], Run]
|
|
87
|
+
get_fab : Callable[[str, int], Fab]
|
|
90
88
|
pull_object : Callable[[str], bytes]
|
|
91
89
|
push_object : Callable[[str, bytes], None]
|
|
92
90
|
confirm_message_received : Callable[[str], None]
|
|
@@ -19,7 +19,7 @@ from collections.abc import Iterator, Sequence
|
|
|
19
19
|
from contextlib import contextmanager
|
|
20
20
|
from logging import ERROR
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Callable, Optional, Union
|
|
22
|
+
from typing import Callable, Optional, Union
|
|
23
23
|
|
|
24
24
|
import grpc
|
|
25
25
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
@@ -45,12 +45,15 @@ from flwr.common.serde import (
|
|
|
45
45
|
from flwr.common.typing import Fab, Run
|
|
46
46
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
47
47
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
ActivateNodeRequest,
|
|
49
|
+
ActivateNodeResponse,
|
|
50
|
+
DeactivateNodeRequest,
|
|
50
51
|
PullMessagesRequest,
|
|
51
52
|
PullMessagesResponse,
|
|
52
53
|
PushMessagesRequest,
|
|
53
54
|
PushMessagesResponse,
|
|
55
|
+
RegisterNodeFleetRequest,
|
|
56
|
+
UnregisterNodeFleetRequest,
|
|
54
57
|
)
|
|
55
58
|
from flwr.proto.fleet_pb2_grpc import FleetStub # pylint: disable=E0611
|
|
56
59
|
from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
|
|
@@ -79,10 +82,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
79
82
|
adapter_cls: Optional[Union[type[FleetStub], type[GrpcAdapter]]] = None,
|
|
80
83
|
) -> Iterator[
|
|
81
84
|
tuple[
|
|
85
|
+
int,
|
|
82
86
|
Callable[[], Optional[tuple[Message, ObjectTree]]],
|
|
83
87
|
Callable[[Message, ObjectTree], set[str]],
|
|
84
|
-
Callable[[], Optional[int]],
|
|
85
|
-
Callable[[], None],
|
|
86
88
|
Callable[[int], Run],
|
|
87
89
|
Callable[[str, int], Fab],
|
|
88
90
|
Callable[[int, str], bytes],
|
|
@@ -125,11 +127,11 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
125
127
|
|
|
126
128
|
Returns
|
|
127
129
|
-------
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
node_id : int
|
|
131
|
+
receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
|
|
132
|
+
send : Callable[[Message, ObjectTree], set[str]]
|
|
133
|
+
get_run : Callable[[int], Run]
|
|
134
|
+
get_fab : Callable[[str, int], Fab]
|
|
133
135
|
pull_object : Callable[[str], bytes]
|
|
134
136
|
push_object : Callable[[str, bytes], None]
|
|
135
137
|
confirm_message_received : Callable[[str], None]
|
|
@@ -138,7 +140,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
138
140
|
root_certificates = Path(root_certificates).read_bytes()
|
|
139
141
|
|
|
140
142
|
# Automatic node auth: generate keys if user didn't provide any
|
|
143
|
+
self_registered = False
|
|
141
144
|
if authentication_keys is None:
|
|
145
|
+
self_registered = True
|
|
142
146
|
authentication_keys = generate_key_pairs()
|
|
143
147
|
|
|
144
148
|
# Always configure auth interceptor, with either user-provided or generated keys
|
|
@@ -164,7 +168,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
164
168
|
# Wrap stub
|
|
165
169
|
_wrap_stub(stub, retry_invoker)
|
|
166
170
|
###########################################################################
|
|
167
|
-
#
|
|
171
|
+
# SuperNode functions
|
|
168
172
|
###########################################################################
|
|
169
173
|
|
|
170
174
|
def send_node_heartbeat() -> bool:
|
|
@@ -201,23 +205,26 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
201
205
|
|
|
202
206
|
heartbeat_sender = HeartbeatSender(send_node_heartbeat)
|
|
203
207
|
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
def register_node() -> None:
|
|
209
|
+
"""Register node with SuperLink."""
|
|
210
|
+
stub.RegisterNode(RegisterNodeFleetRequest(public_key=node_pk))
|
|
211
|
+
|
|
212
|
+
def activate_node() -> int:
|
|
213
|
+
"""Activate node and start heartbeat."""
|
|
214
|
+
req = ActivateNodeRequest(
|
|
208
215
|
public_key=node_pk,
|
|
209
216
|
heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
|
|
210
217
|
)
|
|
211
|
-
|
|
218
|
+
res: ActivateNodeResponse = stub.ActivateNode(req)
|
|
212
219
|
|
|
213
220
|
# Remember the node and start the heartbeat sender
|
|
214
221
|
nonlocal node
|
|
215
|
-
node =
|
|
222
|
+
node = Node(node_id=res.node_id)
|
|
216
223
|
heartbeat_sender.start()
|
|
217
224
|
return node.node_id
|
|
218
225
|
|
|
219
|
-
def
|
|
220
|
-
"""
|
|
226
|
+
def deactivate_node() -> None:
|
|
227
|
+
"""Deactivate node and stop heartbeat."""
|
|
221
228
|
# Get Node
|
|
222
229
|
nonlocal node
|
|
223
230
|
if node is None:
|
|
@@ -228,8 +235,20 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
228
235
|
heartbeat_sender.stop()
|
|
229
236
|
|
|
230
237
|
# Call FleetAPI
|
|
231
|
-
|
|
232
|
-
stub.
|
|
238
|
+
req = DeactivateNodeRequest(node_id=node.node_id)
|
|
239
|
+
stub.DeactivateNode(req)
|
|
240
|
+
|
|
241
|
+
def unregister_node() -> None:
|
|
242
|
+
"""Unregister node from SuperLink."""
|
|
243
|
+
# Get Node
|
|
244
|
+
nonlocal node
|
|
245
|
+
if node is None:
|
|
246
|
+
log(ERROR, "Node instance missing")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Call FleetAPI
|
|
250
|
+
req = UnregisterNodeFleetRequest(node_id=node.node_id)
|
|
251
|
+
stub.UnregisterNode(req)
|
|
233
252
|
|
|
234
253
|
# Cleanup
|
|
235
254
|
node = None
|
|
@@ -336,12 +355,14 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
336
355
|
fn(object_id)
|
|
337
356
|
|
|
338
357
|
try:
|
|
358
|
+
if self_registered:
|
|
359
|
+
register_node()
|
|
360
|
+
node_id = activate_node()
|
|
339
361
|
# Yield methods
|
|
340
362
|
yield (
|
|
363
|
+
node_id,
|
|
341
364
|
receive,
|
|
342
365
|
send,
|
|
343
|
-
create_node,
|
|
344
|
-
delete_node,
|
|
345
366
|
get_run,
|
|
346
367
|
get_fab,
|
|
347
368
|
pull_object,
|
|
@@ -356,7 +377,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
356
377
|
if node is not None:
|
|
357
378
|
# Disable retrying
|
|
358
379
|
retry_invoker.max_tries = 1
|
|
359
|
-
|
|
380
|
+
deactivate_node()
|
|
381
|
+
if self_registered:
|
|
382
|
+
unregister_node()
|
|
360
383
|
except grpc.RpcError:
|
|
361
384
|
pass
|
|
362
385
|
channel.close()
|
|
@@ -36,12 +36,8 @@ from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=
|
|
|
36
36
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
37
37
|
ActivateNodeRequest,
|
|
38
38
|
ActivateNodeResponse,
|
|
39
|
-
CreateNodeRequest,
|
|
40
|
-
CreateNodeResponse,
|
|
41
39
|
DeactivateNodeRequest,
|
|
42
40
|
DeactivateNodeResponse,
|
|
43
|
-
DeleteNodeRequest,
|
|
44
|
-
DeleteNodeResponse,
|
|
45
41
|
PullMessagesRequest,
|
|
46
42
|
PullMessagesResponse,
|
|
47
43
|
PushMessagesRequest,
|
|
@@ -126,18 +122,6 @@ class GrpcAdapter:
|
|
|
126
122
|
response.ParseFromString(container_res.grpc_message_content)
|
|
127
123
|
return response
|
|
128
124
|
|
|
129
|
-
def CreateNode( # pylint: disable=C0103
|
|
130
|
-
self, request: CreateNodeRequest, **kwargs: Any
|
|
131
|
-
) -> CreateNodeResponse:
|
|
132
|
-
"""."""
|
|
133
|
-
return self._send_and_receive(request, CreateNodeResponse, **kwargs)
|
|
134
|
-
|
|
135
|
-
def DeleteNode( # pylint: disable=C0103
|
|
136
|
-
self, request: DeleteNodeRequest, **kwargs: Any
|
|
137
|
-
) -> DeleteNodeResponse:
|
|
138
|
-
"""."""
|
|
139
|
-
return self._send_and_receive(request, DeleteNodeResponse, **kwargs)
|
|
140
|
-
|
|
141
125
|
def RegisterNode( # pylint: disable=C0103
|
|
142
126
|
self, request: RegisterNodeFleetRequest, **kwargs: Any
|
|
143
127
|
) -> RegisterNodeFleetResponse:
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"""Contextmanager for a REST request-response channel to the Flower server."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import secrets
|
|
19
18
|
from collections.abc import Iterator
|
|
20
19
|
from contextlib import contextmanager
|
|
21
20
|
from logging import ERROR, WARN
|
|
@@ -46,14 +45,18 @@ from flwr.common.serde import (
|
|
|
46
45
|
from flwr.common.typing import Fab, Run
|
|
47
46
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
48
47
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
ActivateNodeRequest,
|
|
49
|
+
ActivateNodeResponse,
|
|
50
|
+
DeactivateNodeRequest,
|
|
51
|
+
DeactivateNodeResponse,
|
|
53
52
|
PullMessagesRequest,
|
|
54
53
|
PullMessagesResponse,
|
|
55
54
|
PushMessagesRequest,
|
|
56
55
|
PushMessagesResponse,
|
|
56
|
+
RegisterNodeFleetRequest,
|
|
57
|
+
RegisterNodeFleetResponse,
|
|
58
|
+
UnregisterNodeFleetRequest,
|
|
59
|
+
UnregisterNodeFleetResponse,
|
|
57
60
|
)
|
|
58
61
|
from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
|
|
59
62
|
SendNodeHeartbeatRequest,
|
|
@@ -70,6 +73,7 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
|
|
|
70
73
|
)
|
|
71
74
|
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
|
72
75
|
from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
|
|
76
|
+
from flwr.supercore.primitives.asymmetric import generate_key_pairs, public_key_to_bytes
|
|
73
77
|
|
|
74
78
|
try:
|
|
75
79
|
import requests
|
|
@@ -77,8 +81,10 @@ except ModuleNotFoundError:
|
|
|
77
81
|
flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
|
|
78
82
|
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
PATH_REGISTER_NODE: str = "/api/v0/fleet/register-node"
|
|
85
|
+
PATH_ACTIVATE_NODE: str = "/api/v0/fleet/activate-node"
|
|
86
|
+
PATH_DEACTIVATE_NODE: str = "/api/v0/fleet/deactivate-node"
|
|
87
|
+
PATH_UNREGISTER_NODE: str = "/api/v0/fleet/unregister-node"
|
|
82
88
|
PATH_PULL_MESSAGES: str = "/api/v0/fleet/pull-messages"
|
|
83
89
|
PATH_PUSH_MESSAGES: str = "/api/v0/fleet/push-messages"
|
|
84
90
|
PATH_PULL_OBJECT: str = "/api/v0/fleet/pull-object"
|
|
@@ -105,10 +111,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
105
111
|
] = None,
|
|
106
112
|
) -> Iterator[
|
|
107
113
|
tuple[
|
|
114
|
+
int,
|
|
108
115
|
Callable[[], Optional[tuple[Message, ObjectTree]]],
|
|
109
116
|
Callable[[Message, ObjectTree], set[str]],
|
|
110
|
-
Callable[[], Optional[int]],
|
|
111
|
-
Callable[[], None],
|
|
112
117
|
Callable[[int], Run],
|
|
113
118
|
Callable[[str, int], Fab],
|
|
114
119
|
Callable[[int, str], bytes],
|
|
@@ -144,11 +149,11 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
144
149
|
|
|
145
150
|
Returns
|
|
146
151
|
-------
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
node_id : int
|
|
153
|
+
receive : Callable[[], Optional[tuple[Message, ObjectTree]]]
|
|
154
|
+
send : Callable[[Message, ObjectTree], set[str]]
|
|
155
|
+
get_run : Callable[[int], Run]
|
|
156
|
+
get_fab : Callable[[str, int], Fab]
|
|
152
157
|
pull_object : Callable[[str], bytes]
|
|
153
158
|
push_object : Callable[[str, bytes], None]
|
|
154
159
|
confirm_message_received : Callable[[str], None]
|
|
@@ -179,6 +184,13 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
179
184
|
if authentication_keys is not None:
|
|
180
185
|
log(ERROR, "Client authentication is not supported for this transport type.")
|
|
181
186
|
|
|
187
|
+
# REST does NOT support node authentication
|
|
188
|
+
self_registered = False
|
|
189
|
+
if authentication_keys is None:
|
|
190
|
+
self_registered = True
|
|
191
|
+
authentication_keys = generate_key_pairs()
|
|
192
|
+
node_pk = public_key_to_bytes(authentication_keys[1])
|
|
193
|
+
|
|
182
194
|
# Shared variables for inner functions
|
|
183
195
|
node: Optional[Node] = None
|
|
184
196
|
|
|
@@ -186,7 +198,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
186
198
|
retry_invoker.should_giveup = None
|
|
187
199
|
|
|
188
200
|
###########################################################################
|
|
189
|
-
#
|
|
201
|
+
# SuperNode functions
|
|
190
202
|
###########################################################################
|
|
191
203
|
|
|
192
204
|
def _request(
|
|
@@ -296,28 +308,35 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
296
308
|
|
|
297
309
|
heartbeat_sender = HeartbeatSender(send_node_heartbeat)
|
|
298
310
|
|
|
299
|
-
def
|
|
300
|
-
"""
|
|
301
|
-
req =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
def register_node() -> None:
|
|
312
|
+
"""Register node with SuperLink."""
|
|
313
|
+
req = RegisterNodeFleetRequest(public_key=node_pk)
|
|
314
|
+
|
|
315
|
+
# Send the request
|
|
316
|
+
res = _request(req, RegisterNodeFleetResponse, PATH_REGISTER_NODE)
|
|
317
|
+
if res is None:
|
|
318
|
+
raise RuntimeError("Failed to register node")
|
|
319
|
+
|
|
320
|
+
def activate_node() -> int:
|
|
321
|
+
"""Activate node and start heartbeat."""
|
|
322
|
+
req = ActivateNodeRequest(
|
|
323
|
+
public_key=node_pk,
|
|
305
324
|
heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
|
|
306
325
|
)
|
|
307
326
|
|
|
308
327
|
# Send the request
|
|
309
|
-
res = _request(req,
|
|
328
|
+
res = _request(req, ActivateNodeResponse, PATH_ACTIVATE_NODE)
|
|
310
329
|
if res is None:
|
|
311
|
-
|
|
330
|
+
raise RuntimeError("Failed to activate node")
|
|
312
331
|
|
|
313
332
|
# Remember the node and start the heartbeat sender
|
|
314
333
|
nonlocal node
|
|
315
|
-
node = res.
|
|
334
|
+
node = Node(node_id=res.node_id)
|
|
316
335
|
heartbeat_sender.start()
|
|
317
336
|
return node.node_id
|
|
318
337
|
|
|
319
|
-
def
|
|
320
|
-
"""
|
|
338
|
+
def deactivate_node() -> None:
|
|
339
|
+
"""Deactivate node and stop heartbeat."""
|
|
321
340
|
nonlocal node
|
|
322
341
|
if node is None:
|
|
323
342
|
raise RuntimeError("Node instance missing")
|
|
@@ -325,13 +344,27 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
325
344
|
# Stop the heartbeat sender
|
|
326
345
|
heartbeat_sender.stop()
|
|
327
346
|
|
|
328
|
-
# Send
|
|
329
|
-
req =
|
|
347
|
+
# Send DeactivateNode request
|
|
348
|
+
req = DeactivateNodeRequest(node_id=node.node_id)
|
|
349
|
+
|
|
350
|
+
# Send the request
|
|
351
|
+
res = _request(req, DeactivateNodeResponse, PATH_DEACTIVATE_NODE)
|
|
352
|
+
if res is None:
|
|
353
|
+
raise RuntimeError("Failed to deactivate node")
|
|
354
|
+
|
|
355
|
+
def unregister_node() -> None:
|
|
356
|
+
"""Unregister node from SuperLink."""
|
|
357
|
+
nonlocal node
|
|
358
|
+
if node is None:
|
|
359
|
+
raise RuntimeError("Node instance missing")
|
|
360
|
+
|
|
361
|
+
# Send UnregisterNode request
|
|
362
|
+
req = UnregisterNodeFleetRequest(node_id=node.node_id)
|
|
330
363
|
|
|
331
364
|
# Send the request
|
|
332
|
-
res = _request(req,
|
|
365
|
+
res = _request(req, UnregisterNodeFleetResponse, PATH_UNREGISTER_NODE)
|
|
333
366
|
if res is None:
|
|
334
|
-
|
|
367
|
+
raise RuntimeError("Failed to unregister node")
|
|
335
368
|
|
|
336
369
|
# Cleanup
|
|
337
370
|
node = None
|
|
@@ -447,12 +480,14 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
447
480
|
fn(object_id)
|
|
448
481
|
|
|
449
482
|
try:
|
|
483
|
+
if self_registered:
|
|
484
|
+
register_node()
|
|
485
|
+
node_id = activate_node()
|
|
450
486
|
# Yield methods
|
|
451
487
|
yield (
|
|
488
|
+
node_id,
|
|
452
489
|
receive,
|
|
453
490
|
send,
|
|
454
|
-
create_node,
|
|
455
|
-
delete_node,
|
|
456
491
|
get_run,
|
|
457
492
|
get_fab,
|
|
458
493
|
pull_object,
|
|
@@ -467,6 +502,8 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
467
502
|
if node is not None:
|
|
468
503
|
# Disable retrying
|
|
469
504
|
retry_invoker.max_tries = 1
|
|
470
|
-
|
|
505
|
+
deactivate_node()
|
|
506
|
+
if self_registered:
|
|
507
|
+
unregister_node()
|
|
471
508
|
except RequestsConnectionError:
|
|
472
509
|
pass
|
flwr/common/constant.py
CHANGED
|
@@ -62,7 +62,9 @@ HEARTBEAT_DEFAULT_INTERVAL = 30
|
|
|
62
62
|
HEARTBEAT_CALL_TIMEOUT = 5
|
|
63
63
|
HEARTBEAT_BASE_MULTIPLIER = 0.8
|
|
64
64
|
HEARTBEAT_RANDOM_RANGE = (-0.1, 0.1)
|
|
65
|
-
|
|
65
|
+
HEARTBEAT_MIN_INTERVAL = 10
|
|
66
|
+
HEARTBEAT_MAX_INTERVAL = 1800 # 30 minutes
|
|
67
|
+
HEARTBEAT_INTERVAL_INF = 1e300 # Large value, disabling heartbeats
|
|
66
68
|
HEARTBEAT_PATIENCE = 2
|
|
67
69
|
RUN_FAILURE_DETAILS_NO_HEARTBEAT = "No heartbeat received from the run."
|
|
68
70
|
|