flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.15.0.dev20250115__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (312) hide show
  1. flwr/cli/app.py +16 -2
  2. flwr/cli/build.py +181 -0
  3. flwr/cli/cli_user_auth_interceptor.py +90 -0
  4. flwr/cli/config_utils.py +343 -0
  5. flwr/cli/example.py +4 -1
  6. flwr/cli/install.py +253 -0
  7. flwr/cli/log.py +182 -0
  8. flwr/{server/superlink/state → cli/login}/__init__.py +4 -10
  9. flwr/cli/login/login.py +88 -0
  10. flwr/cli/ls.py +327 -0
  11. flwr/cli/new/__init__.py +1 -0
  12. flwr/cli/new/new.py +210 -66
  13. flwr/cli/new/templates/app/.gitignore.tpl +163 -0
  14. flwr/cli/new/templates/app/LICENSE.tpl +202 -0
  15. flwr/cli/new/templates/app/README.baseline.md.tpl +127 -0
  16. flwr/cli/new/templates/app/README.flowertune.md.tpl +66 -0
  17. flwr/cli/new/templates/app/README.md.tpl +16 -32
  18. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +1 -0
  19. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  20. flwr/cli/new/templates/app/code/client.baseline.py.tpl +58 -0
  21. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +55 -0
  22. flwr/cli/new/templates/app/code/client.jax.py.tpl +50 -0
  23. flwr/cli/new/templates/app/code/client.mlx.py.tpl +73 -0
  24. flwr/cli/new/templates/app/code/client.numpy.py.tpl +7 -7
  25. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +30 -21
  26. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +63 -0
  27. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +57 -1
  28. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +36 -0
  29. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  30. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +126 -0
  31. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +87 -0
  32. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +78 -0
  33. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +94 -0
  34. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +83 -0
  35. flwr/cli/new/templates/app/code/model.baseline.py.tpl +80 -0
  36. flwr/cli/new/templates/app/code/server.baseline.py.tpl +46 -0
  37. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +38 -0
  38. flwr/cli/new/templates/app/code/server.jax.py.tpl +26 -0
  39. flwr/cli/new/templates/app/code/server.mlx.py.tpl +31 -0
  40. flwr/cli/new/templates/app/code/server.numpy.py.tpl +22 -9
  41. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  42. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +36 -0
  43. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  44. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +1 -0
  45. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +102 -0
  46. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  47. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  48. flwr/cli/new/templates/app/code/task.numpy.py.tpl +7 -0
  49. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +29 -24
  50. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +67 -0
  51. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  52. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +1 -0
  53. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +138 -0
  54. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +68 -0
  55. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +46 -0
  56. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +35 -0
  57. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  58. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  59. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  60. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +35 -0
  61. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  62. flwr/cli/run/__init__.py +1 -0
  63. flwr/cli/run/run.py +212 -34
  64. flwr/cli/stop.py +130 -0
  65. flwr/cli/utils.py +240 -5
  66. flwr/client/__init__.py +3 -2
  67. flwr/client/app.py +432 -255
  68. flwr/client/client.py +1 -11
  69. flwr/client/client_app.py +74 -13
  70. flwr/client/clientapp/__init__.py +22 -0
  71. flwr/client/clientapp/app.py +259 -0
  72. flwr/client/clientapp/clientappio_servicer.py +244 -0
  73. flwr/client/clientapp/utils.py +115 -0
  74. flwr/client/dpfedavg_numpy_client.py +7 -8
  75. flwr/client/grpc_adapter_client/__init__.py +15 -0
  76. flwr/client/grpc_adapter_client/connection.py +98 -0
  77. flwr/client/grpc_client/connection.py +21 -7
  78. flwr/client/grpc_rere_client/__init__.py +1 -1
  79. flwr/client/grpc_rere_client/client_interceptor.py +176 -0
  80. flwr/client/grpc_rere_client/connection.py +163 -56
  81. flwr/client/grpc_rere_client/grpc_adapter.py +167 -0
  82. flwr/client/heartbeat.py +74 -0
  83. flwr/client/message_handler/__init__.py +1 -1
  84. flwr/client/message_handler/message_handler.py +10 -11
  85. flwr/client/mod/__init__.py +5 -5
  86. flwr/client/mod/centraldp_mods.py +4 -2
  87. flwr/client/mod/comms_mods.py +5 -4
  88. flwr/client/mod/localdp_mod.py +10 -5
  89. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  90. flwr/client/mod/secure_aggregation/secaggplus_mod.py +26 -26
  91. flwr/client/mod/utils.py +2 -4
  92. flwr/client/nodestate/__init__.py +26 -0
  93. flwr/client/nodestate/in_memory_nodestate.py +38 -0
  94. flwr/client/nodestate/nodestate.py +31 -0
  95. flwr/client/nodestate/nodestate_factory.py +38 -0
  96. flwr/client/numpy_client.py +8 -31
  97. flwr/client/rest_client/__init__.py +1 -1
  98. flwr/client/rest_client/connection.py +199 -176
  99. flwr/client/run_info_store.py +112 -0
  100. flwr/client/supernode/__init__.py +24 -0
  101. flwr/client/supernode/app.py +321 -0
  102. flwr/client/typing.py +1 -0
  103. flwr/common/__init__.py +17 -11
  104. flwr/common/address.py +47 -3
  105. flwr/common/args.py +153 -0
  106. flwr/common/auth_plugin/__init__.py +24 -0
  107. flwr/common/auth_plugin/auth_plugin.py +121 -0
  108. flwr/common/config.py +243 -0
  109. flwr/common/constant.py +135 -1
  110. flwr/common/context.py +32 -2
  111. flwr/common/date.py +22 -4
  112. flwr/common/differential_privacy.py +2 -2
  113. flwr/common/dp.py +2 -4
  114. flwr/common/exit_handlers.py +3 -3
  115. flwr/common/grpc.py +164 -5
  116. flwr/common/logger.py +230 -12
  117. flwr/common/message.py +191 -106
  118. flwr/common/object_ref.py +179 -44
  119. flwr/common/pyproject.py +1 -0
  120. flwr/common/record/__init__.py +2 -1
  121. flwr/common/record/configsrecord.py +58 -18
  122. flwr/common/record/metricsrecord.py +57 -17
  123. flwr/common/record/parametersrecord.py +88 -20
  124. flwr/common/record/recordset.py +153 -30
  125. flwr/common/record/typeddict.py +30 -55
  126. flwr/common/recordset_compat.py +31 -12
  127. flwr/common/retry_invoker.py +123 -30
  128. flwr/common/secure_aggregation/__init__.py +1 -1
  129. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  130. flwr/common/secure_aggregation/crypto/shamir.py +11 -11
  131. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +68 -4
  132. flwr/common/secure_aggregation/ndarrays_arithmetic.py +17 -17
  133. flwr/common/secure_aggregation/quantization.py +8 -8
  134. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  135. flwr/common/secure_aggregation/secaggplus_utils.py +10 -12
  136. flwr/common/serde.py +304 -23
  137. flwr/common/telemetry.py +65 -29
  138. flwr/common/typing.py +120 -19
  139. flwr/common/version.py +17 -3
  140. flwr/proto/clientappio_pb2.py +45 -0
  141. flwr/proto/clientappio_pb2.pyi +132 -0
  142. flwr/proto/clientappio_pb2_grpc.py +135 -0
  143. flwr/proto/clientappio_pb2_grpc.pyi +53 -0
  144. flwr/proto/exec_pb2.py +62 -0
  145. flwr/proto/exec_pb2.pyi +212 -0
  146. flwr/proto/exec_pb2_grpc.py +237 -0
  147. flwr/proto/exec_pb2_grpc.pyi +93 -0
  148. flwr/proto/fab_pb2.py +31 -0
  149. flwr/proto/fab_pb2.pyi +65 -0
  150. flwr/proto/fab_pb2_grpc.py +4 -0
  151. flwr/proto/fab_pb2_grpc.pyi +4 -0
  152. flwr/proto/fleet_pb2.py +42 -23
  153. flwr/proto/fleet_pb2.pyi +123 -1
  154. flwr/proto/fleet_pb2_grpc.py +170 -0
  155. flwr/proto/fleet_pb2_grpc.pyi +61 -0
  156. flwr/proto/grpcadapter_pb2.py +32 -0
  157. flwr/proto/grpcadapter_pb2.pyi +43 -0
  158. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  159. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  160. flwr/proto/log_pb2.py +29 -0
  161. flwr/proto/log_pb2.pyi +39 -0
  162. flwr/proto/log_pb2_grpc.py +4 -0
  163. flwr/proto/log_pb2_grpc.pyi +4 -0
  164. flwr/proto/message_pb2.py +41 -0
  165. flwr/proto/message_pb2.pyi +128 -0
  166. flwr/proto/message_pb2_grpc.py +4 -0
  167. flwr/proto/message_pb2_grpc.pyi +4 -0
  168. flwr/proto/node_pb2.py +2 -2
  169. flwr/proto/node_pb2.pyi +1 -4
  170. flwr/proto/recordset_pb2.py +35 -33
  171. flwr/proto/recordset_pb2.pyi +40 -14
  172. flwr/proto/run_pb2.py +64 -0
  173. flwr/proto/run_pb2.pyi +268 -0
  174. flwr/proto/run_pb2_grpc.py +4 -0
  175. flwr/proto/run_pb2_grpc.pyi +4 -0
  176. flwr/proto/serverappio_pb2.py +52 -0
  177. flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +62 -20
  178. flwr/proto/serverappio_pb2_grpc.py +410 -0
  179. flwr/proto/serverappio_pb2_grpc.pyi +160 -0
  180. flwr/proto/simulationio_pb2.py +38 -0
  181. flwr/proto/simulationio_pb2.pyi +65 -0
  182. flwr/proto/simulationio_pb2_grpc.py +239 -0
  183. flwr/proto/simulationio_pb2_grpc.pyi +94 -0
  184. flwr/proto/task_pb2.py +7 -8
  185. flwr/proto/task_pb2.pyi +8 -5
  186. flwr/proto/transport_pb2.py +8 -8
  187. flwr/proto/transport_pb2.pyi +9 -6
  188. flwr/server/__init__.py +2 -10
  189. flwr/server/app.py +579 -402
  190. flwr/server/client_manager.py +8 -6
  191. flwr/server/compat/app.py +6 -62
  192. flwr/server/compat/app_utils.py +14 -9
  193. flwr/server/compat/driver_client_proxy.py +25 -59
  194. flwr/server/compat/legacy_context.py +5 -4
  195. flwr/server/driver/__init__.py +2 -0
  196. flwr/server/driver/driver.py +36 -131
  197. flwr/server/driver/grpc_driver.py +220 -81
  198. flwr/server/driver/inmemory_driver.py +183 -0
  199. flwr/server/history.py +28 -29
  200. flwr/server/run_serverapp.py +15 -126
  201. flwr/server/server.py +50 -44
  202. flwr/server/server_app.py +59 -10
  203. flwr/server/serverapp/__init__.py +22 -0
  204. flwr/server/serverapp/app.py +256 -0
  205. flwr/server/serverapp_components.py +52 -0
  206. flwr/server/strategy/__init__.py +2 -2
  207. flwr/server/strategy/aggregate.py +37 -23
  208. flwr/server/strategy/bulyan.py +9 -9
  209. flwr/server/strategy/dp_adaptive_clipping.py +25 -25
  210. flwr/server/strategy/dp_fixed_clipping.py +23 -22
  211. flwr/server/strategy/dpfedavg_adaptive.py +8 -8
  212. flwr/server/strategy/dpfedavg_fixed.py +13 -12
  213. flwr/server/strategy/fault_tolerant_fedavg.py +11 -11
  214. flwr/server/strategy/fedadagrad.py +9 -9
  215. flwr/server/strategy/fedadam.py +20 -10
  216. flwr/server/strategy/fedavg.py +16 -16
  217. flwr/server/strategy/fedavg_android.py +17 -17
  218. flwr/server/strategy/fedavgm.py +9 -9
  219. flwr/server/strategy/fedmedian.py +5 -5
  220. flwr/server/strategy/fedopt.py +6 -6
  221. flwr/server/strategy/fedprox.py +7 -7
  222. flwr/server/strategy/fedtrimmedavg.py +8 -8
  223. flwr/server/strategy/fedxgb_bagging.py +12 -12
  224. flwr/server/strategy/fedxgb_cyclic.py +10 -10
  225. flwr/server/strategy/fedxgb_nn_avg.py +6 -6
  226. flwr/server/strategy/fedyogi.py +9 -9
  227. flwr/server/strategy/krum.py +9 -9
  228. flwr/server/strategy/qfedavg.py +16 -16
  229. flwr/server/strategy/strategy.py +10 -10
  230. flwr/server/superlink/driver/__init__.py +2 -2
  231. flwr/server/superlink/driver/serverappio_grpc.py +61 -0
  232. flwr/server/superlink/driver/serverappio_servicer.py +361 -0
  233. flwr/server/superlink/ffs/__init__.py +24 -0
  234. flwr/server/superlink/ffs/disk_ffs.py +108 -0
  235. flwr/server/superlink/ffs/ffs.py +79 -0
  236. flwr/server/superlink/ffs/ffs_factory.py +47 -0
  237. flwr/server/superlink/fleet/__init__.py +1 -1
  238. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  239. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +162 -0
  240. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  241. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +4 -2
  242. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -2
  243. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  244. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -154
  245. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  246. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +120 -13
  247. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +228 -0
  248. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  249. flwr/server/superlink/fleet/message_handler/message_handler.py +156 -13
  250. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  251. flwr/server/superlink/fleet/rest_rere/rest_api.py +119 -81
  252. flwr/server/superlink/fleet/vce/__init__.py +1 -0
  253. flwr/server/superlink/fleet/vce/backend/__init__.py +4 -4
  254. flwr/server/superlink/fleet/vce/backend/backend.py +8 -9
  255. flwr/server/superlink/fleet/vce/backend/raybackend.py +87 -68
  256. flwr/server/superlink/fleet/vce/vce_api.py +208 -146
  257. flwr/server/superlink/linkstate/__init__.py +28 -0
  258. flwr/server/superlink/linkstate/in_memory_linkstate.py +569 -0
  259. flwr/server/superlink/linkstate/linkstate.py +376 -0
  260. flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +19 -10
  261. flwr/server/superlink/linkstate/sqlite_linkstate.py +1196 -0
  262. flwr/server/superlink/linkstate/utils.py +399 -0
  263. flwr/server/superlink/simulation/__init__.py +15 -0
  264. flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
  265. flwr/server/superlink/simulation/simulationio_servicer.py +186 -0
  266. flwr/server/superlink/utils.py +65 -0
  267. flwr/server/typing.py +2 -0
  268. flwr/server/utils/__init__.py +1 -1
  269. flwr/server/utils/tensorboard.py +5 -5
  270. flwr/server/utils/validator.py +40 -45
  271. flwr/server/workflow/default_workflows.py +70 -26
  272. flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -0
  273. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +40 -27
  274. flwr/simulation/__init__.py +12 -5
  275. flwr/simulation/app.py +247 -315
  276. flwr/simulation/legacy_app.py +404 -0
  277. flwr/simulation/ray_transport/__init__.py +1 -1
  278. flwr/simulation/ray_transport/ray_actor.py +42 -67
  279. flwr/simulation/ray_transport/ray_client_proxy.py +37 -17
  280. flwr/simulation/ray_transport/utils.py +1 -0
  281. flwr/simulation/run_simulation.py +306 -163
  282. flwr/simulation/simulationio_connection.py +89 -0
  283. flwr/superexec/__init__.py +15 -0
  284. flwr/superexec/app.py +59 -0
  285. flwr/superexec/deployment.py +188 -0
  286. flwr/superexec/exec_grpc.py +80 -0
  287. flwr/superexec/exec_servicer.py +231 -0
  288. flwr/superexec/exec_user_auth_interceptor.py +101 -0
  289. flwr/superexec/executor.py +96 -0
  290. flwr/superexec/simulation.py +124 -0
  291. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/METADATA +33 -26
  292. flwr_nightly-1.15.0.dev20250115.dist-info/RECORD +328 -0
  293. flwr_nightly-1.15.0.dev20250115.dist-info/entry_points.txt +12 -0
  294. flwr/cli/flower_toml.py +0 -140
  295. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  296. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  297. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  298. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  299. flwr/client/node_state.py +0 -48
  300. flwr/client/node_state_tests.py +0 -65
  301. flwr/proto/driver_pb2.py +0 -44
  302. flwr/proto/driver_pb2_grpc.py +0 -169
  303. flwr/proto/driver_pb2_grpc.pyi +0 -66
  304. flwr/server/superlink/driver/driver_grpc.py +0 -54
  305. flwr/server/superlink/driver/driver_servicer.py +0 -129
  306. flwr/server/superlink/state/in_memory_state.py +0 -230
  307. flwr/server/superlink/state/sqlite_state.py +0 -630
  308. flwr/server/superlink/state/state.py +0 -154
  309. flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
  310. flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
  311. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/LICENSE +0 -0
  312. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.15.0.dev20250115.dist-info}/WHEEL +0 -0
