flwr-nightly 1.13.0.dev20241106__py3-none-any.whl → 1.13.0.dev20241117__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.

Files changed (58) hide show
  1. flwr/cli/app.py +2 -0
  2. flwr/cli/build.py +37 -0
  3. flwr/cli/install.py +5 -3
  4. flwr/cli/ls.py +228 -0
  5. flwr/cli/run/run.py +16 -5
  6. flwr/client/app.py +68 -19
  7. flwr/client/clientapp/app.py +51 -35
  8. flwr/client/grpc_rere_client/connection.py +2 -12
  9. flwr/client/nodestate/__init__.py +25 -0
  10. flwr/client/nodestate/in_memory_nodestate.py +38 -0
  11. flwr/client/nodestate/nodestate.py +30 -0
  12. flwr/client/nodestate/nodestate_factory.py +37 -0
  13. flwr/client/rest_client/connection.py +4 -14
  14. flwr/client/supernode/app.py +57 -53
  15. flwr/common/args.py +148 -0
  16. flwr/common/config.py +10 -0
  17. flwr/common/constant.py +21 -7
  18. flwr/common/date.py +18 -0
  19. flwr/common/logger.py +6 -2
  20. flwr/common/object_ref.py +47 -16
  21. flwr/common/serde.py +10 -0
  22. flwr/common/typing.py +32 -11
  23. flwr/proto/exec_pb2.py +23 -17
  24. flwr/proto/exec_pb2.pyi +50 -20
  25. flwr/proto/exec_pb2_grpc.py +34 -0
  26. flwr/proto/exec_pb2_grpc.pyi +13 -0
  27. flwr/proto/run_pb2.py +32 -27
  28. flwr/proto/run_pb2.pyi +44 -1
  29. flwr/proto/simulationio_pb2.py +2 -2
  30. flwr/proto/simulationio_pb2_grpc.py +34 -0
  31. flwr/proto/simulationio_pb2_grpc.pyi +13 -0
  32. flwr/server/app.py +83 -87
  33. flwr/server/driver/driver.py +1 -1
  34. flwr/server/driver/grpc_driver.py +6 -20
  35. flwr/server/driver/inmemory_driver.py +1 -3
  36. flwr/server/run_serverapp.py +8 -238
  37. flwr/server/serverapp/app.py +44 -89
  38. flwr/server/strategy/aggregate.py +4 -4
  39. flwr/server/superlink/fleet/rest_rere/rest_api.py +10 -9
  40. flwr/server/superlink/linkstate/in_memory_linkstate.py +76 -62
  41. flwr/server/superlink/linkstate/linkstate.py +24 -9
  42. flwr/server/superlink/linkstate/sqlite_linkstate.py +87 -128
  43. flwr/server/superlink/linkstate/utils.py +191 -32
  44. flwr/server/superlink/simulation/simulationio_servicer.py +22 -1
  45. flwr/simulation/__init__.py +3 -1
  46. flwr/simulation/app.py +245 -352
  47. flwr/simulation/legacy_app.py +402 -0
  48. flwr/simulation/run_simulation.py +8 -19
  49. flwr/simulation/simulationio_connection.py +2 -2
  50. flwr/superexec/deployment.py +13 -7
  51. flwr/superexec/exec_servicer.py +32 -3
  52. flwr/superexec/executor.py +4 -3
  53. flwr/superexec/simulation.py +52 -145
  54. {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/METADATA +10 -7
  55. {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/RECORD +58 -51
  56. {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/entry_points.txt +1 -0
  57. {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/LICENSE +0 -0
  58. {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/WHEEL +0 -0
@@ -24,7 +24,9 @@ import grpc
24
24
  from flwr.cli.install import install_from_fab
25
25
  from flwr.client.client_app import ClientApp, LoadClientAppError
26
26
  from flwr.common import Context, Message
27
- from flwr.common.constant import ErrorCode
27
+ from flwr.common.args import add_args_flwr_app_common, try_obtain_root_certificates
28
+ from flwr.common.config import get_flwr_dir
29
+ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCode
28
30
  from flwr.common.grpc import create_channel
29
31
  from flwr.common.logger import log
30
32
  from flwr.common.message import Error
@@ -54,31 +56,25 @@ from .utils import get_load_client_app_fn
54
56
 
55
57
  def flwr_clientapp() -> None:
56
58
  """Run process-isolated Flower ClientApp."""
57
- parser = argparse.ArgumentParser(
58
- description="Run a Flower ClientApp",
59
- )
60
- parser.add_argument(
61
- "--supernode",
62
- type=str,
63
- help="Address of SuperNode ClientAppIo gRPC servicer",
64
- )
65
- parser.add_argument(
66
- "--token",
67
- type=int,
68
- required=False,
69
- help="Unique token generated by SuperNode for each ClientApp execution",
70
- )
71
- args = parser.parse_args()
59
+ args = _parse_args_run_flwr_clientapp().parse_args()
72
60
 
73
61
  log(INFO, "Starting Flower ClientApp")
62
+ certificates = try_obtain_root_certificates(args, args.clientappio_api_address)
63
+
74
64
  log(
75
65
  DEBUG,
76
- "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s "
66
+ "Starting isolated `ClientApp` connected to SuperNode's ClientAppIo API at %s "
77
67
  "with token %s",
78
- args.supernode,
68
+ args.clientappio_api_address,
79
69
  args.token,
80
70
  )
81
- run_clientapp(supernode=args.supernode, token=args.token)
71
+ run_clientapp(
72
+ clientappio_api_address=args.clientappio_api_address,
73
+ run_once=(args.token is not None),
74
+ token=args.token,
75
+ flwr_dir=args.flwr_dir,
76
+ certificates=certificates,
77
+ )
82
78
 
83
79
 
84
80
  def on_channel_state_change(channel_connectivity: str) -> None:
@@ -87,28 +83,26 @@ def on_channel_state_change(channel_connectivity: str) -> None:
87
83
 
88
84
 
89
85
  def run_clientapp( # pylint: disable=R0914
90
- supernode: str,
86
+ clientappio_api_address: str,
87
+ run_once: bool,
91
88
  token: Optional[int] = None,
89
+ flwr_dir: Optional[str] = None,
90
+ certificates: Optional[bytes] = None,
92
91
  ) -> None:
93
- """Run Flower ClientApp process.
94
-
95
- Parameters
96
- ----------
97
- supernode : str
98
- Address of SuperNode
99
- token : Optional[int] (default: None)
100
- Unique SuperNode token for ClientApp-SuperNode authentication
101
- """
92
+ """Run Flower ClientApp process."""
102
93
  channel = create_channel(
103
- server_address=supernode,
104
- insecure=True,
94
+ server_address=clientappio_api_address,
95
+ insecure=(certificates is None),
96
+ root_certificates=certificates,
105
97
  )
106
98
  channel.subscribe(on_channel_state_change)
107
99
 
100
+ # Resolve directory where FABs are installed
101
+ flwr_dir_ = get_flwr_dir(flwr_dir)
102
+
108
103
  try:
109
104
  stub = ClientAppIoStub(channel)
110
105
 
111
- only_once = token is not None
112
106
  while True:
113
107
  # If token is not set, loop until token is received from SuperNode
114
108
  while token is None:
@@ -121,13 +115,13 @@ def run_clientapp( # pylint: disable=R0914
121
115
  # Install FAB, if provided
122
116
  if fab:
123
117
  log(DEBUG, "Flower ClientApp starts FAB installation.")
124
- install_from_fab(fab.content, flwr_dir=None, skip_prompt=True)
118
+ install_from_fab(fab.content, flwr_dir=flwr_dir_, skip_prompt=True)
125
119
 
126
120
  load_client_app_fn = get_load_client_app_fn(
127
121
  default_app_ref="",
128
122
  app_path=None,
129
123
  multi_app=True,
130
- flwr_dir=None,
124
+ flwr_dir=str(flwr_dir_),
131
125
  )
132
126
 
133
127
  try:
@@ -169,7 +163,7 @@ def run_clientapp( # pylint: disable=R0914
169
163
 
170
164
  # Stop the loop if `flwr-clientapp` is expected to process only a single
171
165
  # message
172
- if only_once:
166
+ if run_once:
173
167
  break
174
168
 
175
169
  except KeyboardInterrupt:
@@ -232,3 +226,25 @@ def push_message(
232
226
  except grpc.RpcError as e:
233
227
  log(ERROR, "[PushClientAppOutputs] gRPC error occurred: %s", str(e))
234
228
  raise e
229
+
230
+
231
+ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
232
+ """Parse flwr-clientapp command line arguments."""
233
+ parser = argparse.ArgumentParser(
234
+ description="Run a Flower ClientApp",
235
+ )
236
+ parser.add_argument(
237
+ "--clientappio-api-address",
238
+ default=CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS,
239
+ type=str,
240
+ help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
241
+ f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
242
+ )
243
+ parser.add_argument(
244
+ "--token",
245
+ type=int,
246
+ required=False,
247
+ help="Unique token generated by SuperNode for each ClientApp execution",
248
+ )
249
+ add_args_flwr_app_common(parser=parser)
250
+ return parser
@@ -41,11 +41,7 @@ from flwr.common.grpc import create_channel
41
41
  from flwr.common.logger import log
42
42
  from flwr.common.message import Message, Metadata
43
43
  from flwr.common.retry_invoker import RetryInvoker
44
- from flwr.common.serde import (
45
- message_from_taskins,
46
- message_to_taskres,
47
- user_config_from_proto,
48
- )
44
+ from flwr.common.serde import message_from_taskins, message_to_taskres, run_from_proto
49
45
  from flwr.common.typing import Fab, Run
50
46
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
51
47
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -287,13 +283,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
287
283
  )
288
284
 
289
285
  # Return fab_id and fab_version
290
- return Run(
291
- run_id,
292
- get_run_response.run.fab_id,
293
- get_run_response.run.fab_version,
294
- get_run_response.run.fab_hash,
295
- user_config_from_proto(get_run_response.run.override_config),
296
- )
286
+ return run_from_proto(get_run_response.run)
297
287
 
298
288
  def get_fab(fab_hash: str) -> Fab:
299
289
  # Call FleetAPI
@@ -0,0 +1,25 @@
1
+ # Copyright 2024 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 NodeState."""
16
+
17
+ from .in_memory_nodestate import InMemoryNodeState as InMemoryNodeState
18
+ from .nodestate import NodeState as NodeState
19
+ from .nodestate_factory import NodeStateFactory as NodeStateFactory
20
+
21
+ __all__ = [
22
+ "InMemoryNodeState",
23
+ "NodeState",
24
+ "NodeStateFactory",
25
+ ]
@@ -0,0 +1,38 @@
1
+ # Copyright 2024 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
+ """In-memory NodeState implementation."""
16
+
17
+
18
+ from typing import Optional
19
+
20
+ from flwr.client.nodestate.nodestate import NodeState
21
+
22
+
23
+ class InMemoryNodeState(NodeState):
24
+ """In-memory NodeState implementation."""
25
+
26
+ def __init__(self) -> None:
27
+ # Store node_id
28
+ self.node_id: Optional[int] = None
29
+
30
+ def set_node_id(self, node_id: Optional[int]) -> None:
31
+ """Set the node ID."""
32
+ self.node_id = node_id
33
+
34
+ def get_node_id(self) -> int:
35
+ """Get the node ID."""
36
+ if self.node_id is None:
37
+ raise ValueError("Node ID not set")
38
+ return self.node_id
@@ -0,0 +1,30 @@
1
+ # Copyright 2024 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
+ """Abstract base class NodeState."""
16
+
17
+ import abc
18
+ from typing import Optional
19
+
20
+
21
+ class NodeState(abc.ABC):
22
+ """Abstract NodeState."""
23
+
24
+ @abc.abstractmethod
25
+ def set_node_id(self, node_id: Optional[int]) -> None:
26
+ """Set the node ID."""
27
+
28
+ @abc.abstractmethod
29
+ def get_node_id(self) -> int:
30
+ """Get the node ID."""
@@ -0,0 +1,37 @@
1
+ # Copyright 2024 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
+ """Factory class that creates NodeState instances."""
16
+
17
+ import threading
18
+ from typing import Optional
19
+
20
+ from .in_memory_nodestate import InMemoryNodeState
21
+ from .nodestate import NodeState
22
+
23
+
24
+ class NodeStateFactory:
25
+ """Factory class that creates NodeState instances."""
26
+
27
+ def __init__(self) -> None:
28
+ self.state_instance: Optional[NodeState] = None
29
+ self.lock = threading.RLock()
30
+
31
+ def state(self) -> NodeState:
32
+ """Return a State instance and create it, if necessary."""
33
+ # Lock access to NodeStateFactory to prevent returning different instances
34
+ with self.lock:
35
+ if self.state_instance is None:
36
+ self.state_instance = InMemoryNodeState()
37
+ return self.state_instance
@@ -41,11 +41,7 @@ from flwr.common.constant import (
41
41
  from flwr.common.logger import log
42
42
  from flwr.common.message import Message, Metadata
43
43
  from flwr.common.retry_invoker import RetryInvoker
44
- from flwr.common.serde import (
45
- message_from_taskins,
46
- message_to_taskres,
47
- user_config_from_proto,
48
- )
44
+ from flwr.common.serde import message_from_taskins, message_to_taskres, run_from_proto
49
45
  from flwr.common.typing import Fab, Run
50
46
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
51
47
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -361,15 +357,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
361
357
  # Send the request
362
358
  res = _request(req, GetRunResponse, PATH_GET_RUN)
363
359
  if res is None:
364
- return Run(run_id, "", "", "", {})
365
-
366
- return Run(
367
- run_id,
368
- res.run.fab_id,
369
- res.run.fab_version,
370
- res.run.fab_hash,
371
- user_config_from_proto(res.run.override_config),
372
- )
360
+ return Run.create_empty(run_id)
361
+
362
+ return run_from_proto(res.run)
373
363
 
374
364
  def get_fab(fab_hash: str) -> Fab:
375
365
  # Construct the request
@@ -28,8 +28,13 @@ from cryptography.hazmat.primitives.serialization import (
28
28
  )
29
29
 
30
30
  from flwr.common import EventType, event
31
+ from flwr.common.args import (
32
+ try_obtain_root_certificates,
33
+ try_obtain_server_certificates,
34
+ )
31
35
  from flwr.common.config import parse_config_args
32
36
  from flwr.common.constant import (
37
+ CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
33
38
  FLEET_API_GRPC_RERE_DEFAULT_ADDRESS,
34
39
  ISOLATION_MODE_PROCESS,
35
40
  ISOLATION_MODE_SUBPROCESS,
@@ -61,10 +66,23 @@ def run_supernode() -> None:
61
66
  "Ignoring `--flwr-dir`.",
62
67
  )
63
68
 
64
- root_certificates = _get_certificates(args)
69
+ # Exit if unsupported argument is passed by the user
70
+ if args.app is not None:
71
+ log(
72
+ ERROR,
73
+ "The `app` argument is deprecated. The SuperNode now automatically "
74
+ "uses the ClientApp delivered from the SuperLink. Providing the app "
75
+ "directory manually is no longer supported. Please remove the `app` "
76
+ "argument from your command.",
77
+ )
78
+ sys.exit(1)
79
+
80
+ root_certificates = try_obtain_root_certificates(args, args.superlink)
81
+ # Obtain certificates for ClientAppIo API server
82
+ server_certificates = try_obtain_server_certificates(args, TRANSPORT_TYPE_GRPC_RERE)
65
83
  load_fn = get_load_client_app_fn(
66
84
  default_app_ref="",
67
- app_path=args.app,
85
+ app_path=None,
68
86
  flwr_dir=args.flwr_dir,
69
87
  multi_app=True,
70
88
  )
@@ -86,7 +104,9 @@ def run_supernode() -> None:
86
104
  ),
87
105
  flwr_path=args.flwr_dir,
88
106
  isolation=args.isolation,
89
- supernode_address=args.supernode_address,
107
+ clientappio_api_address=args.clientappio_api_address,
108
+ certificates=server_certificates,
109
+ ssl_ca_certfile=args.ssl_ca_certfile,
90
110
  )
91
111
 
92
112
  # Graceful shutdown
@@ -126,41 +146,6 @@ def _warn_deprecated_server_arg(args: argparse.Namespace) -> None:
126
146
  args.superlink = args.server
127
147
 
128
148
 
129
- def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
130
- """Load certificates if specified in args."""
131
- # Obtain certificates
132
- if args.insecure:
133
- if args.root_certificates is not None:
134
- sys.exit(
135
- "Conflicting options: The '--insecure' flag disables HTTPS, "
136
- "but '--root-certificates' was also specified. Please remove "
137
- "the '--root-certificates' option when running in insecure mode, "
138
- "or omit '--insecure' to use HTTPS."
139
- )
140
- log(
141
- WARN,
142
- "Option `--insecure` was set. "
143
- "Starting insecure HTTP client connected to %s.",
144
- args.superlink,
145
- )
146
- root_certificates = None
147
- else:
148
- # Load the certificates if provided, or load the system certificates
149
- cert_path = args.root_certificates
150
- if cert_path is None:
151
- root_certificates = None
152
- else:
153
- root_certificates = Path(cert_path).read_bytes()
154
- log(
155
- DEBUG,
156
- "Starting secure HTTPS client connected to %s "
157
- "with the following certificates: %s.",
158
- args.superlink,
159
- cert_path,
160
- )
161
- return root_certificates
162
-
163
-
164
149
  def _parse_args_run_supernode() -> argparse.ArgumentParser:
165
150
  """Parse flower-supernode command line arguments."""
166
151
  parser = argparse.ArgumentParser(
@@ -171,12 +156,12 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
171
156
  "app",
172
157
  nargs="?",
173
158
  default=None,
174
- help="Specify the path of the Flower App to load and run the `ClientApp`. "
175
- "The `pyproject.toml` file must be located in the root of this path. "
176
- "When this argument is provided, the SuperNode will exclusively respond to "
177
- "messages from the corresponding `ServerApp` by matching the FAB ID and FAB "
178
- "version. An error will be raised if a message is received from any other "
179
- "`ServerApp`.",
159
+ help=(
160
+ "(REMOVED) This argument is removed. The SuperNode now automatically "
161
+ "uses the ClientApp delivered from the SuperLink, so there is no need to "
162
+ "provide the app directory manually. This argument will be removed in a "
163
+ "future version."
164
+ ),
180
165
  )
181
166
  _parse_args_common(parser)
182
167
  parser.add_argument(
@@ -192,22 +177,22 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
192
177
  )
193
178
  parser.add_argument(
194
179
  "--isolation",
195
- default=None,
180
+ default=ISOLATION_MODE_SUBPROCESS,
196
181
  required=False,
197
182
  choices=[
198
183
  ISOLATION_MODE_SUBPROCESS,
199
184
  ISOLATION_MODE_PROCESS,
200
185
  ],
201
- help="Isolation mode when running a `ClientApp` (optional, possible values: "
202
- "`subprocess`, `process`). By default, a `ClientApp` runs in the same process "
203
- "that executes the SuperNode. Use `subprocess` to configure SuperNode to run "
204
- "a `ClientApp` in a subprocess. Use `process` to indicate that a separate "
205
- "independent process gets created outside of SuperNode.",
186
+ help="Isolation mode when running a `ClientApp` (`subprocess` by default, "
187
+ "possible values: `subprocess`, `process`). Use `subprocess` to configure "
188
+ "SuperNode to run a `ClientApp` in a subprocess. Use `process` to indicate "
189
+ "that a separate independent process gets created outside of SuperNode.",
206
190
  )
207
191
  parser.add_argument(
208
- "--supernode-address",
209
- default="0.0.0.0:9094",
210
- help="Set the SuperNode gRPC server address. Defaults to `0.0.0.0:9094`.",
192
+ "--clientappio-api-address",
193
+ default=CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
194
+ help="ClientAppIo API (gRPC) server address (IPv4, IPv6, or a domain name). "
195
+ f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS}.",
211
196
  )
212
197
 
213
198
  return parser
@@ -250,6 +235,25 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
250
235
  help="Specifies the path to the PEM-encoded root certificate file for "
251
236
  "establishing secure HTTPS connections.",
252
237
  )
238
+ parser.add_argument(
239
+ "--ssl-certfile",
240
+ help="ClientAppIo API server SSL certificate file (as a path str) "
241
+ "to create a secure connection.",
242
+ type=str,
243
+ default=None,
244
+ )
245
+ parser.add_argument(
246
+ "--ssl-keyfile",
247
+ help="ClientAppIo API server SSL private key file (as a path str) "
248
+ "to create a secure connection.",
249
+ type=str,
250
+ )
251
+ parser.add_argument(
252
+ "--ssl-ca-certfile",
253
+ help="ClientAppIo API server SSL CA certificate file (as a path str) "
254
+ "to create a secure connection.",
255
+ type=str,
256
+ )
253
257
  parser.add_argument(
254
258
  "--server",
255
259
  default=FLEET_API_GRPC_RERE_DEFAULT_ADDRESS,
flwr/common/args.py ADDED
@@ -0,0 +1,148 @@
1
+ # Copyright 2024 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
+ """Common Flower arguments."""
16
+
17
+ import argparse
18
+ import sys
19
+ from logging import DEBUG, WARN
20
+ from os.path import isfile
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ from flwr.common.constant import (
25
+ TRANSPORT_TYPE_GRPC_ADAPTER,
26
+ TRANSPORT_TYPE_GRPC_RERE,
27
+ TRANSPORT_TYPE_REST,
28
+ )
29
+ from flwr.common.logger import log
30
+
31
+
32
+ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
33
+ """Add common Flower arguments for flwr-*app to the provided parser."""
34
+ parser.add_argument(
35
+ "--flwr-dir",
36
+ default=None,
37
+ help="""The path containing installed Flower Apps.
38
+ By default, this value is equal to:
39
+
40
+ - `$FLWR_HOME/` if `$FLWR_HOME` is defined
41
+ - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
42
+ - `$HOME/.flwr/` in all other cases
43
+ """,
44
+ )
45
+ parser.add_argument(
46
+ "--insecure",
47
+ action="store_true",
48
+ help="Run the server without HTTPS, regardless of whether certificate "
49
+ "paths are provided. By default, the server runs with HTTPS enabled. "
50
+ "Use this flag only if you understand the risks.",
51
+ )
52
+ parser.add_argument(
53
+ "--root-certificates",
54
+ metavar="ROOT_CERT",
55
+ type=str,
56
+ help="Specifies the path to the PEM-encoded root certificate file for "
57
+ "establishing secure HTTPS connections.",
58
+ )
59
+
60
+
61
+ def try_obtain_root_certificates(
62
+ args: argparse.Namespace,
63
+ grpc_server_address: str,
64
+ ) -> Optional[bytes]:
65
+ """Validate and return the root certificates."""
66
+ root_cert_path = args.root_certificates
67
+ if args.insecure:
68
+ if root_cert_path is not None:
69
+ sys.exit(
70
+ "Conflicting options: The '--insecure' flag disables HTTPS, "
71
+ "but '--root-certificates' was also specified. Please remove "
72
+ "the '--root-certificates' option when running in insecure mode, "
73
+ "or omit '--insecure' to use HTTPS."
74
+ )
75
+ log(
76
+ WARN,
77
+ "Option `--insecure` was set. Starting insecure HTTP channel to %s.",
78
+ grpc_server_address,
79
+ )
80
+ root_certificates = None
81
+ else:
82
+ # Load the certificates if provided, or load the system certificates
83
+ if not isfile(root_cert_path):
84
+ sys.exit("Path argument `--root-certificates` does not point to a file.")
85
+ root_certificates = Path(root_cert_path).read_bytes()
86
+ log(
87
+ DEBUG,
88
+ "Starting secure HTTPS channel to %s "
89
+ "with the following certificates: %s.",
90
+ grpc_server_address,
91
+ root_cert_path,
92
+ )
93
+ return root_certificates
94
+
95
+
96
+ def try_obtain_server_certificates(
97
+ args: argparse.Namespace,
98
+ transport_type: str,
99
+ ) -> Optional[tuple[bytes, bytes, bytes]]:
100
+ """Validate and return the CA cert, server cert, and server private key."""
101
+ if args.insecure:
102
+ log(WARN, "Option `--insecure` was set. Starting insecure HTTP server.")
103
+ return None
104
+ # Check if certificates are provided
105
+ if transport_type in [TRANSPORT_TYPE_GRPC_RERE, TRANSPORT_TYPE_GRPC_ADAPTER]:
106
+ if args.ssl_certfile and args.ssl_keyfile and args.ssl_ca_certfile:
107
+ if not isfile(args.ssl_ca_certfile):
108
+ sys.exit("Path argument `--ssl-ca-certfile` does not point to a file.")
109
+ if not isfile(args.ssl_certfile):
110
+ sys.exit("Path argument `--ssl-certfile` does not point to a file.")
111
+ if not isfile(args.ssl_keyfile):
112
+ sys.exit("Path argument `--ssl-keyfile` does not point to a file.")
113
+ certificates = (
114
+ Path(args.ssl_ca_certfile).read_bytes(), # CA certificate
115
+ Path(args.ssl_certfile).read_bytes(), # server certificate
116
+ Path(args.ssl_keyfile).read_bytes(), # server private key
117
+ )
118
+ return certificates
119
+ if args.ssl_certfile or args.ssl_keyfile or args.ssl_ca_certfile:
120
+ sys.exit(
121
+ "You need to provide valid file paths to `--ssl-certfile`, "
122
+ "`--ssl-keyfile`, and `—-ssl-ca-certfile` to create a secure "
123
+ "connection in Fleet API server (gRPC-rere)."
124
+ )
125
+ if transport_type == TRANSPORT_TYPE_REST:
126
+ if args.ssl_certfile and args.ssl_keyfile:
127
+ if not isfile(args.ssl_certfile):
128
+ sys.exit("Path argument `--ssl-certfile` does not point to a file.")
129
+ if not isfile(args.ssl_keyfile):
130
+ sys.exit("Path argument `--ssl-keyfile` does not point to a file.")
131
+ certificates = (
132
+ b"",
133
+ Path(args.ssl_certfile).read_bytes(), # server certificate
134
+ Path(args.ssl_keyfile).read_bytes(), # server private key
135
+ )
136
+ return certificates
137
+ if args.ssl_certfile or args.ssl_keyfile:
138
+ sys.exit(
139
+ "You need to provide valid file paths to `--ssl-certfile` "
140
+ "and `--ssl-keyfile` to create a secure connection "
141
+ "in Fleet API server (REST, experimental)."
142
+ )
143
+ sys.exit(
144
+ "Certificates are required unless running in insecure mode. "
145
+ "Please provide certificate paths to `--ssl-certfile`, "
146
+ "`--ssl-keyfile`, and `—-ssl-ca-certfile` or run the server "
147
+ "in insecure mode using '--insecure' if you understand the risks."
148
+ )
flwr/common/config.py CHANGED
@@ -22,6 +22,7 @@ from typing import Any, Optional, Union, cast, get_args
22
22
  import tomli
23
23
 
24
24
  from flwr.cli.config_utils import get_fab_config, validate_fields
25
+ from flwr.common import ConfigsRecord
25
26
  from flwr.common.constant import (
26
27
  APP_DIR,
27
28
  FAB_CONFIG_FILE,
@@ -229,3 +230,12 @@ def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
229
230
  config["project"]["version"],
230
231
  f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
231
232
  )
233
+
234
+
235
+ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
236
+ """Construct a `ConfigsRecord` out of a `UserConfig`."""
237
+ c_record = ConfigsRecord()
238
+ for k, v in config.items():
239
+ c_record[k] = v
240
+
241
+ return c_record