flwr 1.18.0__py3-none-any.whl → 1.19.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.
- flwr/app/__init__.py +15 -0
- flwr/app/error.py +68 -0
- flwr/app/metadata.py +223 -0
- flwr/cli/build.py +82 -57
- flwr/cli/log.py +3 -3
- flwr/cli/login/login.py +3 -7
- flwr/cli/ls.py +15 -36
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +10 -18
- flwr/cli/stop.py +2 -2
- flwr/cli/utils.py +31 -5
- flwr/client/__init__.py +2 -2
- flwr/client/client_app.py +1 -1
- flwr/client/clientapp/__init__.py +0 -7
- flwr/client/grpc_adapter_client/connection.py +4 -4
- flwr/client/grpc_rere_client/connection.py +130 -60
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
- flwr/client/message_handler/message_handler.py +1 -1
- flwr/client/mod/comms_mods.py +36 -17
- flwr/client/rest_client/connection.py +173 -67
- flwr/clientapp/__init__.py +15 -0
- flwr/common/__init__.py +2 -2
- flwr/common/auth_plugin/__init__.py +2 -0
- flwr/common/auth_plugin/auth_plugin.py +29 -3
- flwr/common/constant.py +36 -7
- flwr/common/event_log_plugin/event_log_plugin.py +3 -3
- flwr/common/exit_handlers.py +30 -0
- flwr/common/heartbeat.py +165 -0
- flwr/common/inflatable.py +290 -0
- flwr/common/inflatable_grpc_utils.py +99 -0
- flwr/common/inflatable_rest_utils.py +99 -0
- flwr/common/inflatable_utils.py +341 -0
- flwr/common/message.py +110 -242
- flwr/common/record/__init__.py +2 -1
- flwr/common/record/array.py +323 -0
- flwr/common/record/arrayrecord.py +103 -225
- flwr/common/record/configrecord.py +59 -4
- flwr/common/record/conversion_utils.py +1 -1
- flwr/common/record/metricrecord.py +55 -4
- flwr/common/record/recorddict.py +69 -1
- flwr/common/recorddict_compat.py +2 -2
- flwr/common/retry_invoker.py +5 -1
- flwr/common/serde.py +59 -183
- flwr/common/serde_utils.py +175 -0
- flwr/common/typing.py +5 -3
- flwr/compat/__init__.py +15 -0
- flwr/compat/client/__init__.py +15 -0
- flwr/{client → compat/client}/app.py +19 -159
- flwr/compat/common/__init__.py +15 -0
- flwr/compat/server/__init__.py +15 -0
- flwr/compat/server/app.py +174 -0
- flwr/compat/simulation/__init__.py +15 -0
- flwr/proto/fleet_pb2.py +32 -27
- flwr/proto/fleet_pb2.pyi +49 -35
- flwr/proto/fleet_pb2_grpc.py +117 -13
- flwr/proto/fleet_pb2_grpc.pyi +47 -6
- flwr/proto/heartbeat_pb2.py +33 -0
- flwr/proto/heartbeat_pb2.pyi +66 -0
- flwr/proto/heartbeat_pb2_grpc.py +4 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +28 -11
- flwr/proto/message_pb2.pyi +125 -0
- flwr/proto/recorddict_pb2.py +16 -28
- flwr/proto/recorddict_pb2.pyi +46 -64
- flwr/proto/run_pb2.py +24 -32
- flwr/proto/run_pb2.pyi +4 -52
- flwr/proto/serverappio_pb2.py +32 -23
- flwr/proto/serverappio_pb2.pyi +45 -3
- flwr/proto/serverappio_pb2_grpc.py +138 -34
- flwr/proto/serverappio_pb2_grpc.pyi +54 -13
- flwr/proto/simulationio_pb2.py +12 -11
- flwr/proto/simulationio_pb2_grpc.py +35 -0
- flwr/proto/simulationio_pb2_grpc.pyi +14 -0
- flwr/server/__init__.py +1 -1
- flwr/server/app.py +68 -186
- flwr/server/compat/app_utils.py +50 -28
- flwr/server/fleet_event_log_interceptor.py +2 -2
- flwr/server/grid/grpc_grid.py +104 -34
- flwr/server/grid/inmemory_grid.py +5 -4
- flwr/server/serverapp/app.py +18 -0
- flwr/server/superlink/ffs/__init__.py +2 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +13 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +101 -7
- flwr/server/superlink/fleet/message_handler/message_handler.py +135 -18
- flwr/server/superlink/fleet/rest_rere/rest_api.py +72 -11
- flwr/server/superlink/fleet/vce/vce_api.py +6 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
- flwr/server/superlink/linkstate/linkstate.py +53 -20
- flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
- flwr/server/superlink/linkstate/utils.py +33 -29
- flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
- flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
- flwr/server/superlink/simulation/simulationio_servicer.py +25 -1
- flwr/server/superlink/utils.py +44 -2
- flwr/server/utils/validator.py +2 -2
- flwr/serverapp/__init__.py +15 -0
- flwr/simulation/app.py +17 -0
- flwr/supercore/__init__.py +15 -0
- flwr/supercore/object_store/__init__.py +24 -0
- flwr/supercore/object_store/in_memory_object_store.py +229 -0
- flwr/supercore/object_store/object_store.py +192 -0
- flwr/supercore/object_store/object_store_factory.py +44 -0
- flwr/superexec/deployment.py +6 -2
- flwr/superexec/exec_event_log_interceptor.py +4 -4
- flwr/superexec/exec_grpc.py +7 -3
- flwr/superexec/exec_servicer.py +125 -23
- flwr/superexec/exec_user_auth_interceptor.py +37 -8
- flwr/superexec/executor.py +4 -0
- flwr/superexec/simulation.py +7 -1
- flwr/superlink/__init__.py +15 -0
- flwr/{client/supernode → supernode}/__init__.py +0 -7
- flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +7 -14
- flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -12
- flwr/supernode/cli/flwr_clientapp.py +81 -0
- flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
- flwr/supernode/nodestate/nodestate.py +212 -0
- flwr/supernode/runtime/__init__.py +15 -0
- flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +25 -56
- flwr/supernode/servicer/__init__.py +15 -0
- flwr/supernode/servicer/clientappio/__init__.py +24 -0
- flwr/supernode/start_client_internal.py +491 -0
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/METADATA +5 -4
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/RECORD +141 -108
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
- {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
- flwr/client/heartbeat.py +0 -74
- flwr/client/nodestate/in_memory_nodestate.py +0 -38
- /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
- /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
- /flwr/{client → supernode}/nodestate/__init__.py +0 -0
- /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
- /flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +0 -0
flwr/superexec/exec_servicer.py
CHANGED
|
@@ -18,21 +18,25 @@
|
|
|
18
18
|
import time
|
|
19
19
|
from collections.abc import Generator
|
|
20
20
|
from logging import ERROR, INFO
|
|
21
|
-
from typing import Any, Optional
|
|
22
|
-
from uuid import UUID
|
|
21
|
+
from typing import Any, Optional, cast
|
|
23
22
|
|
|
24
23
|
import grpc
|
|
25
24
|
|
|
26
25
|
from flwr.common import now
|
|
27
26
|
from flwr.common.auth_plugin import ExecAuthPlugin
|
|
28
|
-
from flwr.common.constant import
|
|
27
|
+
from flwr.common.constant import (
|
|
28
|
+
LOG_STREAM_INTERVAL,
|
|
29
|
+
RUN_ID_NOT_FOUND_MESSAGE,
|
|
30
|
+
Status,
|
|
31
|
+
SubStatus,
|
|
32
|
+
)
|
|
29
33
|
from flwr.common.logger import log
|
|
30
34
|
from flwr.common.serde import (
|
|
31
35
|
config_record_from_proto,
|
|
32
36
|
run_to_proto,
|
|
33
37
|
user_config_from_proto,
|
|
34
38
|
)
|
|
35
|
-
from flwr.common.typing import RunStatus
|
|
39
|
+
from flwr.common.typing import Run, RunStatus
|
|
36
40
|
from flwr.proto import exec_pb2_grpc # pylint: disable=E0611
|
|
37
41
|
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
38
42
|
GetAuthTokensRequest,
|
|
@@ -50,22 +54,26 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
|
50
54
|
)
|
|
51
55
|
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
52
56
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
57
|
+
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
53
58
|
|
|
59
|
+
from .exec_user_auth_interceptor import shared_account_info
|
|
54
60
|
from .executor import Executor
|
|
55
61
|
|
|
56
62
|
|
|
57
63
|
class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
58
64
|
"""SuperExec API servicer."""
|
|
59
65
|
|
|
60
|
-
def __init__(
|
|
66
|
+
def __init__( # pylint: disable=R0913, R0917
|
|
61
67
|
self,
|
|
62
68
|
linkstate_factory: LinkStateFactory,
|
|
63
69
|
ffs_factory: FfsFactory,
|
|
70
|
+
objectstore_factory: ObjectStoreFactory,
|
|
64
71
|
executor: Executor,
|
|
65
72
|
auth_plugin: Optional[ExecAuthPlugin] = None,
|
|
66
73
|
) -> None:
|
|
67
74
|
self.linkstate_factory = linkstate_factory
|
|
68
75
|
self.ffs_factory = ffs_factory
|
|
76
|
+
self.objectstore_factory = objectstore_factory
|
|
69
77
|
self.executor = executor
|
|
70
78
|
self.executor.initialize(linkstate_factory, ffs_factory)
|
|
71
79
|
self.auth_plugin = auth_plugin
|
|
@@ -76,10 +84,12 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
76
84
|
"""Create run ID."""
|
|
77
85
|
log(INFO, "ExecServicer.StartRun")
|
|
78
86
|
|
|
87
|
+
flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
|
|
79
88
|
run_id = self.executor.start_run(
|
|
80
89
|
request.fab.content,
|
|
81
90
|
user_config_from_proto(request.override_config),
|
|
82
91
|
config_record_from_proto(request.federation_options),
|
|
92
|
+
flwr_aid,
|
|
83
93
|
)
|
|
84
94
|
|
|
85
95
|
if run_id is None:
|
|
@@ -95,12 +105,20 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
95
105
|
log(INFO, "ExecServicer.StreamLogs")
|
|
96
106
|
state = self.linkstate_factory.state()
|
|
97
107
|
|
|
98
|
-
# Retrieve run ID
|
|
108
|
+
# Retrieve run ID and run
|
|
99
109
|
run_id = request.run_id
|
|
110
|
+
run = state.get_run(run_id)
|
|
100
111
|
|
|
101
112
|
# Exit if `run_id` not found
|
|
102
|
-
if not
|
|
103
|
-
context.abort(grpc.StatusCode.NOT_FOUND,
|
|
113
|
+
if not run:
|
|
114
|
+
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
115
|
+
|
|
116
|
+
# If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
|
|
117
|
+
if self.auth_plugin:
|
|
118
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
119
|
+
_check_flwr_aid_in_run(
|
|
120
|
+
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
121
|
+
)
|
|
104
122
|
|
|
105
123
|
after_timestamp = request.after_timestamp + 1e-6
|
|
106
124
|
while context.is_active():
|
|
@@ -119,7 +137,10 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
119
137
|
# is returned at this point and the server ends the stream.
|
|
120
138
|
run_status = state.get_run_status({run_id})[run_id]
|
|
121
139
|
if run_status.status == Status.FINISHED:
|
|
122
|
-
log(INFO, "All logs for run ID `%s` returned",
|
|
140
|
+
log(INFO, "All logs for run ID `%s` returned", run_id)
|
|
141
|
+
|
|
142
|
+
# Delete objects of the run from the object store
|
|
143
|
+
self.objectstore_factory.store().delete_objects_in_run(run_id)
|
|
123
144
|
break
|
|
124
145
|
|
|
125
146
|
time.sleep(LOG_STREAM_INTERVAL) # Sleep briefly to avoid busy waiting
|
|
@@ -131,11 +152,44 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
131
152
|
log(INFO, "ExecServicer.List")
|
|
132
153
|
state = self.linkstate_factory.state()
|
|
133
154
|
|
|
134
|
-
#
|
|
155
|
+
# Build a set of run IDs for `flwr ls --runs`
|
|
135
156
|
if not request.HasField("run_id"):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
if self.auth_plugin:
|
|
158
|
+
# If no `run_id` is specified and user auth is enabled,
|
|
159
|
+
# return run IDs for the authenticated user
|
|
160
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
161
|
+
if flwr_aid is None:
|
|
162
|
+
context.abort(
|
|
163
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
164
|
+
"️⛔️ User authentication is enabled, but `flwr_aid` is None",
|
|
165
|
+
)
|
|
166
|
+
run_ids = state.get_run_ids(flwr_aid=flwr_aid)
|
|
167
|
+
else:
|
|
168
|
+
# If no `run_id` is specified and no user auth is enabled,
|
|
169
|
+
# return all run IDs
|
|
170
|
+
run_ids = state.get_run_ids(None)
|
|
171
|
+
# Build a set of run IDs for `flwr ls --run-id <run_id>`
|
|
172
|
+
else:
|
|
173
|
+
# Retrieve run ID and run
|
|
174
|
+
run_id = request.run_id
|
|
175
|
+
run = state.get_run(run_id)
|
|
176
|
+
|
|
177
|
+
# Exit if `run_id` not found
|
|
178
|
+
if not run:
|
|
179
|
+
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
180
|
+
|
|
181
|
+
# If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
|
|
182
|
+
if self.auth_plugin:
|
|
183
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
184
|
+
_check_flwr_aid_in_run(
|
|
185
|
+
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
run_ids = {run_id}
|
|
189
|
+
|
|
190
|
+
# Init the object store
|
|
191
|
+
store = self.objectstore_factory.store()
|
|
192
|
+
return _create_list_runs_response(run_ids, state, store)
|
|
139
193
|
|
|
140
194
|
def StopRun(
|
|
141
195
|
self, request: StopRunRequest, context: grpc.ServicerContext
|
|
@@ -144,30 +198,42 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
144
198
|
log(INFO, "ExecServicer.StopRun")
|
|
145
199
|
state = self.linkstate_factory.state()
|
|
146
200
|
|
|
201
|
+
# Retrieve run ID and run
|
|
202
|
+
run_id = request.run_id
|
|
203
|
+
run = state.get_run(run_id)
|
|
204
|
+
|
|
147
205
|
# Exit if `run_id` not found
|
|
148
|
-
if not
|
|
149
|
-
context.abort(
|
|
150
|
-
|
|
206
|
+
if not run:
|
|
207
|
+
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
208
|
+
|
|
209
|
+
# If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
|
|
210
|
+
if self.auth_plugin:
|
|
211
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
212
|
+
_check_flwr_aid_in_run(
|
|
213
|
+
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
151
214
|
)
|
|
152
215
|
|
|
153
|
-
run_status = state.get_run_status({
|
|
216
|
+
run_status = state.get_run_status({run_id})[run_id]
|
|
154
217
|
if run_status.status == Status.FINISHED:
|
|
155
218
|
context.abort(
|
|
156
219
|
grpc.StatusCode.FAILED_PRECONDITION,
|
|
157
|
-
f"Run ID {
|
|
220
|
+
f"Run ID {run_id} is already finished",
|
|
158
221
|
)
|
|
159
222
|
|
|
160
223
|
update_success = state.update_run_status(
|
|
161
|
-
run_id=
|
|
224
|
+
run_id=run_id,
|
|
162
225
|
new_status=RunStatus(Status.FINISHED, SubStatus.STOPPED, ""),
|
|
163
226
|
)
|
|
164
227
|
|
|
165
228
|
if update_success:
|
|
166
|
-
message_ids: set[
|
|
229
|
+
message_ids: set[str] = state.get_message_ids_from_run_id(run_id)
|
|
167
230
|
|
|
168
231
|
# Delete Messages and their replies for the `run_id`
|
|
169
232
|
state.delete_messages(message_ids)
|
|
170
233
|
|
|
234
|
+
# Delete objects of the run from the object store
|
|
235
|
+
self.objectstore_factory.store().delete_objects_in_run(run_id)
|
|
236
|
+
|
|
171
237
|
return StopRunResponse(success=update_success)
|
|
172
238
|
|
|
173
239
|
def GetLoginDetails(
|
|
@@ -222,10 +288,46 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
222
288
|
)
|
|
223
289
|
|
|
224
290
|
|
|
225
|
-
def _create_list_runs_response(
|
|
291
|
+
def _create_list_runs_response(
|
|
292
|
+
run_ids: set[int], state: LinkState, store: ObjectStore
|
|
293
|
+
) -> ListRunsResponse:
|
|
226
294
|
"""Create response for `flwr ls --runs` and `flwr ls --run-id <run_id>`."""
|
|
227
|
-
run_dict = {run_id:
|
|
295
|
+
run_dict = {run_id: run for run_id in run_ids if (run := state.get_run(run_id))}
|
|
296
|
+
|
|
297
|
+
# Delete objects of finished runs from the object store
|
|
298
|
+
for run_id, run in run_dict.items():
|
|
299
|
+
if run.status.status == Status.FINISHED:
|
|
300
|
+
store.delete_objects_in_run(run_id)
|
|
301
|
+
|
|
228
302
|
return ListRunsResponse(
|
|
229
|
-
run_dict={run_id: run_to_proto(run) for run_id, run in run_dict.items()
|
|
303
|
+
run_dict={run_id: run_to_proto(run) for run_id, run in run_dict.items()},
|
|
230
304
|
now=now().isoformat(),
|
|
231
305
|
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _check_flwr_aid_in_run(
|
|
309
|
+
flwr_aid: Optional[str], run: Run, context: grpc.ServicerContext
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Guard clause to check if `flwr_aid` matches the run's `flwr_aid`."""
|
|
312
|
+
# `flwr_aid` must not be None. Abort if it is None.
|
|
313
|
+
if flwr_aid is None:
|
|
314
|
+
context.abort(
|
|
315
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
316
|
+
"️⛔️ User authentication is enabled, but `flwr_aid` is None",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# `run.flwr_aid` must not be an empty string. Abort if it is empty.
|
|
320
|
+
run_flwr_aid = run.flwr_aid
|
|
321
|
+
if not run_flwr_aid:
|
|
322
|
+
context.abort(
|
|
323
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
324
|
+
"⛔️ User authentication is enabled, but the run is not associated "
|
|
325
|
+
"with a `flwr_aid`.",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Exit if `flwr_aid` does not match the run's `flwr_aid`
|
|
329
|
+
if run_flwr_aid != flwr_aid:
|
|
330
|
+
context.abort(
|
|
331
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
332
|
+
"⛔️ Run ID does not belong to the user",
|
|
333
|
+
)
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import contextvars
|
|
19
|
-
from typing import Any, Callable, Union
|
|
19
|
+
from typing import Any, Callable, Union
|
|
20
20
|
|
|
21
21
|
import grpc
|
|
22
22
|
|
|
23
|
-
from flwr.common.auth_plugin import ExecAuthPlugin
|
|
24
|
-
from flwr.common.typing import
|
|
23
|
+
from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
|
|
24
|
+
from flwr.common.typing import AccountInfo
|
|
25
25
|
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
26
26
|
GetAuthTokensRequest,
|
|
27
27
|
GetAuthTokensResponse,
|
|
@@ -45,8 +45,8 @@ Response = Union[
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
"
|
|
48
|
+
shared_account_info: contextvars.ContextVar[AccountInfo] = contextvars.ContextVar(
|
|
49
|
+
"account_info", default=AccountInfo(flwr_aid=None, account_name=None)
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
@@ -56,8 +56,10 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
56
56
|
def __init__(
|
|
57
57
|
self,
|
|
58
58
|
auth_plugin: ExecAuthPlugin,
|
|
59
|
+
authz_plugin: ExecAuthzPlugin,
|
|
59
60
|
):
|
|
60
61
|
self.auth_plugin = auth_plugin
|
|
62
|
+
self.authz_plugin = authz_plugin
|
|
61
63
|
|
|
62
64
|
def intercept_service(
|
|
63
65
|
self,
|
|
@@ -91,17 +93,44 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
91
93
|
return call(request, context) # type: ignore
|
|
92
94
|
|
|
93
95
|
# For other requests, check if the user is authenticated
|
|
94
|
-
valid_tokens,
|
|
96
|
+
valid_tokens, account_info = self.auth_plugin.validate_tokens_in_metadata(
|
|
95
97
|
metadata
|
|
96
98
|
)
|
|
97
99
|
if valid_tokens:
|
|
100
|
+
if account_info is None:
|
|
101
|
+
context.abort(
|
|
102
|
+
grpc.StatusCode.UNAUTHENTICATED,
|
|
103
|
+
"Tokens validated, but user info not found",
|
|
104
|
+
)
|
|
105
|
+
raise grpc.RpcError()
|
|
98
106
|
# Store user info in contextvars for authenticated users
|
|
99
|
-
|
|
107
|
+
shared_account_info.set(account_info)
|
|
108
|
+
# Check if the user is authorized
|
|
109
|
+
if not self.authz_plugin.verify_user_authorization(account_info):
|
|
110
|
+
context.abort(
|
|
111
|
+
grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
|
|
112
|
+
)
|
|
113
|
+
raise grpc.RpcError()
|
|
100
114
|
return call(request, context) # type: ignore
|
|
101
115
|
|
|
102
116
|
# If the user is not authenticated, refresh tokens
|
|
103
|
-
tokens = self.auth_plugin.refresh_tokens(
|
|
117
|
+
tokens, account_info = self.auth_plugin.refresh_tokens(metadata)
|
|
104
118
|
if tokens is not None:
|
|
119
|
+
if account_info is None:
|
|
120
|
+
context.abort(
|
|
121
|
+
grpc.StatusCode.UNAUTHENTICATED,
|
|
122
|
+
"Tokens refreshed, but user info not found",
|
|
123
|
+
)
|
|
124
|
+
raise grpc.RpcError()
|
|
125
|
+
# Store user info in contextvars for authenticated users
|
|
126
|
+
shared_account_info.set(account_info)
|
|
127
|
+
# Check if the user is authorized
|
|
128
|
+
if not self.authz_plugin.verify_user_authorization(account_info):
|
|
129
|
+
context.abort(
|
|
130
|
+
grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
|
|
131
|
+
)
|
|
132
|
+
raise grpc.RpcError()
|
|
133
|
+
|
|
105
134
|
context.send_initial_metadata(tokens)
|
|
106
135
|
return call(request, context) # type: ignore
|
|
107
136
|
|
flwr/superexec/executor.py
CHANGED
|
@@ -74,6 +74,7 @@ class Executor(ABC):
|
|
|
74
74
|
fab_file: bytes,
|
|
75
75
|
override_config: UserConfig,
|
|
76
76
|
federation_options: ConfigRecord,
|
|
77
|
+
flwr_aid: Optional[str],
|
|
77
78
|
) -> Optional[int]:
|
|
78
79
|
"""Start a run using the given Flower FAB ID and version.
|
|
79
80
|
|
|
@@ -88,6 +89,9 @@ class Executor(ABC):
|
|
|
88
89
|
The config overrides dict sent by the user (using `flwr run`).
|
|
89
90
|
federation_options: ConfigRecord
|
|
90
91
|
The federation options sent by the user (using `flwr run`).
|
|
92
|
+
flwr_aid : Optional[str]
|
|
93
|
+
The Flower Account ID of the user starting the run, if authentication is
|
|
94
|
+
enabled.
|
|
91
95
|
|
|
92
96
|
Returns
|
|
93
97
|
-------
|
flwr/superexec/simulation.py
CHANGED
|
@@ -77,6 +77,7 @@ class SimulationEngine(Executor):
|
|
|
77
77
|
fab_file: bytes,
|
|
78
78
|
override_config: UserConfig,
|
|
79
79
|
federation_options: ConfigRecord,
|
|
80
|
+
flwr_aid: Optional[str],
|
|
80
81
|
) -> Optional[int]:
|
|
81
82
|
"""Start run using the Flower Simulation Engine."""
|
|
82
83
|
try:
|
|
@@ -96,7 +97,12 @@ class SimulationEngine(Executor):
|
|
|
96
97
|
fab_id, fab_version = get_fab_metadata(fab.content)
|
|
97
98
|
|
|
98
99
|
run_id = self.linkstate.create_run(
|
|
99
|
-
fab_id,
|
|
100
|
+
fab_id,
|
|
101
|
+
fab_version,
|
|
102
|
+
fab_hash,
|
|
103
|
+
override_config,
|
|
104
|
+
federation_options,
|
|
105
|
+
flwr_aid,
|
|
100
106
|
)
|
|
101
107
|
|
|
102
108
|
# Create an empty context for the Run
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower SuperLink."""
|
|
@@ -12,20 +12,13 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""
|
|
15
|
+
"""Flower command line interface for SuperNode."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
from
|
|
18
|
+
from .flower_supernode import flower_supernode
|
|
19
|
+
from .flwr_clientapp import flwr_clientapp
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
""
|
|
24
|
-
|
|
25
|
-
@abc.abstractmethod
|
|
26
|
-
def set_node_id(self, node_id: Optional[int]) -> None:
|
|
27
|
-
"""Set the node ID."""
|
|
28
|
-
|
|
29
|
-
@abc.abstractmethod
|
|
30
|
-
def get_node_id(self) -> int:
|
|
31
|
-
"""Get the node ID."""
|
|
21
|
+
__all__ = [
|
|
22
|
+
"flower_supernode",
|
|
23
|
+
"flwr_clientapp",
|
|
24
|
+
]
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""
|
|
15
|
+
"""`flower-supernode` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
@@ -42,12 +42,10 @@ from flwr.common.constant import (
|
|
|
42
42
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
43
43
|
from flwr.common.exit_handlers import register_exit_handlers
|
|
44
44
|
from flwr.common.logger import log
|
|
45
|
+
from flwr.supernode.start_client_internal import start_client_internal
|
|
45
46
|
|
|
46
|
-
from ..app import start_client_internal
|
|
47
|
-
from ..clientapp.utils import get_load_client_app_fn
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
def run_supernode() -> None:
|
|
48
|
+
def flower_supernode() -> None:
|
|
51
49
|
"""Run Flower SuperNode."""
|
|
52
50
|
args = _parse_args_run_supernode().parse_args()
|
|
53
51
|
|
|
@@ -64,12 +62,6 @@ def run_supernode() -> None:
|
|
|
64
62
|
)
|
|
65
63
|
|
|
66
64
|
root_certificates = try_obtain_root_certificates(args, args.superlink)
|
|
67
|
-
load_fn = get_load_client_app_fn(
|
|
68
|
-
default_app_ref="",
|
|
69
|
-
app_path=None,
|
|
70
|
-
flwr_dir=args.flwr_dir,
|
|
71
|
-
multi_app=True,
|
|
72
|
-
)
|
|
73
65
|
authentication_keys = _try_setup_client_authentication(args)
|
|
74
66
|
|
|
75
67
|
log(DEBUG, "Isolation mode: %s", args.isolation)
|
|
@@ -82,7 +74,6 @@ def run_supernode() -> None:
|
|
|
82
74
|
|
|
83
75
|
start_client_internal(
|
|
84
76
|
server_address=args.superlink,
|
|
85
|
-
load_client_app_fn=load_fn,
|
|
86
77
|
transport=args.transport,
|
|
87
78
|
root_certificates=root_certificates,
|
|
88
79
|
insecure=args.insecure,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""`flwr-clientapp` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
from logging import DEBUG, INFO
|
|
20
|
+
|
|
21
|
+
from flwr.common.args import add_args_flwr_app_common
|
|
22
|
+
from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
|
|
23
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
|
24
|
+
from flwr.common.logger import log
|
|
25
|
+
from flwr.supernode.runtime.run_clientapp import run_clientapp
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def flwr_clientapp() -> None:
|
|
29
|
+
"""Run process-isolated Flower ClientApp."""
|
|
30
|
+
args = _parse_args_run_flwr_clientapp().parse_args()
|
|
31
|
+
if not args.insecure:
|
|
32
|
+
flwr_exit(
|
|
33
|
+
ExitCode.COMMON_TLS_NOT_SUPPORTED,
|
|
34
|
+
"flwr-clientapp does not support TLS yet.",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
log(INFO, "Start `flwr-clientapp` process")
|
|
38
|
+
log(
|
|
39
|
+
DEBUG,
|
|
40
|
+
"`flwr-clientapp` will attempt to connect to SuperNode's "
|
|
41
|
+
"ClientAppIo API at %s with token %s",
|
|
42
|
+
args.clientappio_api_address,
|
|
43
|
+
args.token,
|
|
44
|
+
)
|
|
45
|
+
run_clientapp(
|
|
46
|
+
clientappio_api_address=args.clientappio_api_address,
|
|
47
|
+
run_once=(args.token is not None),
|
|
48
|
+
token=args.token,
|
|
49
|
+
flwr_dir=args.flwr_dir,
|
|
50
|
+
certificates=None,
|
|
51
|
+
parent_pid=args.parent_pid,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
|
|
56
|
+
"""Parse flwr-clientapp command line arguments."""
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
description="Run a Flower ClientApp",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--clientappio-api-address",
|
|
62
|
+
default=CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS,
|
|
63
|
+
type=str,
|
|
64
|
+
help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
|
|
65
|
+
f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--token",
|
|
69
|
+
type=int,
|
|
70
|
+
required=False,
|
|
71
|
+
help="Unique token generated by SuperNode for each ClientApp execution",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--parent-pid",
|
|
75
|
+
type=int,
|
|
76
|
+
default=None,
|
|
77
|
+
help="The PID of the parent process. When set, the process will terminate "
|
|
78
|
+
"when the parent process exits.",
|
|
79
|
+
)
|
|
80
|
+
add_args_flwr_app_common(parser=parser)
|
|
81
|
+
return parser
|