flwr/server/app.py CHANGED
@@ -14,66 +14,115 @@
14
14
  # ==============================================================================
15
15
  """Flower server app."""
16
16
 
17
+
17
18
  import argparse
18
- import asyncio
19
+ import csv
19
20
  import importlib.util
21
+ import multiprocessing
22
+ import multiprocessing.context
20
23
  import sys
21
24
  import threading
22
- from logging import ERROR, INFO, WARN
23
- from os.path import isfile
25
+ from collections.abc import Sequence
26
+ from logging import DEBUG, INFO, WARN
24
27
  from pathlib import Path
25
- from typing import List, Optional, Tuple
28
+ from time import sleep
29
+ from typing import Any, Optional
26
30
 
27
31
  import grpc
32
+ import yaml
33
+ from cryptography.exceptions import UnsupportedAlgorithm
34
+ from cryptography.hazmat.primitives.asymmetric import ec
35
+ from cryptography.hazmat.primitives.serialization import (
36
+ load_ssh_private_key,
37
+ load_ssh_public_key,
38
+ )
28
39
 
29
40
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, event
30
41
  from flwr.common.address import parse_address
42
+ from flwr.common.args import try_obtain_server_certificates
43
+ from flwr.common.auth_plugin import ExecAuthPlugin
44
+ from flwr.common.config import get_flwr_dir, parse_config_args
31
45
  from flwr.common.constant import (
46
+ AUTH_TYPE,
47
+ CLIENT_OCTET,
48
+ EXEC_API_DEFAULT_SERVER_ADDRESS,
49
+ FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS,
50
+ FLEET_API_GRPC_RERE_DEFAULT_ADDRESS,
51
+ FLEET_API_REST_DEFAULT_ADDRESS,
52
+ ISOLATION_MODE_PROCESS,
53
+ ISOLATION_MODE_SUBPROCESS,
32
54
  MISSING_EXTRA_REST,
55
+ SERVER_OCTET,
56
+ SERVERAPPIO_API_DEFAULT_SERVER_ADDRESS,
57
+ SIMULATIONIO_API_DEFAULT_SERVER_ADDRESS,
58
+ TRANSPORT_TYPE_GRPC_ADAPTER,
33
59
  TRANSPORT_TYPE_GRPC_RERE,
34
60
  TRANSPORT_TYPE_REST,
35
- TRANSPORT_TYPE_VCE,
36
61
  )
