Skip to content

Commit

Permalink
add get_client_config
Browse files Browse the repository at this point in the history
  • Loading branch information
Wrede committed Aug 18, 2023
1 parent 6fa1979 commit 77b83cd
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 13 deletions.
4 changes: 4 additions & 0 deletions config/settings-reducer.yaml.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
network_id: fedn-network
controller:
host: api-server
port: 8092
debug: True

statestore:
type: MongoDB
Expand Down
6 changes: 4 additions & 2 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ Now navigate to http://localhost:8090/network and download the client config fil

.. code:: python
>>> ...
>>> client.get_client_config("client.yaml", checksum=True)
>>> import yaml
>>> config = client.get_client_config(checksum=True)
>>> with open("client.yaml", "w") as f:
>>> f.write(yaml.dump(config))
To connect a client that uses the data partition 'data/clients/1/mnist.pt':

Expand Down
16 changes: 16 additions & 0 deletions fedn/fedn/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,19 @@ def get_network_config(file=STATESTORE_CONFIG):
except yaml.YAMLError as e:
raise (e)
return settings["network_id"]


def get_controller_config(file=STATESTORE_CONFIG):
""" Get the controller configuration from file.
:param file: The controller configuration file (yaml) path.
:type file: str
:return: The controller configuration as a dict.
:rtype: dict
"""
with open(file, 'r') as config_file:
try:
settings = dict(yaml.safe_load(config_file))
except yaml.YAMLError as e:
raise (e)
return settings["controller"]
12 changes: 12 additions & 0 deletions fedn/fedn/network/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def get_active_clients(self, combiner_id):
response = requests.get(self._get_url('get_active_clients'), params={'combiner': combiner_id}, verify=self.verify)
return response.json()

def get_client_config(self, checksum=True):
""" Get the controller configuration. Optionally include the checksum.
The config is used for clients to connect to the controller and ask for combiner assignment.
:param checksum: Whether to include the checksum of the package.
:type checksum: bool
:return: The client configuration.
:rtype: dict
"""
response = requests.get(self._get_url('get_client_config'), params={'checksum': checksum}, verify=self.verify)
return response.json()

def list_combiners(self):
""" Get all combiners in the network.
Expand Down
54 changes: 45 additions & 9 deletions fedn/fedn/network/api/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from flask import jsonify, send_from_directory
from werkzeug.utils import secure_filename

from fedn.common.config import get_controller_config, get_network_config
from fedn.network.combiner.interfaces import (CombinerInterface,
CombinerUnavailableError)
from fedn.network.state import ReducerState, ReducerStateToString
Expand Down Expand Up @@ -269,24 +270,39 @@ def download_compute_package(self, name):
finally:
mutex.release()

def get_checksum(self, name):
""" Get the checksum of the compute package.
def _create_checksum(self, name=None):
""" Create the checksum of the compute package.
:return: The checksum as a json object.
:rtype: :class:`flask.Response`
:param name: The name of the compute package.
:type name: str
:return: Success or failure boolean, message and the checksum.
:rtype: bool, str, str
"""

if name is None:
name, message = self._get_compute_package_name()
if name is None:
return jsonify({"success": False, "message": message}), 404

return False, message, ''
file_path = os.path.join('/app/client/package/', name) # TODO: make configurable, perhaps in config.py or package.py
try:
sum = str(sha(file_path))
except FileNotFoundError:
sum = ''
message = 'File not found.'
return True, message, sum

def get_checksum(self, name):
""" Get the checksum of the compute package.
:param name: The name of the compute package.
:type name: str
:return: The checksum as a json object.
:rtype: :py:class:`flask.Response`
"""

success, message, sum = self._create_checksum(name)
if not success:
return jsonify({"success": False, "message": message}), 404
payload = {'checksum': sum}

return jsonify(payload)
Expand All @@ -295,16 +311,15 @@ def get_controller_status(self):
""" Get the status of the controller.
:return: The status of the controller as a json object.
:rtype: :class:`flask.Response`
:rtype: :py:class:`flask.Response`
"""
return jsonify({'state': ReducerStateToString(self.control.state())})
# function with kwargs

def get_events(self, **kwargs):
""" Get the events of the federated network.
:return: The events as a json object.
:rtype: :class:`flask.Response`
:rtype: :py:class:`flask.Response`
"""
event_objects = self.statestore.get_events(**kwargs)
if event_objects is None:
Expand Down Expand Up @@ -575,6 +590,27 @@ def get_round(self, round_id):
}
return jsonify(payload)

def get_client_config(self, checksum=True):
""" Get the client config.
:return: The client config as json response.
:rtype: :py:class:`flask.Response`
"""
config = get_controller_config()
network_id = get_network_config()
port = config['port']
host = config['host']
payload = {
'network_id': network_id,
'discover_host': host,
'discover_port': port,
}
if checksum:
success, _, checksum_str = self._create_checksum()
if success:
payload['checksum'] = checksum_str
return jsonify(payload)

def start_session(self, session_id, rounds=5, round_timeout=180, round_buffer_size=-1, delete_models=False,
validate=True, helper='keras', min_clients=1, requested_clients=8):
""" Start a session.
Expand Down
18 changes: 16 additions & 2 deletions fedn/fedn/network/api/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import Flask, jsonify, request

from fedn.common.config import get_network_config, get_statestore_config
from fedn.common.config import (get_controller_config, get_network_config,
get_statestore_config)
from fedn.network.api.interface import API
from fedn.network.controller.control import Control
from fedn.network.statestore.mongostatestore import MongoStateStore
Expand Down Expand Up @@ -238,6 +239,16 @@ def get_controller_status():
return api.get_controller_status()


@app.route('/get_client_config', methods=['GET'])
def get_client_config():
""" Get the client configuration.
return: The client configuration as a json object.
rtype: json
"""
checksum = request.args.get('checksum', True)
return api.get_client_config(checksum)


@app.route('/get_events', methods=['GET'])
def get_events():
""" Get the events from the statestore.
Expand Down Expand Up @@ -300,4 +311,7 @@ def add_client():


if __name__ == '__main__':
app.run(debug=True, port=8092, host='0.0.0.0')
config = get_controller_config()
port = config['port']
debug = config['debug']
app.run(debug=debug, port=port, host='0.0.0.0')
13 changes: 13 additions & 0 deletions fedn/fedn/network/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,19 @@ def test_get_controller_status(self):
# Assert api.get_controller_status was called
fedn.network.api.server.api.get_controller_status.assert_called_once_with()

def test_get_client_config(self):
""" Test get_client_config endpoint. """
# Mock api.get_client_config
return_value = {"test": "test"}
fedn.network.api.server.api.get_client_config = MagicMock(return_value=return_value)
# Make request
response = self.app.get('/get_client_config')
# Assert response
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, return_value)
# Assert api.get_client_config was called
fedn.network.api.server.api.get_client_config.assert_called_once_with(True)


if __name__ == '__main__':
unittest.main()

0 comments on commit 77b83cd

Please sign in to comment.