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.
- flwr/cli/app.py +2 -0
- flwr/cli/build.py +37 -0
- flwr/cli/install.py +5 -3
- flwr/cli/ls.py +228 -0
- flwr/cli/run/run.py +16 -5
- flwr/client/app.py +68 -19
- flwr/client/clientapp/app.py +51 -35
- flwr/client/grpc_rere_client/connection.py +2 -12
- flwr/client/nodestate/__init__.py +25 -0
- flwr/client/nodestate/in_memory_nodestate.py +38 -0
- flwr/client/nodestate/nodestate.py +30 -0
- flwr/client/nodestate/nodestate_factory.py +37 -0
- flwr/client/rest_client/connection.py +4 -14
- flwr/client/supernode/app.py +57 -53
- flwr/common/args.py +148 -0
- flwr/common/config.py +10 -0
- flwr/common/constant.py +21 -7
- flwr/common/date.py +18 -0
- flwr/common/logger.py +6 -2
- flwr/common/object_ref.py +47 -16
- flwr/common/serde.py +10 -0
- flwr/common/typing.py +32 -11
- flwr/proto/exec_pb2.py +23 -17
- flwr/proto/exec_pb2.pyi +50 -20
- flwr/proto/exec_pb2_grpc.py +34 -0
- flwr/proto/exec_pb2_grpc.pyi +13 -0
- flwr/proto/run_pb2.py +32 -27
- flwr/proto/run_pb2.pyi +44 -1
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +34 -0
- flwr/proto/simulationio_pb2_grpc.pyi +13 -0
- flwr/server/app.py +83 -87
- flwr/server/driver/driver.py +1 -1
- flwr/server/driver/grpc_driver.py +6 -20
- flwr/server/driver/inmemory_driver.py +1 -3
- flwr/server/run_serverapp.py +8 -238
- flwr/server/serverapp/app.py +44 -89
- flwr/server/strategy/aggregate.py +4 -4
- flwr/server/superlink/fleet/rest_rere/rest_api.py +10 -9
- flwr/server/superlink/linkstate/in_memory_linkstate.py +76 -62
- flwr/server/superlink/linkstate/linkstate.py +24 -9
- flwr/server/superlink/linkstate/sqlite_linkstate.py +87 -128
- flwr/server/superlink/linkstate/utils.py +191 -32
- flwr/server/superlink/simulation/simulationio_servicer.py +22 -1
- flwr/simulation/__init__.py +3 -1
- flwr/simulation/app.py +245 -352
- flwr/simulation/legacy_app.py +402 -0
- flwr/simulation/run_simulation.py +8 -19
- flwr/simulation/simulationio_connection.py +2 -2
- flwr/superexec/deployment.py +13 -7
- flwr/superexec/exec_servicer.py +32 -3
- flwr/superexec/executor.py +4 -3
- flwr/superexec/simulation.py +52 -145
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/METADATA +10 -7
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/RECORD +58 -51
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/entry_points.txt +1 -0
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.13.0.dev20241106.dist-info → flwr_nightly-1.13.0.dev20241117.dist-info}/WHEEL +0 -0
flwr/client/clientapp/app.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
"
|
|
66
|
+
"Starting isolated `ClientApp` connected to SuperNode's ClientAppIo API at %s "
|
|
77
67
|
"with token %s",
|
|
78
|
-
args.
|
|
68
|
+
args.clientappio_api_address,
|
|
79
69
|
args.token,
|
|
80
70
|
)
|
|
81
|
-
run_clientapp(
|
|
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
|
-
|
|
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=
|
|
104
|
-
insecure=
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
flwr/client/supernode/app.py
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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=
|
|
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` (
|
|
202
|
-
"`subprocess`, `process`).
|
|
203
|
-
"
|
|
204
|
-
"
|
|
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
|
-
"--
|
|
209
|
-
default=
|
|
210
|
-
help="
|
|
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
|