flwr-nightly 1.23.0.dev20251007__py3-none-any.whl → 1.23.0.dev20251009__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/cli/auth_plugin/__init__.py +7 -3
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +4 -13
- flwr/cli/ls.py +2 -2
- flwr/cli/pull.py +2 -2
- flwr/cli/run/run.py +2 -2
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/create.py +137 -11
- flwr/cli/supernode/delete.py +88 -10
- flwr/cli/supernode/ls.py +2 -2
- flwr/cli/utils.py +65 -55
- flwr/client/grpc_rere_client/connection.py +6 -4
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +2 -2
- flwr/client/rest_client/connection.py +7 -1
- flwr/common/constant.py +13 -0
- flwr/proto/control_pb2.py +1 -1
- flwr/proto/control_pb2.pyi +2 -2
- flwr/proto/fleet_pb2.py +22 -22
- flwr/proto/fleet_pb2.pyi +4 -1
- flwr/proto/node_pb2.py +2 -2
- flwr/proto/node_pb2.pyi +4 -1
- flwr/server/app.py +32 -31
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +8 -4
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +18 -37
- flwr/server/superlink/fleet/message_handler/message_handler.py +5 -3
- flwr/server/superlink/fleet/vce/vce_api.py +10 -1
- flwr/server/superlink/linkstate/in_memory_linkstate.py +52 -54
- flwr/server/superlink/linkstate/linkstate.py +20 -10
- flwr/server/superlink/linkstate/sqlite_linkstate.py +54 -61
- flwr/server/utils/validator.py +2 -3
- flwr/supercore/primitives/asymmetric.py +8 -0
- flwr/superlink/auth_plugin/__init__.py +29 -0
- flwr/superlink/servicer/control/control_grpc.py +9 -7
- flwr/superlink/servicer/control/control_servicer.py +89 -48
- {flwr_nightly-1.23.0.dev20251007.dist-info → flwr_nightly-1.23.0.dev20251009.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251007.dist-info → flwr_nightly-1.23.0.dev20251009.dist-info}/RECORD +38 -38
- {flwr_nightly-1.23.0.dev20251007.dist-info → flwr_nightly-1.23.0.dev20251009.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251007.dist-info → flwr_nightly-1.23.0.dev20251009.dist-info}/entry_points.txt +0 -0
flwr/cli/auth_plugin/__init__.py
CHANGED
@@ -22,9 +22,13 @@ from .noop_auth_plugin import NoOpCliAuthPlugin
|
|
22
22
|
from .oidc_cli_plugin import OidcCliPlugin
|
23
23
|
|
24
24
|
|
25
|
-
def
|
25
|
+
def get_cli_plugin_class(authn_type: str) -> type[CliAuthPlugin]:
|
26
26
|
"""Return all CLI authentication plugins."""
|
27
|
-
|
27
|
+
if authn_type == AuthnType.NOOP:
|
28
|
+
return NoOpCliAuthPlugin
|
29
|
+
if authn_type == AuthnType.OIDC:
|
30
|
+
return OidcCliPlugin
|
31
|
+
raise ValueError(f"Unsupported authentication type: {authn_type}")
|
28
32
|
|
29
33
|
|
30
34
|
__all__ = [
|
@@ -32,5 +36,5 @@ __all__ = [
|
|
32
36
|
"LoginError",
|
33
37
|
"NoOpCliAuthPlugin",
|
34
38
|
"OidcCliPlugin",
|
35
|
-
"
|
39
|
+
"get_cli_plugin_class",
|
36
40
|
]
|
flwr/cli/log.py
CHANGED
@@ -35,7 +35,7 @@ from flwr.common.logger import log as logger
|
|
35
35
|
from flwr.proto.control_pb2 import StreamLogsRequest # pylint: disable=E0611
|
36
36
|
from flwr.proto.control_pb2_grpc import ControlStub
|
37
37
|
|
38
|
-
from .utils import flwr_cli_grpc_exc_handler, init_channel,
|
38
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
39
39
|
|
40
40
|
|
41
41
|
class AllLogsRetrieved(BaseException):
|
@@ -186,7 +186,7 @@ def _log_with_control_api(
|
|
186
186
|
run_id: int,
|
187
187
|
stream: bool,
|
188
188
|
) -> None:
|
189
|
-
auth_plugin =
|
189
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
190
190
|
channel = init_channel(app, federation_config, auth_plugin)
|
191
191
|
|
192
192
|
if stream:
|
flwr/cli/login/login.py
CHANGED
@@ -20,7 +20,7 @@ from typing import Annotated, Optional
|
|
20
20
|
|
21
21
|
import typer
|
22
22
|
|
23
|
-
from flwr.cli.auth_plugin import LoginError
|
23
|
+
from flwr.cli.auth_plugin import LoginError, NoOpCliAuthPlugin
|
24
24
|
from flwr.cli.config_utils import (
|
25
25
|
exit_if_no_address,
|
26
26
|
get_insecure_flag,
|
@@ -40,7 +40,7 @@ from ..utils import (
|
|
40
40
|
account_auth_enabled,
|
41
41
|
flwr_cli_grpc_exc_handler,
|
42
42
|
init_channel,
|
43
|
-
|
43
|
+
load_cli_auth_plugin,
|
44
44
|
)
|
45
45
|
|
46
46
|
|
@@ -95,7 +95,7 @@ def login( # pylint: disable=R0914
|
|
95
95
|
)
|
96
96
|
raise typer.Exit(code=1)
|
97
97
|
|
98
|
-
channel = init_channel(app, federation_config,
|
98
|
+
channel = init_channel(app, federation_config, NoOpCliAuthPlugin(Path()))
|
99
99
|
stub = ControlStub(channel)
|
100
100
|
|
101
101
|
login_request = GetLoginDetailsRequest()
|
@@ -104,16 +104,7 @@ def login( # pylint: disable=R0914
|
|
104
104
|
|
105
105
|
# Get the auth plugin
|
106
106
|
authn_type = login_response.authn_type
|
107
|
-
auth_plugin =
|
108
|
-
app, federation, federation_config, authn_type
|
109
|
-
)
|
110
|
-
if auth_plugin is None:
|
111
|
-
typer.secho(
|
112
|
-
f'❌ Authentication type "{authn_type}" not found',
|
113
|
-
fg=typer.colors.RED,
|
114
|
-
bold=True,
|
115
|
-
)
|
116
|
-
raise typer.Exit(code=1)
|
107
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config, authn_type)
|
117
108
|
|
118
109
|
# Login
|
119
110
|
details = AccountAuthLoginDetails(
|
flwr/cli/ls.py
CHANGED
@@ -44,7 +44,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
44
44
|
)
|
45
45
|
from flwr.proto.control_pb2_grpc import ControlStub
|
46
46
|
|
47
|
-
from .utils import flwr_cli_grpc_exc_handler, init_channel,
|
47
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
48
48
|
|
49
49
|
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
50
50
|
|
@@ -127,7 +127,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
127
127
|
raise ValueError(
|
128
128
|
"The options '--runs' and '--run-id' are mutually exclusive."
|
129
129
|
)
|
130
|
-
auth_plugin =
|
130
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
131
131
|
channel = init_channel(app, federation_config, auth_plugin)
|
132
132
|
stub = ControlStub(channel)
|
133
133
|
|
flwr/cli/pull.py
CHANGED
@@ -34,7 +34,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
34
34
|
)
|
35
35
|
from flwr.proto.control_pb2_grpc import ControlStub
|
36
36
|
|
37
|
-
from .utils import flwr_cli_grpc_exc_handler, init_channel,
|
37
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
38
38
|
|
39
39
|
|
40
40
|
def pull( # pylint: disable=R0914
|
@@ -74,7 +74,7 @@ def pull( # pylint: disable=R0914
|
|
74
74
|
channel = None
|
75
75
|
try:
|
76
76
|
|
77
|
-
auth_plugin =
|
77
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
78
78
|
channel = init_channel(app, federation_config, auth_plugin)
|
79
79
|
stub = ControlStub(channel)
|
80
80
|
with flwr_cli_grpc_exc_handler():
|
flwr/cli/run/run.py
CHANGED
@@ -45,7 +45,7 @@ from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
|
|
45
45
|
from flwr.proto.control_pb2_grpc import ControlStub
|
46
46
|
|
47
47
|
from ..log import start_stream
|
48
|
-
from ..utils import flwr_cli_grpc_exc_handler, init_channel,
|
48
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
49
49
|
|
50
50
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
51
51
|
|
@@ -148,7 +148,7 @@ def _run_with_control_api(
|
|
148
148
|
) -> None:
|
149
149
|
channel = None
|
150
150
|
try:
|
151
|
-
auth_plugin =
|
151
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
152
152
|
channel = init_channel(app, federation_config, auth_plugin)
|
153
153
|
stub = ControlStub(channel)
|
154
154
|
|
flwr/cli/stop.py
CHANGED
@@ -38,7 +38,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
38
38
|
)
|
39
39
|
from flwr.proto.control_pb2_grpc import ControlStub
|
40
40
|
|
41
|
-
from .utils import flwr_cli_grpc_exc_handler, init_channel,
|
41
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
42
42
|
|
43
43
|
|
44
44
|
def stop( # pylint: disable=R0914
|
@@ -89,7 +89,7 @@ def stop( # pylint: disable=R0914
|
|
89
89
|
exit_if_no_address(federation_config, "stop")
|
90
90
|
channel = None
|
91
91
|
try:
|
92
|
-
auth_plugin =
|
92
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
93
93
|
channel = init_channel(app, federation_config, auth_plugin)
|
94
94
|
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
95
95
|
|
flwr/cli/supernode/create.py
CHANGED
@@ -14,10 +14,16 @@
|
|
14
14
|
# ==============================================================================
|
15
15
|
"""Flower command line interface `supernode create` command."""
|
16
16
|
|
17
|
+
|
18
|
+
import io
|
19
|
+
import json
|
17
20
|
from pathlib import Path
|
18
21
|
from typing import Annotated, Optional
|
19
22
|
|
20
23
|
import typer
|
24
|
+
from cryptography.hazmat.primitives import serialization
|
25
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
26
|
+
from rich.console import Console
|
21
27
|
|
22
28
|
from flwr.cli.config_utils import (
|
23
29
|
exit_if_no_address,
|
@@ -25,14 +31,23 @@ from flwr.cli.config_utils import (
|
|
25
31
|
process_loaded_project_config,
|
26
32
|
validate_federation_in_project_config,
|
27
33
|
)
|
28
|
-
from flwr.common.constant import FAB_CONFIG_FILE
|
34
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
35
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
36
|
+
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
37
|
+
CreateNodeCliRequest,
|
38
|
+
CreateNodeCliResponse,
|
39
|
+
)
|
40
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
41
|
+
from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
|
42
|
+
|
43
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
29
44
|
|
30
45
|
|
31
46
|
def create( # pylint: disable=R0914
|
32
47
|
public_key: Annotated[
|
33
48
|
Path,
|
34
49
|
typer.Argument(
|
35
|
-
help="Path to
|
50
|
+
help="Path to a P-384 (or any other NIST EC curve) public key file.",
|
36
51
|
),
|
37
52
|
],
|
38
53
|
app: Annotated[
|
@@ -43,16 +58,127 @@ def create( # pylint: disable=R0914
|
|
43
58
|
Optional[str],
|
44
59
|
typer.Argument(help="Name of the federation"),
|
45
60
|
] = None,
|
61
|
+
output_format: Annotated[
|
62
|
+
str,
|
63
|
+
typer.Option(
|
64
|
+
"--format",
|
65
|
+
case_sensitive=False,
|
66
|
+
help="Format output using 'default' view or 'json'",
|
67
|
+
),
|
68
|
+
] = CliOutputFormat.DEFAULT,
|
46
69
|
) -> None:
|
47
70
|
"""Add a SuperNode to the federation."""
|
48
|
-
|
71
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
72
|
+
captured_output = io.StringIO()
|
73
|
+
|
74
|
+
# Load public key
|
75
|
+
public_key_path = Path(public_key)
|
76
|
+
public_key_bytes = try_load_public_key(public_key_path)
|
77
|
+
|
78
|
+
try:
|
79
|
+
if suppress_output:
|
80
|
+
redirect_output(captured_output)
|
81
|
+
|
82
|
+
# Load and validate federation config
|
83
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
84
|
+
|
85
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
86
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
87
|
+
config = process_loaded_project_config(config, errors, warnings)
|
88
|
+
federation, federation_config = validate_federation_in_project_config(
|
89
|
+
federation, config
|
90
|
+
)
|
91
|
+
exit_if_no_address(federation_config, "supernode create")
|
92
|
+
|
93
|
+
channel = None
|
94
|
+
try:
|
95
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
96
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
97
|
+
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
98
|
+
|
99
|
+
_create_node(
|
100
|
+
stub=stub, public_key=public_key_bytes, output_format=output_format
|
101
|
+
)
|
102
|
+
|
103
|
+
except ValueError as err:
|
104
|
+
typer.secho(
|
105
|
+
f"❌ {err}",
|
106
|
+
fg=typer.colors.RED,
|
107
|
+
bold=True,
|
108
|
+
)
|
109
|
+
raise typer.Exit(code=1) from err
|
110
|
+
finally:
|
111
|
+
if channel:
|
112
|
+
channel.close()
|
113
|
+
|
114
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
115
|
+
if suppress_output:
|
116
|
+
restore_output()
|
117
|
+
e_message = captured_output.getvalue()
|
118
|
+
print_json_error(e_message, err)
|
119
|
+
else:
|
120
|
+
typer.secho(
|
121
|
+
f"{err}",
|
122
|
+
fg=typer.colors.RED,
|
123
|
+
bold=True,
|
124
|
+
)
|
125
|
+
finally:
|
126
|
+
if suppress_output:
|
127
|
+
restore_output()
|
128
|
+
captured_output.close()
|
129
|
+
|
130
|
+
|
131
|
+
def _create_node(stub: ControlStub, public_key: bytes, output_format: str) -> None:
|
132
|
+
"""Create a node."""
|
133
|
+
with flwr_cli_grpc_exc_handler():
|
134
|
+
response: CreateNodeCliResponse = stub.CreateNodeCli(
|
135
|
+
request=CreateNodeCliRequest(public_key=public_key)
|
136
|
+
)
|
137
|
+
if response.node_id:
|
138
|
+
typer.secho(
|
139
|
+
f"✅ Node {response.node_id} created successfully.", fg=typer.colors.GREEN
|
140
|
+
)
|
141
|
+
if output_format == CliOutputFormat.JSON:
|
142
|
+
run_output = json.dumps(
|
143
|
+
{
|
144
|
+
"success": True,
|
145
|
+
"node-id": response.node_id,
|
146
|
+
}
|
147
|
+
)
|
148
|
+
restore_output()
|
149
|
+
Console().print_json(run_output)
|
150
|
+
else:
|
151
|
+
typer.secho("❌ Node couldn't be created.", fg=typer.colors.RED)
|
152
|
+
|
153
|
+
|
154
|
+
def try_load_public_key(public_key_path: Path) -> bytes:
|
155
|
+
"""Try to load a public key from a file."""
|
156
|
+
if not public_key_path.exists():
|
157
|
+
typer.secho(
|
158
|
+
f"❌ Public key file '{public_key_path}' does not exist.",
|
159
|
+
fg=typer.colors.RED,
|
160
|
+
bold=True,
|
161
|
+
)
|
162
|
+
raise typer.Exit(code=1)
|
163
|
+
|
164
|
+
with open(public_key_path, "rb") as key_file:
|
165
|
+
try:
|
166
|
+
public_key = serialization.load_pem_public_key(key_file.read())
|
167
|
+
|
168
|
+
if not isinstance(public_key, ec.EllipticCurvePublicKey):
|
169
|
+
raise ValueError(f"Not an EC public key, got {type(public_key)}")
|
49
170
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
)
|
56
|
-
exit_if_no_address(federation_config, "supernode add")
|
171
|
+
# Verify it's one of the approved NIST curves
|
172
|
+
if not uses_nist_ec_curve(public_key):
|
173
|
+
raise ValueError(
|
174
|
+
f"EC curve {public_key.curve.name} is not an approved NIST curve"
|
175
|
+
)
|
57
176
|
|
58
|
-
|
177
|
+
except ValueError as err:
|
178
|
+
typer.secho(
|
179
|
+
f"❌ Unable to load public key from '{public_key_path}': {err}",
|
180
|
+
fg=typer.colors.RED,
|
181
|
+
bold=True,
|
182
|
+
)
|
183
|
+
raise typer.Exit(code=1) from err
|
184
|
+
return public_key_to_bytes(public_key)
|
flwr/cli/supernode/delete.py
CHANGED
@@ -14,10 +14,14 @@
|
|
14
14
|
# ==============================================================================
|
15
15
|
"""Flower command line interface `supernode delete` command."""
|
16
16
|
|
17
|
+
|
18
|
+
import io
|
19
|
+
import json
|
17
20
|
from pathlib import Path
|
18
21
|
from typing import Annotated, Optional
|
19
22
|
|
20
23
|
import typer
|
24
|
+
from rich.console import Console
|
21
25
|
|
22
26
|
from flwr.cli.config_utils import (
|
23
27
|
exit_if_no_address,
|
@@ -25,7 +29,12 @@ from flwr.cli.config_utils import (
|
|
25
29
|
process_loaded_project_config,
|
26
30
|
validate_federation_in_project_config,
|
27
31
|
)
|
28
|
-
from flwr.common.constant import FAB_CONFIG_FILE
|
32
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
33
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
34
|
+
from flwr.proto.control_pb2 import DeleteNodeCliRequest # pylint: disable=E0611
|
35
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
36
|
+
|
37
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
29
38
|
|
30
39
|
|
31
40
|
def delete( # pylint: disable=R0914
|
@@ -43,16 +52,85 @@ def delete( # pylint: disable=R0914
|
|
43
52
|
Optional[str],
|
44
53
|
typer.Argument(help="Name of the federation"),
|
45
54
|
] = None,
|
55
|
+
output_format: Annotated[
|
56
|
+
str,
|
57
|
+
typer.Option(
|
58
|
+
"--format",
|
59
|
+
case_sensitive=False,
|
60
|
+
help="Format output using 'default' view or 'json'",
|
61
|
+
),
|
62
|
+
] = CliOutputFormat.DEFAULT,
|
46
63
|
) -> None:
|
47
64
|
"""Remove a SuperNode from the federation."""
|
48
|
-
|
65
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
66
|
+
captured_output = io.StringIO()
|
67
|
+
|
68
|
+
try:
|
69
|
+
if suppress_output:
|
70
|
+
redirect_output(captured_output)
|
71
|
+
|
72
|
+
# Load and validate federation config
|
73
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
74
|
+
|
75
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
76
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
77
|
+
config = process_loaded_project_config(config, errors, warnings)
|
78
|
+
federation, federation_config = validate_federation_in_project_config(
|
79
|
+
federation, config
|
80
|
+
)
|
81
|
+
exit_if_no_address(federation_config, "supernode remove")
|
49
82
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
)
|
56
|
-
exit_if_no_address(federation_config, "supernode rm")
|
83
|
+
channel = None
|
84
|
+
try:
|
85
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
86
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
87
|
+
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
57
88
|
|
58
|
-
|
89
|
+
_delete_node(stub=stub, node_id=node_id, output_format=output_format)
|
90
|
+
|
91
|
+
except ValueError as err:
|
92
|
+
typer.secho(
|
93
|
+
f"❌ {err}",
|
94
|
+
fg=typer.colors.RED,
|
95
|
+
bold=True,
|
96
|
+
)
|
97
|
+
raise typer.Exit(code=1) from err
|
98
|
+
finally:
|
99
|
+
if channel:
|
100
|
+
channel.close()
|
101
|
+
|
102
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
103
|
+
if suppress_output:
|
104
|
+
restore_output()
|
105
|
+
e_message = captured_output.getvalue()
|
106
|
+
print_json_error(e_message, err)
|
107
|
+
else:
|
108
|
+
typer.secho(
|
109
|
+
f"{err}",
|
110
|
+
fg=typer.colors.RED,
|
111
|
+
bold=True,
|
112
|
+
)
|
113
|
+
finally:
|
114
|
+
if suppress_output:
|
115
|
+
restore_output()
|
116
|
+
captured_output.close()
|
117
|
+
|
118
|
+
|
119
|
+
def _delete_node(
|
120
|
+
stub: ControlStub,
|
121
|
+
node_id: int,
|
122
|
+
output_format: str,
|
123
|
+
) -> None:
|
124
|
+
"""Delete a SuperNode from the federation."""
|
125
|
+
with flwr_cli_grpc_exc_handler():
|
126
|
+
stub.DeleteNodeCli(request=DeleteNodeCliRequest(node_id=node_id))
|
127
|
+
typer.secho(f"✅ SuperNode {node_id} deleted successfully.", fg=typer.colors.GREEN)
|
128
|
+
if output_format == CliOutputFormat.JSON:
|
129
|
+
run_output = json.dumps(
|
130
|
+
{
|
131
|
+
"success": True,
|
132
|
+
"node-id": node_id,
|
133
|
+
}
|
134
|
+
)
|
135
|
+
restore_output()
|
136
|
+
Console().print_json(run_output)
|
flwr/cli/supernode/ls.py
CHANGED
@@ -42,7 +42,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
42
42
|
from flwr.proto.control_pb2_grpc import ControlStub
|
43
43
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
44
44
|
|
45
|
-
from ..utils import flwr_cli_grpc_exc_handler, init_channel,
|
45
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
46
46
|
|
47
47
|
_NodeListType = tuple[int, str, str, str, str, str, str, str]
|
48
48
|
|
@@ -94,7 +94,7 @@ def ls( # pylint: disable=R0914
|
|
94
94
|
exit_if_no_address(federation_config, f"supernode {command_name}")
|
95
95
|
channel = None
|
96
96
|
try:
|
97
|
-
auth_plugin =
|
97
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
98
98
|
channel = init_channel(app, federation_config, auth_plugin)
|
99
99
|
stub = ControlStub(channel)
|
100
100
|
typer.echo("📄 Listing all nodes...")
|
flwr/cli/utils.py
CHANGED
@@ -32,8 +32,12 @@ from flwr.common.constant import (
|
|
32
32
|
FLWR_DIR,
|
33
33
|
NO_ACCOUNT_AUTH_MESSAGE,
|
34
34
|
NO_ARTIFACT_PROVIDER_MESSAGE,
|
35
|
+
NODE_NOT_FOUND_MESSAGE,
|
36
|
+
PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
|
37
|
+
PUBLIC_KEY_NOT_VALID,
|
35
38
|
PULL_UNFINISHED_RUN_MESSAGE,
|
36
39
|
RUN_ID_NOT_FOUND_MESSAGE,
|
40
|
+
AuthnType,
|
37
41
|
)
|
38
42
|
from flwr.common.grpc import (
|
39
43
|
GRPC_MAX_MESSAGE_LENGTH,
|
@@ -41,7 +45,7 @@ from flwr.common.grpc import (
|
|
41
45
|
on_channel_state_change,
|
42
46
|
)
|
43
47
|
|
44
|
-
from .auth_plugin import CliAuthPlugin,
|
48
|
+
from .auth_plugin import CliAuthPlugin, get_cli_plugin_class
|
45
49
|
from .cli_account_auth_interceptor import CliAccountAuthInterceptor
|
46
50
|
from .config_utils import validate_certificate_in_federation_config
|
47
51
|
|
@@ -230,71 +234,54 @@ def account_auth_enabled(federation_config: dict[str, Any]) -> bool:
|
|
230
234
|
return enabled
|
231
235
|
|
232
236
|
|
233
|
-
def
|
237
|
+
def retrieve_authn_type(config_path: Path) -> str:
|
238
|
+
"""Retrieve the auth type from the config file or return NOOP if not found."""
|
239
|
+
try:
|
240
|
+
with config_path.open("r", encoding="utf-8") as file:
|
241
|
+
json_file = json.load(file)
|
242
|
+
authn_type: str = json_file[AUTHN_TYPE_JSON_KEY]
|
243
|
+
return authn_type
|
244
|
+
except (FileNotFoundError, KeyError):
|
245
|
+
return AuthnType.NOOP
|
246
|
+
|
247
|
+
|
248
|
+
def load_cli_auth_plugin(
|
234
249
|
root_dir: Path,
|
235
250
|
federation: str,
|
236
251
|
federation_config: dict[str, Any],
|
237
252
|
authn_type: Optional[str] = None,
|
238
|
-
) ->
|
253
|
+
) -> CliAuthPlugin:
|
239
254
|
"""Load the CLI-side account auth plugin for the given authn type."""
|
240
|
-
#
|
241
|
-
if not account_auth_enabled(federation_config):
|
242
|
-
return None
|
243
|
-
|
255
|
+
# Find the path to the account auth config file
|
244
256
|
config_path = get_account_auth_config_path(root_dir, federation)
|
245
257
|
|
246
|
-
#
|
247
|
-
#
|
258
|
+
# Determine the auth type if not provided
|
259
|
+
# Only `flwr login` command can provide `authn_type` explicitly, as it can query the
|
260
|
+
# SuperLink for the auth type.
|
248
261
|
if authn_type is None:
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
authn_type = json_file[AUTHN_TYPE_JSON_KEY]
|
253
|
-
except (FileNotFoundError, KeyError):
|
254
|
-
typer.secho(
|
255
|
-
"❌ Missing or invalid credentials for account authentication. "
|
256
|
-
"Please run `flwr login` to authenticate.",
|
257
|
-
fg=typer.colors.RED,
|
258
|
-
bold=True,
|
259
|
-
)
|
260
|
-
raise typer.Exit(code=1) from None
|
262
|
+
authn_type = AuthnType.NOOP
|
263
|
+
if account_auth_enabled(federation_config):
|
264
|
+
authn_type = retrieve_authn_type(config_path)
|
261
265
|
|
262
266
|
# Retrieve auth plugin class and instantiate it
|
263
267
|
try:
|
264
|
-
|
265
|
-
auth_plugin_class = all_plugins[authn_type]
|
268
|
+
auth_plugin_class = get_cli_plugin_class(authn_type)
|
266
269
|
return auth_plugin_class(config_path)
|
267
|
-
except
|
270
|
+
except ValueError:
|
268
271
|
typer.echo(f"❌ Unknown account authentication type: {authn_type}")
|
269
272
|
raise typer.Exit(code=1) from None
|
270
|
-
except ImportError:
|
271
|
-
typer.echo("❌ No authentication plugins are currently supported.")
|
272
|
-
raise typer.Exit(code=1) from None
|
273
273
|
|
274
274
|
|
275
275
|
def init_channel(
|
276
|
-
app: Path, federation_config: dict[str, Any], auth_plugin:
|
276
|
+
app: Path, federation_config: dict[str, Any], auth_plugin: CliAuthPlugin
|
277
277
|
) -> grpc.Channel:
|
278
278
|
"""Initialize gRPC channel to the Control API."""
|
279
279
|
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
280
280
|
app, federation_config
|
281
281
|
)
|
282
282
|
|
283
|
-
#
|
284
|
-
|
285
|
-
if auth_plugin is not None:
|
286
|
-
# Check if TLS is enabled. If not, raise an error
|
287
|
-
if insecure:
|
288
|
-
typer.secho(
|
289
|
-
"❌ Account authentication requires TLS to be enabled. "
|
290
|
-
"Remove `insecure = true` from the federation configuration.",
|
291
|
-
fg=typer.colors.RED,
|
292
|
-
bold=True,
|
293
|
-
)
|
294
|
-
raise typer.Exit(code=1)
|
295
|
-
|
296
|
-
auth_plugin.load_tokens()
|
297
|
-
interceptors.append(CliAccountAuthInterceptor(auth_plugin))
|
283
|
+
# Load tokens
|
284
|
+
auth_plugin.load_tokens()
|
298
285
|
|
299
286
|
# Create the gRPC channel
|
300
287
|
channel = create_channel(
|
@@ -302,14 +289,14 @@ def init_channel(
|
|
302
289
|
insecure=insecure,
|
303
290
|
root_certificates=root_certificates_bytes,
|
304
291
|
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
305
|
-
interceptors=
|
292
|
+
interceptors=[CliAccountAuthInterceptor(auth_plugin)],
|
306
293
|
)
|
307
294
|
channel.subscribe(on_channel_state_change)
|
308
295
|
return channel
|
309
296
|
|
310
297
|
|
311
298
|
@contextmanager
|
312
|
-
def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
299
|
+
def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-branches
|
313
300
|
"""Context manager to handle specific gRPC errors.
|
314
301
|
|
315
302
|
It catches grpc.RpcError exceptions with UNAUTHENTICATED, UNIMPLEMENTED,
|
@@ -367,16 +354,21 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
|
367
354
|
bold=True,
|
368
355
|
)
|
369
356
|
raise typer.Exit(code=1) from None
|
370
|
-
if (
|
371
|
-
e.
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
)
|
379
|
-
|
357
|
+
if e.code() == grpc.StatusCode.NOT_FOUND:
|
358
|
+
if e.details() == RUN_ID_NOT_FOUND_MESSAGE: # pylint: disable=E1101
|
359
|
+
typer.secho(
|
360
|
+
"❌ Run ID not found.",
|
361
|
+
fg=typer.colors.RED,
|
362
|
+
bold=True,
|
363
|
+
)
|
364
|
+
raise typer.Exit(code=1) from None
|
365
|
+
if e.details() == NODE_NOT_FOUND_MESSAGE: # pylint: disable=E1101
|
366
|
+
typer.secho(
|
367
|
+
"❌ Node ID not found for this account.",
|
368
|
+
fg=typer.colors.RED,
|
369
|
+
bold=True,
|
370
|
+
)
|
371
|
+
raise typer.Exit(code=1) from None
|
380
372
|
if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
|
381
373
|
if e.details() == PULL_UNFINISHED_RUN_MESSAGE: # pylint: disable=E1101
|
382
374
|
typer.secho(
|
@@ -386,4 +378,22 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
|
386
378
|
bold=True,
|
387
379
|
)
|
388
380
|
raise typer.Exit(code=1) from None
|
381
|
+
if (
|
382
|
+
e.details() == PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
|
383
|
+
): # pylint: disable=E1101
|
384
|
+
typer.secho(
|
385
|
+
"❌ The provided public key is already in use by another "
|
386
|
+
"SuperNode.",
|
387
|
+
fg=typer.colors.RED,
|
388
|
+
bold=True,
|
389
|
+
)
|
390
|
+
raise typer.Exit(code=1) from None
|
391
|
+
if e.details() == PUBLIC_KEY_NOT_VALID: # pylint: disable=E1101
|
392
|
+
typer.secho(
|
393
|
+
"❌ The provided public key is invalid. Please provide a valid "
|
394
|
+
"NIST EC public key.",
|
395
|
+
fg=typer.colors.RED,
|
396
|
+
bold=True,
|
397
|
+
)
|
398
|
+
raise typer.Exit(code=1) from None
|
389
399
|
raise
|