37
62
  from flwr.common.exit_handlers import register_exit_handlers
38
- from flwr.common.logger import log
63
+ from flwr.common.grpc import generic_create_grpc_server
64
+ from flwr.common.logger import log, warn_deprecated_feature
65
+ from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
66
+ private_key_to_bytes,
67
+ public_key_to_bytes,
68
+ )
39
69
  from flwr.proto.fleet_pb2_grpc import ( # pylint: disable=E0611
40
70
  add_FleetServicer_to_server,
41
71
  )
72
+ from flwr.proto.grpcadapter_pb2_grpc import add_GrpcAdapterServicer_to_server
73
+ from flwr.server.serverapp.app import flwr_serverapp
74
+ from flwr.simulation.app import flwr_simulation
75
+ from flwr.superexec.app import load_executor
76
+ from flwr.superexec.exec_grpc import run_exec_api_grpc
42
77
 
43
78
  from .client_manager import ClientManager
44
79
  from .history import History
45
80
  from .server import Server, init_defaults, run_fl
46
81
  from .server_config import ServerConfig
47
82
  from .strategy import Strategy
48
- from .superlink.driver.driver_grpc import run_driver_api_grpc
49
- from .superlink.fleet.grpc_bidi.grpc_server import (
50
- generic_create_grpc_server,
51
- start_grpc_server,
52
- )
83
+ from .superlink.driver.serverappio_grpc import run_serverappio_api_grpc
84
+ from .superlink.ffs.ffs_factory import FfsFactory
85
+ from .superlink.fleet.grpc_adapter.grpc_adapter_servicer import GrpcAdapterServicer
86
+ from .superlink.fleet.grpc_bidi.grpc_server import start_grpc_server
53
87
  from .superlink.fleet.grpc_rere.fleet_servicer import FleetServicer
54
- from .superlink.fleet.vce import start_vce
55
- from .superlink.state import StateFactory
56
-
57
- ADDRESS_DRIVER_API = "0.0.0.0:9091"
58
- ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092"
59
- ADDRESS_FLEET_API_GRPC_BIDI = "[::]:8080" # IPv6 to keep start_server compatible
60
- ADDRESS_FLEET_API_REST = "0.0.0.0:9093"
88
+ from .superlink.fleet.grpc_rere.server_interceptor import AuthenticateServerInterceptor
89
+ from .superlink.linkstate import LinkStateFactory
90
+ from .superlink.simulation.simulationio_grpc import run_simulationio_api_grpc
61
91
 
62
92
  DATABASE = ":flwr-in-memory-state:"
93
+ BASE_DIR = get_flwr_dir() / "superlink" / "ffs"
94
+
95
+
96
+ try:
97
+ from flwr.ee import add_ee_args_superlink, get_exec_auth_plugins
98
+ except ImportError:
99
+
100
+ # pylint: disable-next=unused-argument
101
+ def add_ee_args_superlink(parser: argparse.ArgumentParser) -> None:
102
+ """Add EE-specific arguments to the parser."""
103
+
104
+ def get_exec_auth_plugins() -> dict[str, type[ExecAuthPlugin]]:
105
+ """Return all Exec API authentication plugins."""
106
+ raise NotImplementedError("No authentication plugins are currently supported.")
63
107
 
64
108
 
65
109
  def start_server( # pylint: disable=too-many-arguments,too-many-locals
66
110
  *,
67
- server_address: str = ADDRESS_FLEET_API_GRPC_BIDI,
111
+ server_address: str = FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS,
68
112
  server: Optional[Server] = None,
69
113
  config: Optional[ServerConfig] = None,
70
114
  strategy: Optional[Strategy] = None,
71
115
  client_manager: Optional[ClientManager] = None,
72
116
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
73
- certificates: Optional[Tuple[bytes, bytes, bytes]] = None,
117
+ certificates: Optional[tuple[bytes, bytes, bytes]] = None,
74
118
  ) -> History:
75
119
  """Start a Flower server using the gRPC transport layer.
76
120
 
121
+ Warning
122
+ -------
123
+ This function is deprecated since 1.13.0. Use the :code:`flower-superlink` command
124
+ instead to start a SuperLink.
125
+
77
126
  Parameters
78
127
  ----------
79
128
  server_address : Optional[str]
@@ -131,6 +180,17 @@ def start_server( # pylint: disable=too-many-arguments,too-many-locals
131
180
  >>> )
132
181
  >>> )
133
182
  """
183
+ msg = (
184
+ "flwr.server.start_server() is deprecated."
185
+ "\n\tInstead, use the `flower-superlink` CLI command to start a SuperLink "
186
+ "as shown below:"
187
+ "\n\n\t\t$ flower-superlink --insecure"
188
+ "\n\n\tTo view usage and all available options, run:"
189
+ "\n\n\t\t$ flower-superlink --help"
190
+ "\n\n\tUsing `start_server()` is deprecated."
191
+ )
192
+ warn_deprecated_feature(name=msg)
193
+
134
194
  event(EventType.START_SERVER_ENTER)
135
195
 
136
196
  # Parse IP address
