import os
import json
from contextlib import closing
import socket
from simulaqron.toolbox import get_simulaqron_path
from simulaqron.settings import simulaqron_settings
config_files = [simulaqron_settings.app_file, simulaqron_settings.cqc_file, simulaqron_settings.vnode_file]
node_config_file = simulaqron_settings.nodes_file
simulaqron_path = get_simulaqron_path.main()
config_folder = "config"
default_topology_file = os.path.join(simulaqron_path, config_folder, "topology.json")
[docs]class NetworksConfigConstructor:
def __init__(self, file_path=None):
"""
Used to construct the config file of networks.abs
When all nodes and networks are added the content of this object can
be written to a file by calling the method 'write_to_file'.
:param file_path: None or str
Path to the network config_file. If None an empty networkconfig constructor is initalized.
Otherwise the content of the file is loaded.
"""
self.networks = {}
self.used_sockets = []
self.file_path = file_path
if self.file_path is not None:
if os.path.exists(self.file_path):
self.read_from_file()
[docs] def add_node(self, node_name, network_name="default", app_hostname=None, cqc_hostname=None, vnode_hostname=None,
app_port=None, cqc_port=None, vnode_port=None, neighbors=None):
"""
Adds a node with the given name to a network (default: "default").
If hostnames are None they will default to 'localhost'.
If the port numbers None, unused ones will be chosen between 8000 and 9000.
If neighbors are specified a restricted topology can be constructed (default is fully connected).
:param node_name: str
Name of the node, e.g. Alice
:param network_name: str
Name of the network (default: "default")
:param app_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param cqc_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param vnode_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param app_port: int or None
Port number for the application
:param cqc_port: int or None
Port number for the cqc server
:param vnode_port: int or None
Port number for the virtual node
:param neighbors: (list of str) or None
A list of neighbors, of this node.
If None all current nodes in the network will be adjacent to the added node.
:return: None
"""
if network_name is None:
network_name = "default"
socket_addresses = [(app_hostname, app_port), (cqc_hostname, cqc_port), (vnode_hostname, vnode_port)]
for i, socket_address in enumerate(socket_addresses):
hostname, port = socket_address
if hostname is None:
hostname = "localhost"
if port is None:
port = self._get_unused_port(hostname)
else:
free = self._check_port_available(hostname, port)
if not free:
raise ValueError("Cannot add node {}, since socket address ({}, {}) is already in use."
.format(node_name, hostname, port))
socket_address = (hostname, port)
self.used_sockets.append(socket_address)
socket_addresses[i] = socket_address
app_hostname, app_port = socket_addresses[0]
cqc_hostname, cqc_port = socket_addresses[1]
vnode_hostname, vnode_port = socket_addresses[2]
if network_name in self.networks:
self.networks[network_name].add_node(name=node_name, app_hostname=app_hostname, cqc_hostname=cqc_hostname,
vnode_hostname=vnode_hostname, app_port=app_port, cqc_port=cqc_port,
vnode_port=vnode_port, neighbors=neighbors)
else:
network = _NetworkConfig()
network.add_node(name=node_name, app_hostname=app_hostname, cqc_hostname=cqc_hostname,
vnode_hostname=vnode_hostname, app_port=app_port, cqc_port=cqc_port,
vnode_port=vnode_port, neighbors=neighbors)
self.networks[network_name] = network
[docs] def remove_node(self, node_name, network_name="default"):
"""
Removes a node from the network.
:param node_name: str
Name of the node, e.g. Alice
:param network_name: str
Name of the network (default: "default")
"""
if network_name is None:
network_name = "default"
if network_name in self.networks:
nodes = self.networks[network_name].nodes
nodes.pop(node_name, None)
[docs] def reset(self):
"""
Resets the current object to a single network ("default")
with the nodes Alice, Bob, Charlie, David and Eve.
Note that this does not overwrite any config file but can be done
by calling 'write_to_file'.
:return:
"""
for network_name in list(self.networks.keys()):
self.remove_network(network_name=network_name)
node_names = ["Alice", "Bob", "Charlie", "David", "Eve"]
self.add_network(node_names=node_names)
[docs] def add_network(self, node_names, network_name="default", topology=None):
"""
Adds a new network to the config, with some specified nodes.
:param node_names: list of str
Name of the nodes, e.g. [Alice, Bob]
:param network_name: str
Name of the network (default: "default")
:param topology: None or dict
The topology of the network (optional) (default is fully connected)
"""
if network_name is None:
network_name = "default"
self.remove_network(network_name=network_name)
for node_name in node_names:
if topology is not None:
neighbors = topology[node_name]
else:
neighbors = None
self.add_node(node_name, network_name=network_name, neighbors=neighbors)
[docs] def remove_network(self, network_name="default"):
"""
Removes a network from the config.
:param network_name: str
Name of the network (default: "default")
"""
if network_name is None:
network_name = "default"
self.networks.pop(network_name, None)
[docs] def get_nodes(self, network_name="default"):
"""
Returns the node-config objects (_NodeConfig) in a network.
:param network_name: str
Name of the network (default: "default")
:return: list of _NodeConfig
"""
if network_name is None:
network_name = "default"
if network_name in self.networks:
nodes = self.networks[network_name].nodes
return list(nodes.values())
else:
raise ValueError("{} is not a network in this config".format(network_name))
[docs] def get_node_names(self, network_name="default"):
"""
Returns the names of the nodes in a network.
:param network_name: str
Name of the network (default: "default")
:return: list of str
"""
if network_name is None:
network_name = "default"
if network_name in self.networks:
nodes = self.networks[network_name].nodes
return list(nodes.keys())
else:
raise ValueError("{} is not a network in this config".format(network_name))
[docs] def to_dict(self):
"""
Constructs a dictionary with all the content that can be written to a json file
:return: dict
"""
return {network_name: network.to_dict() for network_name, network in self.networks.items()}
[docs] def write_to_file(self, file_path=None):
"""
Writes the content of this config to a file.
:param file_path: None or str
If a file_path was specified upon __init__ this will be used if file_path is None.
"""
if file_path is None:
file_path = self.file_path
if file_path is None:
raise ValueError("Since this networks config was not initialized with a file_path you need to specify one")
dict = self.to_dict()
with open(file_path, 'w') as f:
json.dump(dict, f, indent=4)
[docs] def read_from_file(self, file_path=None):
"""
Reads config from a file.
:param file_path: None or str
If a file_path was specified upon __init__ this will be used if file_path is None.
"""
if file_path is None:
file_path = self.file_path
if file_path is None:
raise ValueError("Since this networks config was not initialized with a file_path you need to specify one")
if os.path.exists(file_path):
with open(file_path, 'r') as f:
dict = json.load(f)
else:
raise ValueError("No such file {}".format(file_path))
for network_name, network_dict in dict.items():
nodes_dict = network_dict["nodes"]
topology = network_dict["topology"]
network = _NetworkConfig()
network.topology = topology
for node_name, node_dict in nodes_dict.items():
app_hostname, app_port = node_dict["app_socket"]
cqc_hostname, cqc_port = node_dict["cqc_socket"]
vnode_hostname, vnode_port = node_dict["vnode_socket"]
socket_addresses = [(app_hostname, app_port), (cqc_hostname, cqc_port), (vnode_hostname, vnode_port)]
for socket_address in socket_addresses:
if socket_address not in self.used_sockets:
self.used_sockets.append(socket_address)
node = _NodeConfig(name=node_name, app_hostname=app_hostname, cqc_hostname=cqc_hostname,
vnode_hostname=vnode_hostname, app_port=app_port, cqc_port=cqc_port,
vnode_port=vnode_port)
network.nodes[node_name] = node
self.networks[network_name] = network
def _get_unused_port(self, hostname):
"""
Returns an unused port in the interval 8000 to 9000, if such exists, otherwise returns None.
:param hostname: str
Hostname, e.g. localhost or 192.168.0.1
:return: int or None
"""
for port in range(8000, 9001):
if self._check_port_available(hostname, port):
return port
def _check_port_available(self, hostname, port):
"""
Checks if the given port is not already set in the config files or used by some other process.
:param hostname: str
Hostname, e.g. localhost or 192.168.0.1
:param port: int
The port number
:return: bool
"""
if (hostname, port) in self.used_sockets:
return False
return self._check_socket_is_free(port)
@staticmethod
def _check_socket_is_free(port):
"""
Checks if a given socket on localhost is in use.
This is done by trying to open the port and check if it succeeds.
:param port: int
The port number
"""
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
address = ('localhost', port)
try:
sock.bind(address)
except socket.error:
return False
return True
class _NetworkConfig:
def __init__(self):
"""
Used by NetworksConfigConstructor to keep track of the config of a single network.
"""
self.topology = None
self.nodes = {}
def add_node(self, name, app_hostname, cqc_hostname, vnode_hostname, app_port, cqc_port, vnode_port, neighbors):
"""
Adds a node with the given name to a network (default: "default").
If hostnames are None they will default to 'localhost'.
If the port numbers None, unused ones will be chosen between 8000 and 9000.
If neighbors are specified a restricted topology can be constructed (default is fully connected).
:param node_name: str
Name of the node, e.g. Alice
:param app_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param cqc_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param vnode_hostname: str or None
Hostname, e.g. localhost (default) or 192.168.0.1
:param app_port: int or None
Port number for the application
:param cqc_port: int or None
Port number for the cqc server
:param vnode_port: int or None
Port number for the virtual node
:param neighbors: (list of str) or None
A list of neighbors, of this node.
If None all current nodes in the network will be adjacent to the added node.
:return: None
"""
if neighbors is not None:
if self.topology is None:
# Assume that whatever nodes were there before are fully connnected
self.topology = {}
node_names = self.nodes.keys()
for node_name in node_names:
self.topology[node_name] = [neigh for neigh in node_names if not neigh == node_name]
self.topology[name] = neighbors
self.nodes[name] = _NodeConfig(name=name, app_hostname=app_hostname, cqc_hostname=cqc_hostname,
vnode_hostname=vnode_hostname, app_port=app_port, cqc_port=cqc_port,
vnode_port=vnode_port)
def to_dict(self):
"""
Constructs a dictionary with all the config of this network.
:return: dict
"""
nodes = {node_name: node.to_dict() for node_name, node in self.nodes.items()}
return {"nodes": nodes, "topology": self.topology}
class _NodeConfig:
def __init__(self, name, app_hostname, cqc_hostname, vnode_hostname, app_port, cqc_port, vnode_port):
"""
Used by _NetworkConfig to keep track of the config of a single node.
"""
self.name = name
self.app_hostname = app_hostname
self.cqc_hostname = cqc_hostname
self.vnode_hostname = vnode_hostname
self.app_port = app_port
self.cqc_port = cqc_port
self.vnode_port = vnode_port
def to_dict(self):
"""
Constructs a dictionary with all the config of this node.
:return: dict
"""
return {
"app_socket": [self.app_hostname, self.app_port],
"cqc_socket": [self.cqc_hostname, self.cqc_port],
"vnode_socket": [self.vnode_hostname, self.vnode_port]
}