@@ -181,245 +241,407 @@ def start_server( # pylint: disable=too-many-arguments,too-many-locals
181
241
  return hist
182
242
 
183
243
 
184
- def run_driver_api() -> None:
185
- """Run Flower server (Driver API)."""
186
- log(INFO, "Starting Flower server (Driver API)")
187
- event(EventType.RUN_DRIVER_API_ENTER)
188
- args = _parse_args_run_driver_api().parse_args()
244
+ # pylint: disable=too-many-branches, too-many-locals, too-many-statements
245
+ def run_superlink() -> None:
246
+ """Run Flower SuperLink (ServerAppIo API and Fleet API)."""
247
+ args = _parse_args_run_superlink().parse_args()
189
248
 
190
- # Parse IP address
191
- parsed_address = parse_address(args.driver_api_address)
192
- if not parsed_address:
193
- sys.exit(f"Driver IP address ({args.driver_api_address}) cannot be parsed.")
194
- host, port, is_v6 = parsed_address
195
- address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
249
+ log(INFO, "Starting Flower SuperLink")
250
+
251
+ event(EventType.RUN_SUPERLINK_ENTER)
252
+
253
+ # Warn unused options
254
+ if args.flwr_dir is not None:
255
+ log(
256
+ WARN, "The `--flwr-dir` option is currently not in use and will be ignored."
257
+ )
258
+
259
+ # Parse IP addresses
260
+ serverappio_address, _, _ = _format_address(args.serverappio_api_address)
261
+ exec_address, _, _ = _format_address(args.exec_api_address)
262
+ simulationio_address, _, _ = _format_address(args.simulationio_api_address)
196
263
 
197
264
  # Obtain certificates
198
- certificates = _try_obtain_certificates(args)
265
+ certificates = try_obtain_server_certificates(args, args.fleet_api_type)
266
+
267
+ auth_plugin: Optional[ExecAuthPlugin] = None
268
+ # Load the auth plugin if the args.user_auth_config is provided
269
+ if cfg_path := getattr(args, "user_auth_config", None):
270
+ auth_plugin = _try_obtain_exec_auth_plugin(Path(cfg_path))
199
271
 
200
272
  # Initialize StateFactory
201
- state_factory = StateFactory(args.database)
273
+ state_factory = LinkStateFactory(args.database)
274
+
275
+ # Initialize FfsFactory
276
+ ffs_factory = FfsFactory(args.storage_dir)
202
277
 
203
- # Start server
204
- grpc_server: grpc.Server = run_driver_api_grpc(
205
- address=address,
278
+ # Start Exec API
279
+ executor = load_executor(args)
280
+ exec_server: grpc.Server = run_exec_api_grpc(
281
+ address=exec_address,
206
282
  state_factory=state_factory,
283
+ ffs_factory=ffs_factory,
284
+ executor=executor,
207
285
  certificates=certificates,
286
+ config=parse_config_args(
287
+ [args.executor_config] if args.executor_config else args.executor_config
288
+ ),
289
+ auth_plugin=auth_plugin,
208
290
  )
291
+ grpc_servers = [exec_server]
209
292
 
210
- # Graceful shutdown
211
- register_exit_handlers(
212
- event_type=EventType.RUN_DRIVER_API_LEAVE,
213
- grpc_servers=[grpc_server],
214
- bckg_threads=[],
215
- )
216
-
217
- # Block
218
- grpc_server.wait_for_termination()
293
+ # Determine Exec plugin
294
+ # If simulation is used, don't start ServerAppIo and Fleet APIs
295
+ sim_exec = executor.__class__.__qualname__ == "SimulationEngine"
296
+ bckg_threads: list[threading.Thread] = []
219
297
 
298
+ if sim_exec:
299
+ simulationio_server: grpc.Server = run_simulationio_api_grpc(
300
+ address=simulationio_address,
301
+ state_factory=state_factory,
302
+ ffs_factory=ffs_factory,
303
+ certificates=None, # SimulationAppIo API doesn't support SSL yet
304
+ )
305
+ grpc_servers.append(simulationio_server)
220
306
 
221
- def run_fleet_api() -> None:
222
- """Run Flower server (Fleet API)."""
223
- log(INFO, "Starting Flower server (Fleet API)")
224
- event(EventType.RUN_FLEET_API_ENTER)
225
- args = _parse_args_run_fleet_api().parse_args()
226
-
227
- # Obtain certificates
228
- certificates = _try_obtain_certificates(args)
307
+ else:
308
+ # Start ServerAppIo API
309
+ serverappio_server: grpc.Server = run_serverappio_api_grpc(
310
+ address=serverappio_address,
311
+ state_factory=state_factory,
312
+ ffs_factory=ffs_factory,
313
+ certificates=None, # ServerAppIo API doesn't support SSL yet
314
+ )
315
+ grpc_servers.append(serverappio_server)
316
+
317
+ # Start Fleet API
318
+ if not args.fleet_api_address:
319
+ if args.fleet_api_type in [
320
+ TRANSPORT_TYPE_GRPC_RERE,
321
+ TRANSPORT_TYPE_GRPC_ADAPTER,
322
+ ]:
323
+ args.fleet_api_address = FLEET_API_GRPC_RERE_DEFAULT_ADDRESS
324
+ elif args.fleet_api_type == TRANSPORT_TYPE_REST:
325
+ args.fleet_api_address = FLEET_API_REST_DEFAULT_ADDRESS
326
+
327
+ fleet_address, host, port = _format_address(args.fleet_api_address)
328
+
329
+ num_workers = args.fleet_api_num_workers
330
+ if num_workers != 1:
331
+ log(
332
+ WARN,
333
+ "The Fleet API currently supports only 1 worker. "
334
+ "You have specified %d workers. "
335
+ "Support for multiple workers will be added in future releases. "
336
+ "Proceeding with a single worker.",
337
+ args.fleet_api_num_workers,
338
+ )
339
+ num_workers = 1
340
+
341
+ if args.fleet_api_type == TRANSPORT_TYPE_REST:
342
+ if (
343
+ importlib.util.find_spec("requests")
344
+ and importlib.util.find_spec("starlette")
345
+ and importlib.util.find_spec("uvicorn")
346
+ ) is None:
347
+ sys.exit(MISSING_EXTRA_REST)
348
+
349
+ _, ssl_certfile, ssl_keyfile = (
350
+ certificates if certificates is not None else (None, None, None)
351
+ )
352
+
353
+ fleet_thread = threading.Thread(
354
+ target=_run_fleet_api_rest,
355
+ args=(
356
+ host,
357
+ port,
358
+ ssl_keyfile,
359
+ ssl_certfile,
360
+ state_factory,
361
+ ffs_factory,
362
+ num_workers,
363
+ ),
364
+ daemon=True,
365
+ )
366
+ fleet_thread.start()
367
+ bckg_threads.append(fleet_thread)
368
+ elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE:
369
+ maybe_keys = _try_setup_node_authentication(args, certificates)
370
+ interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None
371
+ if maybe_keys is not None:
372
+ (
373
+ node_public_keys,
374
+ server_private_key,
375
+ server_public_key,
376
+ ) = maybe_keys
377
+ state = state_factory.state()
378
+ state.clear_supernode_auth_keys_and_credentials()
379
+ state.store_node_public_keys(node_public_keys)
380
+ state.store_server_private_public_key(
381
+ private_key_to_bytes(server_private_key),
382
+ public_key_to_bytes(server_public_key),
383
+ )
384
+ log(
385
+ INFO,
386
+ "Node authentication enabled with %d known public keys",
387
+ len(node_public_keys),
388
+ )
389
+ interceptors = [AuthenticateServerInterceptor(state_factory)]
390
+
391
+ fleet_server = _run_fleet_api_grpc_rere(
392
+ address=fleet_address,
393
+ state_factory=state_factory,
394
+ ffs_factory=ffs_factory,
395
+ certificates=certificates,
396
+ interceptors=interceptors,
397
+ )
398
+ grpc_servers.append(fleet_server)
399
+ elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER:
400
+ fleet_server = _run_fleet_api_grpc_adapter(
401
+ address=fleet_address,
402
+ state_factory=state_factory,
403
+ ffs_factory=ffs_factory,
404
+ certificates=certificates,
405
+ )
406
+ grpc_servers.append(fleet_server)
407
+ else:
408
+ raise ValueError(f"Unknown fleet_api_type: {args.fleet_api_type}")
409
+
410
+ if args.isolation == ISOLATION_MODE_SUBPROCESS:
411
+
412
+ _octet, _colon, _port = serverappio_address.rpartition(":")
413
+ io_address = (
414
+ f"{CLIENT_OCTET}:{_port}" if _octet == SERVER_OCTET else serverappio_address
415
+ )
416
+ address_arg = (
417
+ "--simulationio-api-address" if sim_exec else "--serverappio-api-address"
418
+ )
419
+ address = simulationio_address if sim_exec else io_address
420
+ cmd = "flwr-simulation" if sim_exec else "flwr-serverapp"
229
421
 
230
- # Initialize StateFactory
231
- state_factory = StateFactory(args.database)
232
-
233
- grpc_servers = []
234
- bckg_threads = []
235
-
236
- # Start Fleet API
237
- if args.fleet_api_type == TRANSPORT_TYPE_REST:
238
- if (
239
- importlib.util.find_spec("requests")
240
- and importlib.util.find_spec("starlette")
241
- and importlib.util.find_spec("uvicorn")
242
- ) is None:
243
- sys.exit(MISSING_EXTRA_REST)
244
- address_arg = args.rest_fleet_api_address
245
- parsed_address = parse_address(address_arg)
246
- if not parsed_address:
247
- sys.exit(f"Fleet IP address ({address_arg}) cannot be parsed.")
248
- host, port, _ = parsed_address
249
- fleet_thread = threading.Thread(
250
- target=_run_fleet_api_rest,
422
+ # Scheduler thread
423
+ scheduler_th = threading.Thread(
424
+ target=_flwr_scheduler,
251
425
  args=(
252
- host,
253
- port,
254
- args.ssl_keyfile,
255
- args.ssl_certfile,
256
426
  state_factory,
257
- args.rest_fleet_api_workers,
427
+ address_arg,
428
+ address,
429
+ cmd,
258
430
  ),
431
+ daemon=True,
259
432
  )
260
- fleet_thread.start()
261
- bckg_threads.append(fleet_thread)
262
- elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE:
263
- address_arg = args.grpc_rere_fleet_api_address
264
- parsed_address = parse_address(address_arg)
265
- if not parsed_address:
266
- sys.exit(f"Fleet IP address ({address_arg}) cannot be parsed.")
267
- host, port, is_v6 = parsed_address
268
- address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
269
- fleet_server = _run_fleet_api_grpc_rere(
270
- address=address,
271
- state_factory=state_factory,
272
- certificates=certificates,
273
- )
274
- grpc_servers.append(fleet_server)
275
- else:
276
- raise ValueError(f"Unknown fleet_api_type: {args.fleet_api_type}")
433
+ scheduler_th.start()
434
+ bckg_threads.append(scheduler_th)
277
435
 
278
436
  # Graceful shutdown
279
437
  register_exit_handlers(
280
- event_type=EventType.RUN_FLEET_API_LEAVE,
438
+ event_type=EventType.RUN_SUPERLINK_LEAVE,
281
439
  grpc_servers=grpc_servers,
282
- bckg_threads=bckg_threads,
283
440
  )
284
441
 
285
- # Block
286
- if len(grpc_servers) > 0:
287
- grpc_servers[0].wait_for_termination()
288
- elif len(bckg_threads) > 0:
289
- bckg_threads[0].join()
442
+ # Block until a thread exits prematurely
443
+ while all(thread.is_alive() for thread in bckg_threads):
444
+ sleep(0.1)
290
445
 
446
+ # Exit if any thread has exited prematurely
447
+ sys.exit(1)
291
448
 
292
- # pylint: disable=too-many-branches, too-many-locals, too-many-statements
293
- def run_superlink() -> None:
294
- """Run Flower server (Driver API and Fleet API)."""
295
- log(INFO, "Starting Flower server")
296
- event(EventType.RUN_SUPERLINK_ENTER)
297
- args = _parse_args_run_superlink().parse_args()
298
449
 
299
- # Parse IP address
300
- parsed_address = parse_address(args.driver_api_address)
301
- if not parsed_address:
302
- sys.exit(f"Driver IP address ({args.driver_api_address}) cannot be parsed.")
303
- host, port, is_v6 = parsed_address
304
- address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
450
+ def _run_flwr_command(args: list[str]) -> None:
451
+ sys.argv = args
452
+ if args[0] == "flwr-serverapp":
453
+ flwr_serverapp()
454
+ elif args[0] == "flwr-simulation":
455
+ flwr_simulation()
456
+ else:
457
+ raise ValueError(f"Unknown command: {args[0]}")
305
458
 
306
- # Obtain certificates
307
- certificates = _try_obtain_certificates(args)
308
459
 
309
- # Initialize StateFactory
310
- state_factory = StateFactory(args.database)
460
+ def _flwr_scheduler(
461
+ state_factory: LinkStateFactory,
462
+ io_api_arg: str,
463
+ io_api_address: str,
464
+ cmd: str,
465
+ ) -> None:
466
+ log(DEBUG, "Started %s scheduler thread.", cmd)
467
+ state = state_factory.state()
468
+ run_id_to_proc: dict[int, multiprocessing.context.SpawnProcess] = {}
311
469
 
312
- # Start Driver API
313
- driver_server: grpc.Server = run_driver_api_grpc(
314
- address=address,
315
- state_factory=state_factory,
316
- certificates=certificates,
317
- )
470
+ # Use the "spawn" start method for multiprocessing.
471
+ mp_spawn_context = multiprocessing.get_context("spawn")
318
472
 
319
- grpc_servers = [driver_server]
320
- bckg_threads = []
321
-
322
- # Start Fleet API
323
- if args.fleet_api_type == TRANSPORT_TYPE_REST:
324
- if (
325
- importlib.util.find_spec("requests")
326
- and importlib.util.find_spec("starlette")
327
- and importlib.util.find_spec("uvicorn")
328
- ) is None:
329
- sys.exit(MISSING_EXTRA_REST)
330
- address_arg = args.rest_fleet_api_address
331
- parsed_address = parse_address(address_arg)
332
- if not parsed_address:
333
- sys.exit(f"Fleet IP address ({address_arg}) cannot be parsed.")
334
- host, port, _ = parsed_address
335
- fleet_thread = threading.Thread(
336
- target=_run_fleet_api_rest,
337
- args=(
338
- host,
339
- port,
340
- args.ssl_keyfile,
341
- args.ssl_certfile,
342
- state_factory,
343
- args.rest_fleet_api_workers,
344
- ),
473
+ # Periodically check for a pending run in the LinkState
474
+ while True:
475
+ sleep(0.1)
476
+ pending_run_id = state.get_pending_run_id()
477
+
478
+ if pending_run_id and pending_run_id not in run_id_to_proc:
479
+
480
+ log(
481
+ INFO,
482
+ "Launching %s subprocess. Connects to SuperLink on %s",
483
+ cmd,
484
+ io_api_address,
485
+ )
486
+ # Start subprocess
487
+ command = [
488
+ cmd,
489
+ "--run-once",
490
+ io_api_arg,
491
+ io_api_address,
492
+ "--insecure",
493
+ ]
494
+
495
+ proc = mp_spawn_context.Process(
496
+ target=_run_flwr_command, args=(command,), daemon=True
497
+ )
498
+ proc.start()
499
+
500
+ # Store the process
501
+ run_id_to_proc[pending_run_id] = proc
502
+
503
+ # Clean up finished processes
504
+ for run_id, proc in list(run_id_to_proc.items()):
505
+ if not proc.is_alive():
506
+ del run_id_to_proc[run_id]
507
+
508
+
509
+ def _format_address(address: str) -> tuple[str, str, int]:
510
+ parsed_address = parse_address(address)
511
+ if not parsed_address:
512
+ sys.exit(
513
+ f"Address ({address}) cannot be parsed (expected: URL or IPv4 or IPv6)."
345
514
  )
346
- fleet_thread.start()
347
- bckg_threads.append(fleet_thread)
348
- elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE:
349
- address_arg = args.grpc_rere_fleet_api_address
350
- parsed_address = parse_address(address_arg)
351
- if not parsed_address:
352
- sys.exit(f"Fleet IP address ({address_arg}) cannot be parsed.")
353
- host, port, is_v6 = parsed_address
354
- address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
355
- fleet_server = _run_fleet_api_grpc_rere(
356
- address=address,
357
- state_factory=state_factory,
358
- certificates=certificates,
515
+ host, port, is_v6 = parsed_address
516
+ return (f"[{host}]:{port}" if is_v6 else f"{host}:{port}", host, port)
517
+
518
+
519
+ def _try_setup_node_authentication(
520
+ args: argparse.Namespace,
521
+ certificates: Optional[tuple[bytes, bytes, bytes]],
522
+ ) -> Optional[tuple[set[bytes], ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]:
523
+ if (
524
+ not args.auth_list_public_keys
525
+ and not args.auth_superlink_private_key
526
+ and not args.auth_superlink_public_key
527
+ ):
528
+ return None
529
+
530
+ if (
531
+ not args.auth_list_public_keys
532
+ or not args.auth_superlink_private_key
533
+ or not args.auth_superlink_public_key
534
+ ):
535
+ sys.exit(
536
+ "Authentication requires providing file paths for "
537
+ "'--auth-list-public-keys', '--auth-superlink-private-key' and "
538
+ "'--auth-superlink-public-key'. Provide all three to enable authentication."
359
539
  )
360
- grpc_servers.append(fleet_server)
361
- elif args.fleet_api_type == TRANSPORT_TYPE_VCE:
362
- f_stop = asyncio.Event() # Does nothing
363
- _run_fleet_api_vce(
364
- num_supernodes=args.num_supernodes,
365
- client_app_attr=args.client_app,
366
- backend_name=args.backend,
367
- backend_config_json_stream=args.backend_config,
368
- app_dir=args.app_dir,
369
- state_factory=state_factory,
370
- f_stop=f_stop,
540
+
541
+ if certificates is None:
542
+ sys.exit(
543
+ "Authentication requires secure connections. "
544
+ "Please provide certificate paths to `--ssl-certfile`, "
545
+ "`--ssl-keyfile`, and `—-ssl-ca-certfile` and try again."
371
546
  )
372
- else:
373
- raise ValueError(f"Unknown fleet_api_type: {args.fleet_api_type}")
374
547
 
375
- # Graceful shutdown
376
- register_exit_handlers(
377
- event_type=EventType.RUN_SUPERLINK_LEAVE,
378
- grpc_servers=grpc_servers,
379
- bckg_threads=bckg_threads,
380
- )
548
+ node_keys_file_path = Path(args.auth_list_public_keys)
549
+ if not node_keys_file_path.exists():
550
+ sys.exit(
551
+ "The provided path to the known public keys CSV file does not exist: "
552
+ f"{node_keys_file_path}. "
553
+ "Please provide the CSV file path containing known public keys "
554
+ "to '--auth-list-public-keys'."
555
+ )
381
556
 
382
- # Block
383
- while True:
384
- if bckg_threads:
385
- for thread in bckg_threads:
386
- if not thread.is_alive():
387
- sys.exit(1)
388
- driver_server.wait_for_termination(timeout=1)
557
+ node_public_keys: set[bytes] = set()
389
558
 
559
+ try:
560
+ ssh_private_key = load_ssh_private_key(
561
+ Path(args.auth_superlink_private_key).read_bytes(),
562
+ None,
563
+ )
564
+ if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
565
+ raise ValueError()
566
+ except (ValueError, UnsupportedAlgorithm):
567
+ sys.exit(
568
+ "Error: Unable to parse the private key file in "
569
+ "'--auth-superlink-private-key'. Authentication requires elliptic "
570
+ "curve private and public key pair. Please ensure that the file "
571
+ "path points to a valid private key file and try again."
572
+ )
390
573
 
391
- def _try_obtain_certificates(
392
- args: argparse.Namespace,
393
- ) -> Optional[Tuple[bytes, bytes, bytes]]:
394
- # Obtain certificates
395
- if args.insecure:
396
- log(WARN, "Option `--insecure` was set. Starting insecure HTTP server.")
397
- certificates = None
398
- # Check if certificates are provided
399
- elif args.certificates:
400
- certificates = (
401
- Path(args.certificates[0]).read_bytes(), # CA certificate
402
- Path(args.certificates[1]).read_bytes(), # server certificate
403
- Path(args.certificates[2]).read_bytes(), # server private key
574
+ try:
575
+ ssh_public_key = load_ssh_public_key(
576
+ Path(args.auth_superlink_public_key).read_bytes()
404
577
  )
405
- else:
578
+ if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
579
+ raise ValueError()
580
+ except (ValueError, UnsupportedAlgorithm):
406
581
  sys.exit(
407
- "Certificates are required unless running in insecure mode. "
408
- "Please provide certificate paths with '--certificates' or run the server "
409
- "in insecure mode using '--insecure' if you understand the risks."
582
+ "Error: Unable to parse the public key file in "
583
+ "'--auth-superlink-public-key'. Authentication requires elliptic "
584
+ "curve private and public key pair. Please ensure that the file "
585
+ "path points to a valid public key file and try again."
410
586
  )
411
- return certificates
587
+
588
+ with open(node_keys_file_path, newline="", encoding="utf-8") as csvfile:
589
+ reader = csv.reader(csvfile)
590
+ for row in reader:
591
+ for element in row:
592
+ public_key = load_ssh_public_key(element.encode())
593
+ if isinstance(public_key, ec.EllipticCurvePublicKey):
594
+ node_public_keys.add(public_key_to_bytes(public_key))
595
+ else:
596
+ sys.exit(
597
+ "Error: Unable to parse the public keys in the CSV "
598
+ "file. Please ensure that the CSV file path points to a valid "
599
+ "known SSH public keys files and try again."
600
+ )
601
+ return (
602
+ node_public_keys,
603
+ ssh_private_key,
604
+ ssh_public_key,
605
+ )
606
+
607
+
608
+ def _try_obtain_exec_auth_plugin(config_path: Path) -> Optional[ExecAuthPlugin]:
609
+ # Load YAML file
610
+ with config_path.open("r", encoding="utf-8") as file:
611
+ config: dict[str, Any] = yaml.safe_load(file)
612
+
613
+ # Load authentication configuration
614
+ auth_config: dict[str, Any] = config.get("authentication", {})
615
+ auth_type: str = auth_config.get(AUTH_TYPE, "")
616
+
617
+ # Load authentication plugin
618
+ try:
619
+ all_plugins: dict[str, type[ExecAuthPlugin]] = get_exec_auth_plugins()
620
+ auth_plugin_class = all_plugins[auth_type]
621
+ return auth_plugin_class(user_auth_config_path=config_path)
622
+ except KeyError:
623
+ if auth_type != "":
624
+ sys.exit(
625
+ f'Authentication type "{auth_type}" is not supported. '
626
+ "Please provide a valid authentication type in the configuration."
627
+ )
628
+ sys.exit("No authentication type is provided in the configuration.")
629
+ except NotImplementedError:
630
+ sys.exit("No authentication plugins are currently supported.")
412
631
 
413
632
 
414
633
  def _run_fleet_api_grpc_rere(
415
634
  address: str,
416
- state_factory: StateFactory,
417
- certificates: Optional[Tuple[bytes, bytes, bytes]],
635
+ state_factory: LinkStateFactory,
636
+ ffs_factory: FfsFactory,
637
+ certificates: Optional[tuple[bytes, bytes, bytes]],
638
+ interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
418
639
  ) -> grpc.Server:
419
640
  """Run Fleet API (gRPC, request-response)."""
420
641
  # Create Fleet API gRPC server
421
642
  fleet_servicer = FleetServicer(
422
643
  state_factory=state_factory,
644
+ ffs_factory=ffs_factory,
423
645
  )
424
646
  fleet_add_servicer_to_server_fn = add_FleetServicer_to_server
425
647
  fleet_grpc_server = generic_create_grpc_server(
@@ -427,6 +649,7 @@ def _run_fleet_api_grpc_rere(
427
649
  server_address=address,
428
650
  max_message_length=GRPC_MAX_MESSAGE_LENGTH,
429
651
  certificates=certificates,
652
+ interceptors=interceptors,
430
653
  )
431
654
 
432
655
  log(INFO, "Flower ECE: Starting Fleet API (gRPC-rere) on %s", address)
@@ -435,63 +658,56 @@ def _run_fleet_api_grpc_rere(
435
658
  return fleet_grpc_server
436
659
 
437
660
 
438
- # pylint: disable=too-many-arguments
439
- def _run_fleet_api_vce(
440
- num_supernodes: int,
441
- client_app_attr: str,
442
- backend_name: str,
443
- backend_config_json_stream: str,
444
- app_dir: str,
445
- state_factory: StateFactory,
446
- f_stop: asyncio.Event,
447
- ) -> None:
448
- log(INFO, "Flower VCE: Starting Fleet API (VirtualClientEngine)")
449
-
450
- start_vce(
451
- num_supernodes=num_supernodes,
452
- client_app_attr=client_app_attr,
453
- backend_name=backend_name,
454
- backend_config_json_stream=backend_config_json_stream,
661
+ def _run_fleet_api_grpc_adapter(
662
+ address: str,
663
+ state_factory: LinkStateFactory,
664
+ ffs_factory: FfsFactory,
665
+ certificates: Optional[tuple[bytes, bytes, bytes]],
666
+ ) -> grpc.Server:
667
+ """Run Fleet API (GrpcAdapter)."""
668
+ # Create Fleet API gRPC server
669
+ fleet_servicer = GrpcAdapterServicer(
455
670
  state_factory=state_factory,
456
- app_dir=app_dir,
457
- f_stop=f_stop,
671
+ ffs_factory=ffs_factory,
672
+ )
673
+ fleet_add_servicer_to_server_fn = add_GrpcAdapterServicer_to_server
674
+ fleet_grpc_server = generic_create_grpc_server(
675
+ servicer_and_add_fn=(fleet_servicer, fleet_add_servicer_to_server_fn),
676
+ server_address=address,
677
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
678
+ certificates=certificates,
458
679
  )
459
680
 
681
+ log(INFO, "Flower ECE: Starting Fleet API (GrpcAdapter) on %s", address)
682
+ fleet_grpc_server.start()
683
+
684
+ return fleet_grpc_server
685
+
460
686
 
461
687
  # pylint: disable=import-outside-toplevel,too-many-arguments
688
+ # pylint: disable=too-many-positional-arguments
462
689
  def _run_fleet_api_rest(
463
690
  host: str,
464
691
  port: int,
465
692
  ssl_keyfile: Optional[str],
466
693
  ssl_certfile: Optional[str],
467
- state_factory: StateFactory,
468
- workers: int,
694
+ state_factory: LinkStateFactory,
695
+ ffs_factory: FfsFactory,
696
+ num_workers: int,
469
697
  ) -> None:
470
- """Run Driver API (REST-based)."""
698
+ """Run ServerAppIo API (REST-based)."""
471
699
  try:
472
700
  import uvicorn
473
701
 
474
702
  from flwr.server.superlink.fleet.rest_rere.rest_api import app as fast_api_app
475
703
  except ModuleNotFoundError:
476
704
  sys.exit(MISSING_EXTRA_REST)
477
- if workers != 1:
478
- raise ValueError(
479
- f"The supported number of workers for the Fleet API (REST server) is "
480
- f"1. Instead given {workers}. The functionality of >1 workers will be "
481
- f"added in the future releases."
482
- )
705
+
483
706
  log(INFO, "Starting Flower REST server")
484
707
 
485
708
  # See: https://www.starlette.io/applications/#accessing-the-app-instance
486
709
  fast_api_app.state.STATE_FACTORY = state_factory
487
-
488
- validation_exceptions = _validate_ssl_files(
489
- ssl_certfile=ssl_certfile, ssl_keyfile=ssl_keyfile
490
- )
491
- if any(validation_exceptions):
492
- # Starting with 3.11 we can use ExceptionGroup but for now
493
- # this seems to be the reasonable approach.
494
- raise ValueError(validation_exceptions)
710
+ fast_api_app.state.FFS_FACTORY = ffs_factory
495
711
 
496
712
  uvicorn.run(
497
713
  app="flwr.server.superlink.fleet.rest_rere.rest_api:app",
@@ -501,81 +717,22 @@ def _run_fleet_api_rest(
501
717
  access_log=True,
502
718
  ssl_keyfile=ssl_keyfile,
503
719
  ssl_certfile=ssl_certfile,
504
- workers=workers,
720
+ workers=num_workers,
505
721
  )
506
722
 
507
723
 
508
- def _validate_ssl_files(
509
- ssl_keyfile: Optional[str], ssl_certfile: Optional[str]
510
- ) -> List[ValueError]:
511
- validation_exceptions = []
512
-
513
- if ssl_keyfile is not None and not isfile(ssl_keyfile):
514
- msg = "Path argument `--ssl-keyfile` does not point to a file."
515
- log(ERROR, msg)
516
- validation_exceptions.append(ValueError(msg))
517
-
518
- if ssl_certfile is not None and not isfile(ssl_certfile):
519
- msg = "Path argument `--ssl-certfile` does not point to a file."
520
- log(ERROR, msg)
521
- validation_exceptions.append(ValueError(msg))
522
-
523
- if not bool(ssl_keyfile) == bool(ssl_certfile):
524
- msg = (
525
- "When setting one of `--ssl-keyfile` and "
526
- "`--ssl-certfile`, both have to be used."
527
- )
528
- log(ERROR, msg)
529
- validation_exceptions.append(ValueError(msg))
530
-
531
- return validation_exceptions
532
-
533
-
534
- def _parse_args_run_driver_api() -> argparse.ArgumentParser:
535
- """Parse command line arguments for Driver API."""
536
- parser = argparse.ArgumentParser(
537
- description="Start a Flower Driver API server. "
538
- "This server will be responsible for "
539
- "receiving TaskIns from the Driver script and "
540
- "sending them to the Fleet API. Once the client nodes "
541
- "are done, they will send the TaskRes back to this Driver API server (through"
542
- " the Fleet API) which will then send them back to the Driver script.",
543
- )
544
-
545
- _add_args_common(parser=parser)
546
- _add_args_driver_api(parser=parser)
547
-
548
- return parser
549
-
550
-
551
- def _parse_args_run_fleet_api() -> argparse.ArgumentParser:
552
- """Parse command line arguments for Fleet API."""
553
- parser = argparse.ArgumentParser(
554
- description="Start a Flower Fleet API server."
555
- "This server will be responsible for "
556
- "sending TaskIns (received from the Driver API) to the client nodes "
557
- "and of receiving TaskRes sent back from those same client nodes once "
558
- "they are done. Then, this Fleet API server can send those "
559
- "TaskRes back to the Driver API.",
560
- )
561
-
562
- _add_args_common(parser=parser)
563
- _add_args_fleet_api(parser=parser)
564
-
565
- return parser
566
-
567
-
568
724
  def _parse_args_run_superlink() -> argparse.ArgumentParser:
569
- """Parse command line arguments for both Driver API and Fleet API."""
725
+ """Parse command line arguments for both ServerAppIo API and Fleet API."""
570
726
  parser = argparse.ArgumentParser(
571
- description="This will start a Flower server "
572
- "(meaning, a Driver API and a Fleet API), "
573
- "that clients will be able to connect to.",
727
+ description="Start a Flower SuperLink",
574
728
  )
575
729
 
576
730
  _add_args_common(parser=parser)
577
- _add_args_driver_api(parser=parser)
731
+ add_ee_args_superlink(parser=parser)
732
+ _add_args_serverappio_api(parser=parser)
578
733
  _add_args_fleet_api(parser=parser)
734
+ _add_args_exec_api(parser=parser)
735
+ _add_args_simulationio_api(parser=parser)
579
736
 
580
737
  return parser
581
738
 
@@ -589,13 +746,47 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None:
589
746
  "Use this flag only if you understand the risks.",
590
747
  )
591
748
  parser.add_argument(
592
- "--certificates",
593
- nargs=3,
594
- metavar=("CA_CERT", "SERVER_CERT", "PRIVATE_KEY"),
749
+ "--flwr-dir",
750
+ default=None,
751
+ help="""The path containing installed Flower Apps.
752
+ The default directory is:
753
+
754
+ - `$FLWR_HOME/` if `$FLWR_HOME` is defined
755
+ - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
756
+ - `$HOME/.flwr/` in all other cases
757
+ """,
758
+ )
759
+ parser.add_argument(
760
+ "--ssl-certfile",
761
+ help="Fleet API server SSL certificate file (as a path str) "
762
+ "to create a secure connection.",
595
763
  type=str,
596
- help="Paths to the CA certificate, server certificate, and server private "
597
- "key, in that order. Note: The server can only be started without "
598
- "certificates by enabling the `--insecure` flag.",
764
+ default=None,
765
+ )
766
+ parser.add_argument(
767
+ "--ssl-keyfile",
768
+ help="Fleet API server SSL private key file (as a path str) "
769
+ "to create a secure connection.",
770
+ type=str,
771
+ )
772
+ parser.add_argument(
773
+ "--ssl-ca-certfile",
774
+ help="Fleet API server SSL CA certificate file (as a path str) "
775
+ "to create a secure connection.",
776
+ type=str,
777
+ )
778
+ parser.add_argument(
779
+ "--isolation",
780
+ default=ISOLATION_MODE_SUBPROCESS,
781
+ required=False,
782
+ choices=[
783
+ ISOLATION_MODE_SUBPROCESS,
784
+ ISOLATION_MODE_PROCESS,
785
+ ],
786
+ help="Isolation mode when running a `ServerApp` (`subprocess` by default, "
787
+ "possible values: `subprocess`, `process`). Use `subprocess` to configure "
788
+ "SuperLink to run a `ServerApp` in a subprocess. Use `process` to indicate "
789
+ "that a separate independent process gets created outside of SuperLink.",
599
790
  )
600
791
  parser.add_argument(
601
792
  "--database",
@@ -606,108 +797,94 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None:
606
797
  "Flower will just create a state in memory.",
607
798
  default=DATABASE,
608
799
  )
800
+ parser.add_argument(
801
+ "--storage-dir",
802
+ help="The base directory to store the objects for the Flower File System.",
803
+ default=BASE_DIR,
804
+ )
805
+ parser.add_argument(
806
+ "--auth-list-public-keys",
807
+ type=str,
808
+ help="A CSV file (as a path str) containing a list of known public "
809
+ "keys to enable authentication.",
810
+ )
811
+ parser.add_argument(
812
+ "--auth-superlink-private-key",
813
+ type=str,
814
+ help="The SuperLink's private key (as a path str) to enable authentication.",
815
+ )
816
+ parser.add_argument(
817
+ "--auth-superlink-public-key",
818
+ type=str,
819
+ help="The SuperLink's public key (as a path str) to enable authentication.",
820
+ )
609
821
 
610
822
 
611
- def _add_args_driver_api(parser: argparse.ArgumentParser) -> None:
823
+ def _add_args_serverappio_api(parser: argparse.ArgumentParser) -> None:
612
824
  parser.add_argument(
613
- "--driver-api-address",
614
- help="Driver API (gRPC) server address (IPv4, IPv6, or a domain name)",
615
- default=ADDRESS_DRIVER_API,
825
+ "--serverappio-api-address",
826
+ default=SERVERAPPIO_API_DEFAULT_SERVER_ADDRESS,
827
+ help="ServerAppIo API (gRPC) server address (IPv4, IPv6, or a domain name). "
828
+ f"By default, it is set to {SERVERAPPIO_API_DEFAULT_SERVER_ADDRESS}.",
616
829
  )
617
830
 
618
831
 
619
832
  def _add_args_fleet_api(parser: argparse.ArgumentParser) -> None:
620
833
  # Fleet API transport layer type
621
- ex_group = parser.add_mutually_exclusive_group()
622
- ex_group.add_argument(
623
- "--grpc-rere",
624
- action="store_const",
625
- dest="fleet_api_type",
626
- const=TRANSPORT_TYPE_GRPC_RERE,
834
+ parser.add_argument(
835
+ "--fleet-api-type",
627
836
  default=TRANSPORT_TYPE_GRPC_RERE,
628
- help="Start a Fleet API server (gRPC-rere)",
837
+ type=str,
838
+ choices=[
839
+ TRANSPORT_TYPE_GRPC_RERE,
840
+ TRANSPORT_TYPE_GRPC_ADAPTER,
841
+ TRANSPORT_TYPE_REST,
842
+ ],
843
+ help="Start a gRPC-rere or REST (experimental) Fleet API server.",
629
844
  )
630
- ex_group.add_argument(
631
- "--rest",
632
- action="store_const",
633
- dest="fleet_api_type",
634
- const=TRANSPORT_TYPE_REST,
635
- help="Start a Fleet API server (REST, experimental)",
845
+ parser.add_argument(
846
+ "--fleet-api-address",
847
+ help="Fleet API server address (IPv4, IPv6, or a domain name).",
636
848
  )
637
-
638
- ex_group.add_argument(
639
- "--vce",
640
- action="store_const",
641
- dest="fleet_api_type",
642
- const=TRANSPORT_TYPE_VCE,
643
- help="Start a Fleet API server (VirtualClientEngine)",
849
+ parser.add_argument(
850
+ "--fleet-api-num-workers",
851
+ default=1,
852
+ type=int,
853
+ help="Set the number of concurrent workers for the Fleet API server.",
644
854
  )
645
855
 
646
- # Fleet API gRPC-rere options
647
- grpc_rere_group = parser.add_argument_group(
648
- "Fleet API (gRPC-rere) server options", ""
649
- )
650
- grpc_rere_group.add_argument(
651
- "--grpc-rere-fleet-api-address",
652
- help="Fleet API (gRPC-rere) server address (IPv4, IPv6, or a domain name)",
653
- default=ADDRESS_FLEET_API_GRPC_RERE,
654
- )
655
856
 
656
- # Fleet API REST options
657
- rest_group = parser.add_argument_group("Fleet API (REST) server options", "")
658
- rest_group.add_argument(
659
- "--rest-fleet-api-address",
660
- help="Fleet API (REST) server address (IPv4, IPv6, or a domain name)",
661
- default=ADDRESS_FLEET_API_REST,
857
+ def _add_args_exec_api(parser: argparse.ArgumentParser) -> None:
858
+ """Add command line arguments for Exec API."""
859
+ parser.add_argument(
860
+ "--exec-api-address",
861
+ help="Exec API server address (IPv4, IPv6, or a domain name) "
862
+ f"By default, it is set to {EXEC_API_DEFAULT_SERVER_ADDRESS}.",
863
+ default=EXEC_API_DEFAULT_SERVER_ADDRESS,
662
864
  )
663
- rest_group.add_argument(
664
- "--ssl-certfile",
665
- help="Fleet API (REST) server SSL certificate file (as a path str), "
666
- "needed for using 'https'.",
667
- default=None,
865
+ parser.add_argument(
866
+ "--executor",
867
+ help="For example: `deployment:exec` or `project.package.module:wrapper.exec`. "
868
+ "The default is `flwr.superexec.deployment:executor`",
869
+ default="flwr.superexec.deployment:executor",
668
870
  )
669
- rest_group.add_argument(
670
- "--ssl-keyfile",
671
- help="Fleet API (REST) server SSL private key file (as a path str), "
672
- "needed for using 'https'.",
673
- default=None,
871
+ parser.add_argument(
872
+ "--executor-dir",
873
+ help="The directory for the executor.",
874
+ default=".",
674
875
  )
675
- rest_group.add_argument(
676
- "--rest-fleet-api-workers",
677
- help="Set the number of concurrent workers for the Fleet API REST server.",
678
- type=int,
679
- default=1,
876
+ parser.add_argument(
877
+ "--executor-config",
878
+ help="Key-value pairs for the executor config, separated by spaces. "
879
+ "For example:\n\n`--executor-config 'verbose=true "
880
+ 'root-certificates="certificates/superlink-ca.crt"\'`',
680
881
  )
681
882
 
682
- # Fleet API VCE options
683
- vce_group = parser.add_argument_group("Fleet API (VCE) server options", "")
684
- vce_group.add_argument(
685
- "--client-app",
686
- help="For example: `client:app` or `project.package.module:wrapper.app`.",
687
- )
688
- vce_group.add_argument(
689
- "--num-supernodes",
690
- type=int,
691
- help="Number of simulated SuperNodes.",
692
- )
693
- vce_group.add_argument(
694
- "--backend",
695
- default="ray",
696
- type=str,
697
- help="Simulation backend that executes the ClientApp.",
698
- )
699
- vce_group.add_argument(
700
- "--backend-config",
701
- type=str,
702
- default='{"client_resources": {"num_cpus":1, "num_gpus":0.0}, "tensorflow": 0}',
703
- help='A JSON formatted stream, e.g \'{"<keyA>":<value>, "<keyB>":<value>}\' to '
704
- "configure a backend. Values supported in <value> are those included by "
705
- "`flwr.common.typing.ConfigsRecordValues`. ",
706
- )
883
+
884
+ def _add_args_simulationio_api(parser: argparse.ArgumentParser) -> None:
707
885
  parser.add_argument(
708
- "--app-dir",
709
- default="",
710
- help="Add specified directory to the PYTHONPATH and load"
711
- "ClientApp from there."
712
- " Default: current working directory.",
886
+ "--simulationio-api-address",
887
+ default=SIMULATIONIO_API_DEFAULT_SERVER_ADDRESS,
888
+ help="SimulationIo API (gRPC) server address (IPv4, IPv6, or a domain name)."
889
+ f"By default, it is set to {SIMULATIONIO_API_DEFAULT_SERVER_ADDRESS}.",
713
890
  )