diff --git a/setup.cfg b/setup.cfg index 635b1a1..2fe5bd2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,9 @@ install_requires = importlib-metadata; python_version<"3.8" requests pytricia + cvxopt + dataclasses-json + pytest [options.packages.find] diff --git a/src/alto/app/estimator.py b/src/alto/app/estimator.py new file mode 100644 index 0000000..475c876 --- /dev/null +++ b/src/alto/app/estimator.py @@ -0,0 +1,121 @@ +""" +alto-estimator.py + +Estimates the throughput of a number of different flows. + +Can be used as a standalone script. --alto-server gives the hostname + of the ALTO server, and --flows gives a path to an input file. + The flows file is of the form + SRC1 -> DST1 DST2 DST3 ... + SRC2 -> DST4 DST5 DST6 ... + ... +""" + +import json +import requests + +def input_to_json(input_str): + """Converts a string, representing a list of flows, + into a dict that can be converted to JSON and sent to an ALTO server. + + Args: + input_str (str): A string representing a list of flows. + The list of flows should be of the form: + SRC1 -> DST1 DST2 DST3 ... + SRC2 -> DST4 DST5 DST6 ... + ... + + Returns: + A list of flows. + """ + input_lines = input_str.splitlines() + """ + TODO: add compression. https://github.com/openalto/alto/issues/7 + What does "compression" mean in this context? As was decided during a + meeting before the hackathon (although I can't find the Google Doc + in which this decision was made), the format of requests should + allow flows to be specified by specifying a many-to-many relationship + between sources and destinations. An example is as follows: + + {"srcs": ["src1", "src2", "src3"], "dsts": ["dst1", "dst2"]} + + The above dict defines six flows: one flow from each source to each + destination. + + Now, this format was chosen to allow for the compression of requests. + However, the input format groups flows by source. Thus, an input like + + SRC1 -> DST1 DST2 + SRC2 -> DST1 DST2 + SRC3 -> DST1 DST2 + + can be compressed to the dictionary given above. But this code doesn't + currently do that. The problem of finding an optimal compression + strikes me as NP-hard (although I haven't actually thought it through). + Thus, the current code simply naively translates the input string into + a dict. + """ + ef_arr = [] + for line in input_lines: + line_split = line.split("->") + src = line_split[0].strip() + dst_arr = line_split[1].strip().split(" ") + dst_arr = list(filter(lambda a: a != 0, dst_arr)) + + ef_arr.append({"srcs": [src], "dsts": dst_arr}) + return ef_arr + +from alto.client import Client + +def do_request_from_str(input_str, alto_server): + """Obtains ALTO throughput data for an input string representing flows + of interest. + + Args: + input_str (str): A string representing a list of flows. + The list of flows should be of the form: + SRC1 -> DST1 DST2 DST3 ... + SRC2 -> DST4 DST5 DST6 ... + ... + + alto_server (str): The base URL for the ALTO server. This URL cannot + end in a "/". + + Returns: + A JSON string representing the throughput for each flow + """ + c = Client() + return c.get_throughput(input_to_json(input_str), url=alto_server+"/endpoint/cost") + +if __name__ == "__main__": + """Obtains ALTO throughput data for an input file representing flows + of interest. + + Args: + --flows (str): A path to a list of flows. + The list of flows should be of the form: + SRC1 -> DST1 DST2 DST3 ... + SRC2 -> DST4 DST5 DST6 ... + ... + + --alto-server (str): The base URL for the ALTO server. This URL cannot + end in a "/". + + Returns: + A JSON string representing the throughput for each flow + """ + import argparse + import sys + + parser = argparse.ArgumentParser(description="Estimate throughput for flows") + parser.add_argument('--alto-server', required=True) + parser.add_argument('--flows', required=True) + args = parser.parse_args(sys.argv[1:]) + + alto_server = args.alto_server + + fp = open(args.flows, "r") + input_str = fp.read() + fp.close() + + print(do_request_from_str(input_str, alto_server)) diff --git a/src/alto/client/client.py b/src/alto/client/client.py index 65fd12d..d73e88a 100644 --- a/src/alto/client/client.py +++ b/src/alto/client/client.py @@ -24,11 +24,14 @@ # Authors: # - Jensen Zhang # - Kai Gao +# - Jacob Dunefsky import logging from typing import List, Dict +import requests + from alto.config import Config from alto.model import ALTONetworkMap, ALTOCostMap @@ -149,4 +152,40 @@ def get_routing_costs(self, src_ips: List[str], dst_ips: List[str], for dip in dst_ips } for sip in src_ips } + + def get_throughput(self, input_dict: str, url=None) -> str: + """Obtains ALTO throughput data for an input list representing flows + of interest. + + Args: + input_dict (list): A list of dicts, representing a list of flows. + The list should be of the form + [{"srcs": [src1, src2, src3, ...], "dsts": [dst1, dst2, dst3, ...]}, + {"srcs": [src4, src5, src6, ...], "dsts": [dst4, dst5, dst6, ...]}, + ...] + + url : (optional) str + URI to access the cost map + + Returns: + A JSON string representing the throughput for each flow + """ + query_json = { + "cost-type": {"cost-mode" : "numerical", + "cost-metric" : "tput"}, + "endpoint-flows" : input_dict + } + query_str = json.dumps(query_json) + query_headers = { + "Content-Type": "application/alto-endpointcostparams+json", + "Accept": "application/alto-endpointcost+json,application/alto-error+json" + } + if url=None: url = self.config.get_costmap_uri() + alto_r = requests.post(url, + headers=query_headers, + data=query_str + ) + + alto_json_resp = alto_r.json() + return json.dumps(alto_json_resp) diff --git a/src/alto/model/rfc7285.py b/src/alto/model/rfc7285.py index 3db27cc..433564b 100644 --- a/src/alto/model/rfc7285.py +++ b/src/alto/model/rfc7285.py @@ -26,39 +26,36 @@ # - Kai Gao -from dataclasses import dataclass -from typing import List, Dict +from dataclasses import dataclass, field +from typing import List, Dict, Optional import requests import pytricia +from dataclasses_json import dataclass_json, LetterCase, config ALTO_CTYPE_NM = 'application/alto-networkmap+json' ALTO_CTYPE_CM = 'application/alto-costmap+json' +ALTO_CTYPE_ECS = 'application/alto-endpointcost+json' +ALTO_CTYPE_ECS_PARAM = 'application/alto-endpointcostparams+json' ALTO_CTYPE_ERROR = 'application/alto-error+json' +@dataclass_json(letter_case=LetterCase.KEBAB) @dataclass class Vtag: resource_id: str tag: str - @staticmethod - def from_json(data): - rid = data['resource-id'] - tag = data['tag'] - return Vtag(resource_id = rid, tag = tag) - - +@dataclass_json(letter_case=LetterCase.KEBAB) @dataclass class CostType: - metric: str - mode: str - - @staticmethod - def from_json(data): - metric = data['cost-metric'] - mode = data['cost-mode'] - return CostType(metric=metric, mode=mode) + cost_metric: str + cost_mode: str +@dataclass_json(letter_case=LetterCase.KEBAB) +@dataclass +class EndpointFilter: + srcs: List[str] + dsts: List[str] class Meta(object): @@ -128,12 +125,28 @@ def get(self): return r + def post(self, ptype, params): + self.check_params(params) + + headers = { + 'accepts': self.ctype + ',' + ALTO_CTYPE_ERROR, + 'Content-type': ptype + } + r = requests.get(self.url, auth=self.auth, headers=headers) + r.raise_for_status() + + self.check_headers(r) + self.check_contents(r) + def check_headers(self, r): raise NotImplementedError def check_contents(self, r): raise NotImplementedError + def check_params(self, params): + raise NotImplementedError + class ALTONetworkMap(ALTOBaseResource): vtag: Vtag @@ -151,6 +164,9 @@ def check_headers(self, r): def check_contents(self, r): pass + def check_params(self, params): + pass + def __build_networkmap(self, data): self.vtag_ = Vtag.from_json(data['meta'].get('vtag', {})) @@ -185,6 +201,9 @@ def check_headers(self, r): def check_contents(self, r): pass + def check_params(self, params): + pass + def __build_costmap(self, data): self.dependent_vtags = [Vtag.from_json(dv) for dv in data['meta'].get('dependent_vtags', [])] @@ -201,3 +220,16 @@ def get_costs(self, spid: List[str], result.update({s: {d: self.cmap_[s][d] for d in set(dpid) if d in self.cmap_[s]}}) return result +@dataclass_json(letter_case=LetterCase.KEBAB) +@dataclass +class ALTOEndpointCostParam: + cost_type: CostType + endpoint_flows: Optional[list[EndpointFilter]] = field(default=None, metadata=config(exclude=lambda x: x is None)) + endpoints: Optional[EndpointFilter] = field(default=None, metadata=config(exclude=lambda x: x is None)) + +class ALTOEndpointCostService(ALTOBaseResource): + + def __init__(self, param, url, **kwargs): + ALTOBaseResource.__init__(self, ALTO_CTYPE_ECS, url, **kwargs) + + r = self.post(ALTO_CTYPE_ECS_PARAM, param) diff --git a/src/alto/server/estimate_throughput.py b/src/alto/server/estimate_throughput.py new file mode 100644 index 0000000..0a04238 --- /dev/null +++ b/src/alto/server/estimate_throughput.py @@ -0,0 +1,112 @@ +""" +estimate_throughput.py + +A Flask app that runs a basic ALTO server. + +IMPORTANT NOTES: +* This script assumes that the configuration files are located at + "input/g2.conf" and "output/input_routing.conf". If this is not the case, + then change the variables g2fp_str and infp_str +* This script estimates RTT by summing up the delays of each link. This is + not accurate!!! But, since taking into account queuing dynamics is + too difficult, the script does this instead. +""" + +import json +import numpy as np + +NETWORK_TYPE = "G2_MININET" +"""The format in which the network is configured. + +Currently, only `G2_MININET` is supported. +""" + +SOLVER = "jensen" +"""The numerical optimizer that is used. + +Currently, only `jensen` is supported. +""" + +if __name__ == "__main__": + from flask import Flask, request + import json + + if SOLVER == "jensen": + from solvers.jensen import solve + + app = Flask(__name__) + + # TODO: In the future, we will abstract out this code block into + # a proper plugin interface, that will allow the server to support a + # variety of network APIs in addition to G2_MININET. + if NETWORK_TYPE == "G2_MININET": + # TODO: depending on where this script is run from + # and where the files are + # you might want to change these paths + + g2fp_str = "input/g2.conf" + infp_str = "output/input_routing.conf" + + g2fp = open(g2fp_str, "r") + g2conf_str = g2fp.read() + g2fp.close() + + infp = open(infp_str, "r") + input_str = infp.read() + infp.close() + + from g2mininet_parser import G2MininetParser + parser = G2MininetParser(g2conf_str, input_str) + + alto_endpoint = "/endpoint/cost/" + @app.route(alto_endpoint, methods=['POST']) + def get_throughput(): + """An endpoint that serves ALTO requests for network cost. + + Note: + This function takes no Python args because it reads an HTTP + request. This request must be according to the ALTO specification + given in RFC 7285. + + Returns: + The network cost map in the JSON format used by ALTO, as specified + in RFC 7285. + + """ + json = request.get_json() + flows = json["endpoint-flows"] + + # make flows datastructure + id_to_flow = [] + flow_to_id = {} + + i = 0 + + for flow_set in flows: + for src in flow_set["srcs"]: + for dst in flow_set["dsts"]: + id_to_flow.append(src, dst) + flow_to_id[(src, dst)] = i + i += 1 + + flow_info = parser.construct_from_flows(id_to_flow) + + tput = solve(flow_info["A"], flow_info["c"], flow_info['RTT'])[0] + + tput_dict = {} + for flow in id_to_flow: + tput_dict[flow[0]] = {} # initialize each source to empty dict + + for i, cur_tput in enumerate(tput): + src, dst = flow_to_id[i] + tput_dict[src][dst] = cur_tput + + retval = { + "endpoint-cost-map": tput_dict + } + + response = flask.make_response(json.dumps(retval)) + response.headers['Content-Type'] = 'application/alto-endpointcost+json' + return response + + app.run() diff --git a/src/alto/server/g2mininet_parser.py b/src/alto/server/g2mininet_parser.py new file mode 100644 index 0000000..1486c7a --- /dev/null +++ b/src/alto/server/g2mininet_parser.py @@ -0,0 +1,298 @@ +""" +g2mininet_parser.py implements a parser for networks specified as G2-mininet +""" + +import json + +class G2MininetParser: + """ + A parser for G2-Mininet networks that returns info needed for optimization. + + In particular, this info is a flow matrix A, a capacity vector C, and + an RTT vector. The info can be obtained by calling + `parser.construct_from_flows`. + """ + def __init__(self, g2conf_str, input_str): + """ + Args: + g2conf_str (str): A path to the g2.conf file. + input_str (str): A path to the input_routing.conf file. + """ + self.g2conf_str = g2conf_str + self.input_str = input_str + + def parse_line(self, line): + """A helper method used to parse each line of the config files. + + Note: + Lines in the config files are given in the form: + `field_name: val1; val2; val3` + + Args: + line (str): The line to be parsed. + + Returns: + An array of values represented by the line. + In the example above, the values would be ['val1', 'val2', 'val3']. + + """ + line_split = line.split(":") + # line_split[0] is the field name, e.g. "links" + line_val = line_split[1].strip() + return line_val.split(';') + + def make_link_to_id(self, links_str): + """From a string defining links, makes a dict mapping links to ids. + + Args: + links_str (str): The configuration line defining the links of the + network. + + Returns: + A dict which maps a (src, dst) pair to its link id. + + """ + tuple_strs = self.parse_line(links_str) + link_to_id = {} + for tuple_str in tuple_strs: + tuple_str = tuple_str[1:-1] #remove parens + tuple_split = tuple_str.split(",") + + link_id = int(tuple_split[0].strip()) + src = tuple_split[1].strip() + dst = tuple_split[2].strip() + + link_to_id[(src, dst)] = link_id + return link_to_id + + + def construct_routing_col(self, link_to_id, path): + """Constructs a column of the routing matrix, given a single path. + + Note: + This column is a vector with ones in the entries corresponding to + links traversed by the given path, and zeros everywhere else. + + Args: + link_to_id (dict): A dict mapping (src, dst) pairs to link ids. + path (list): A list of nodes, in order, visited by the path. + + Returns: + The routing column vector for the path. + + """ + link_num = max(link_to_id.values) + col = np.zeros((link_num+1,1)) # links are 1-indexed, so link_num+1 + for i in range(len(path)-1): + cur_link = link_to_id[(path[i], path[i+1])] + col[cur_link] = 1 + return col + + + def construct_routing_matrix(self, link_to_id, path_json, flows): + """Constructs the routing matrix for a list of flows. + + Args: + link_to_id (dict): a dict mapping (src, dst) pairs to link ids. + path_json (dict): a multi-level dict (converted from json) which + allows one to get the path of nodes traversed from the source + node and the destination node. + flows (list): a list of flows given as (src, dst) pairs. + + Returns: + A routing matrix which will be used by the optimizer. + + """ + + # flows given as [(src, dst)...] + # flows are zero-indexed, unlike links + link_num = max(link_to_id.values) + mat = np.zeros((link_num+1, len(flows))) + for i, flow in enumerate(flows): + src = flow[0] + dst = flow[1] + + path = path_json[src][dst] + col = self.construct_routing_col(link_to_id, path) + mat[:,i] = col[:,0] + return mat + + + def construct_cap_vector(self, + link_to_id, + link_info_str=None, + default_link_info_str=None): + """Constructs the capacity vector from configuration strings giving + information about the links in the network. + + Note: + At least one of `link_info_str` and `default_link_info_str` must + be provided. + + Args: + link_to_id (dict): a dict mapping (src, dst) pairs to link ids. + link_info_str (str, optional): a configuration string giving + information about all links in the network. See g2.conf. + default_link_info_str (str, optional): a configuration string + giving information about links in the network that have no + further information given in the link_info_str. See g2.conf. + + Returns: + The vector representing the capacity of each link. This vector + is utilized by the optimizer. + + """ + + assert(link_info_str is not None or default_link_info_str is not None) + + default_bw = 0 + if default_link_info_str is not None: + default_bw = int(self.parse_line(link_info_str)[0][0].strip()) + + link_num = max(link_to_id.values) + cap_vector = [default_bw for i in range(link_num+1)] + + if link_info_str is not None: + link_info_list = self.parse_line(link_info_str) + for i, cur_link_info in enumerate(link_info_list): + src = cur_link_info[0].strip() + dst = cur_link_info[1].strip() + cur_bw = int(cur_link_info[2].strip()) + cap_vector[link_to_id[(src, dst)]] = cur_bw + + return cap_vector + + def construct_delay_dict(self, link_info_str=None, default_link_info_str=None): + """A helper function that constructs a dictionary mapping (src, dst) + pairs to the estimated delay between those two nodes. + + Note: + At least one of `link_info_str` and `default_link_info_str` must + be provided. + + Args: + link_to_id (dict): a dict mapping (src, dst) pairs to link ids. + link_info_str (str, optional): a configuration string giving + information about all links in the network. See g2.conf. + default_link_info_str (str, optional): a configuration string + giving information about links in the network that have no + further information given in the link_info_str. See g2.conf. + + Returns: + A dictionary mapping (src, dst) pairs to the estimated delay between + those two nodes. + + """ + assert(link_info_str is not None or default_link_info_str is not None) + + default_bw = 0 + if default_link_info_str is not None: + default_bw = int(self.parse_line(link_info_str)[0][0].strip()) + + delay_dict = {} + + if link_info_str is not None: + link_info_list = self.parse_line(link_info_str) + for i, cur_link_info in enumerate(link_info_list): + src = cur_link_info[0].strip() + dst = cur_link_info[1].strip() + delay = float(cur_link_info[3].strip().replace("ms","")) + delay_dict[(src, dst)] = delay + + return delay_dict + + def get_path_delay(self, delay_dict, path): + """Estimates the delay along a path between two nodes. + + Note: + The estimation procedure is not the most accurate. It simply + sums up the delays along each link traversed by the path, not + taking into account queueing delays, etc. + + Args: + delay_dict (dict): A dict mapping (src, dst) pairs to the + delay between those two nodes. + path (list): A list of nodes traversed by a path. + + Returns: + The estimated total delay along the given path. + + """ + c = 0 + for i in range(len(path)-1): + c += delay_dict[(path[i], path[i+1])] + return c + + def get_flows_delay(self, delay_dict, path_json, flows): + """Constructs a vector of total delays for each flow. + + Args: + delay_dict (dict): A dict mapping (src, dst) pairs to the + delay between those two nodes. + path_json (dict): a multi-level dict (converted from json) which + allows one to get the path of nodes traversed from the source + node and the destination node. + flows (list): a list of flows given as (src, dst) pairs. + + Returns: + A vector in which the entry at position i is the estimated delay + for flow i. This vector will be used by the optimizer. + + """ + # flows given as [(src, dst)...] + delay_vec = [0 for i in range(len(flows))] + for i, flow in enumerate(flows): + src = flow[0] + dst = flow[1] + + path = path_json[src][dst] + delay = self.get_path_delay(delay_dict, path) + delay_vec[i] = delay + return delay_vec + + def construct_from_flows(self, flows): + """Given a list of flows, constructs all data structures necessary + for the optimizer to estimate throughput. + + Args: + flows (list): a list of flows given as (src, dst) pairs. + + Returns: + A dict which will be used by the optimizer. This dict contains the + routing matrix 'A', the capacity vector 'c', and the delay vector + 'RTT'. + + """ + g2conf_lines = lines(self.g2conf_str) + path_json = json.loads(self.input_str) + + link_info_str = None + default_link_info_str = None + links_str = None + + for line in g2conf_lines: + if line.startswith("link_info:"): link_info_str = line + elif line.startswith("default_link_info:"): + default_link_info_str = line + elif line.startswith("links:"): + links_str = line + + if link_info_str is not None\ + and default_link_info_str is not None\ + and links_str is not None: + break + + link_to_id = self.make_link_to_id(links_str) + delay_dict = self.construct_delay_dict(link_info_str, default_link_info_str) + + cap_vector = self.construct_cap_vector(link_to_id, + link_info_str, + default_link_info_str) + mat = self.construct_routing_matrix(link_to_id, path_json, flows) + delay_vec = self.get_flows_delay(delay_dict, path_json, flows) + + return { + "c": cap_vector, + "A": mat, + "RTT": delay_vec + } diff --git a/src/alto/server/solvers/__init__.py b/src/alto/server/solvers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/alto/server/solvers/jensen.py b/src/alto/server/solvers/jensen.py new file mode 100644 index 0000000..637455f --- /dev/null +++ b/src/alto/server/solvers/jensen.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +from cvxopt import solvers, matrix, spdiag, log +import numpy as np +from scipy.optimize import fmin_slsqp, least_squares + +def solve_num(A, c, alpha, rho, niter=100, debug=False): + """ + This function solves the NUM problem. + + max sum(U(x)) + Ax <= c + + The srikant's utility function: + U(x) = rho * x^(1-alpha) / (1 - alpha) + """ + B = matrix(A, tc='d') + c = matrix(c, tc='d') + + alpha = np.array(alpha) + rho = np.array(rho) + + m, n = B.size + + assert n == len(alpha) + assert n == len(rho) + assert m == len(c) + + alphas = 1 - alpha + + def f(x): + y = np.array(x.T).flatten() + return sum(np.where(alphas==0, + rho*np.log(y), + rho*np.power(y, alphas) / (alphas + 1e-9))) + + def fprime(x): + y = np.array(x.T).flatten() + return rho * np.power(y, -alpha) + + def fpprime(x, z): + y = np.array(x.T).flatten() + return z[0] * rho * -alpha * y**(-alpha-1) + + def F(x=None, z=None): + if x is None: + return 0, matrix(1.0, (n, 1)) + if min(x) <= 0.0: + return None + fx = matrix(-f(x), (1,1)) + fpx = matrix(-fprime(x), (1, n)) + if z is None: + return fx, fpx + fppx = spdiag(matrix(-fpprime(x,z), (n, 1))) + return fx, fpx, fppx + + ret = solvers.cp(F, G=B, h=c, maxiters=niter, options={'show_progress': debug}) + x, u = ret['x'], ret['zl'] + return np.array(x).flatten(), np.array(u).flatten() + +def get_num_param(n, RTT, ccalg='cubic'): + alpha, rho = None, None + if ccalg == 'vegas': + alpha = np.ones(n) + rho = np.ones(n) + elif ccalg == 'reno': + alpha = [2] * n + rho = [rtt**(-2) for rtt in RTT] + elif ccalg == 'cubic': + alpha = [4/3] * n + rho = [rtt**(-1/3) for rtt in RTT] + return alpha, rho + +def solve(A, c, RTT, niter=100, debug=False): + """ + For backward compatibility + """ + alpha, rho = get_num_param(len(RTT), RTT) + return solve_num(A, c, alpha, rho, niter, debug) + +def train(samples, A, c, alpha): + m, n = A.shape + + def f(rho0): + rho = np.concatenate((rho0, np.ones(1))) + x, u = solve_num(A, c, alpha, rho) + return sum([np.power(x - sample, 2) for sample in samples]) + + rho = fmin_slsqp(f, np.zeros(m-1), + bounds=[(0, np.inf) for i in range(m-1)], + disp=0) + return rho + +if __name__ == '__main__': + A = np.array([[1, 0, 0], [1, 1, 0], [1, 0, 1]]) + c = np.array([1, 1, 1]) + alpha = np.ones(3) + + samples = [np.array([0.33, 0.67, 0.67])] + + rho = train(samples, A, c, alpha) + + print(rho) diff --git a/tests/bwest/flows b/tests/bwest/flows new file mode 100644 index 0000000..a84cbea --- /dev/null +++ b/tests/bwest/flows @@ -0,0 +1 @@ +ipv4:10.11.12.2 -> ipv4:10.20.30.2 ipv4:10.20.30.3 \ No newline at end of file diff --git a/tests/bwest/g2.conf b/tests/bwest/g2.conf new file mode 100644 index 0000000..ff0d366 --- /dev/null +++ b/tests/bwest/g2.conf @@ -0,0 +1,154 @@ +[Topology] + +# str: tuples separated by semi-colon +# Each tuple is a network link, host-switch or switch-switch. +# Format of each tuple is (linkID, node1, node2), linkIDs should be integers starting at 1. +# It is recommended that switch-switch links are specified first, followed by switch-host links. +# 6 flows and 4 links smallFlow example: +links: (1,s1,s2);(2,s2,s3);(3,s3,s4);(4,s4,s5);(5,h1,s1);(6,h2,s2);(7,h3,s3);(8,h4,s4);(9,h5,s5); + +# str +# First two octets of dotted decimal IP address. +base_addr: 10.0 + +# int [0,255] or str {x} +# Third octet of IP address. +# 'x' denotes that the following address has to be assigned automatically and sequentially for each host. +# At least one of the third and fourth octet must be specified as 'x'. +subnet_addr: x + +# int [0,255] or str {x} +# Fourth octet of IP address. +# 'x' denotes that the following address has to be assigned automatically and sequentially for each host. +host_addr: 10 + +# int [0,32] +# Bit-length of subnet mask (also known as CIDR prefix). +netmask_length: 24 + +# str {yes|no} +# Manually assign IP addresses to some or all of the hosts. +override_ip: no + +# str: tuples separated by semi-colon +# Each tuple denotes (host_ID, IP address). +ip_info: (h1,10.11.12.13/24);(h2,10.20.30.40/24) + +# str +# Path to a file that receives topology information in JSON format, which is input to controller. +topology_json_outfile: topo.json + +# str {shortest_path | path/to/file.json} +# shortest_path: shortest paths between each pair of nodes will be used to create routing rules. +# path/to/file.json: user-specified paths that will be used to create routing tables. +flow_paths_file: shortest_path + +# Parameter spec for a link between a pair of switches. Specs should be separated by semicolon. +# Format of a link spec is: src (str), dst (str), bw (Mbit/s: float), delay (ms: float), loss (percent: float), max_queue_size (int), use_htb {0|1} +# Write 'N/A' for the field that you don't want to specify and just want to keep to it's default value. +# But src and dst must be specified. +link_info: s1, s2, 25, 1ms, 0, N/A, N/A; s2, s3, 50, 1ms, 0, N/A, N/A; s3, s4, 100, 1ms, 0, N/A, N/A; s4, s5, 75, 1ms, 0, N/A, N/A; + +# Similar to above, specify default parameters for all other links. +# {None| comma-separated string} +# If not None: bw, delay, and loss are required, other two parameters can be 'N/A' +# Mininet would use default values (in case of N/A): max_queue_size = None and use_htb = False +# default_link_info: None +default_link_info: 1000, 0ms, 0, N/A, N/A + + +[General] + +# str +# Path to a file that receives adjacency list of the graph represent by Mininet network. +adjacency_list_outfile: adj_list.txt + +# str +# Path to a file that receives routing information, which is input to controller. +routing_conf_outfile: output_routing.conf + +# int {0|1} +# Debug mode off or on. +# If debug mode is on, a tcpdump will be started at each host and a .pcap file will be written out. +debug: 0 + +# int {0|1} +# Whether or not to start Mininet CLI after creating the network. +start_cli: 0 + +# str {cubic|reno|bbr} +# TCP congestion control mechanism to use for iperf test. +tcp_congestion_control: cubic + +# int {2|3} +# Iperf version. +iperf_version: 2 + + +[Monitoring] + +# int {0|1|2} +# 0: don't run ping at all +# 1: run ping only prior to sending data +# 2: run ping both prior to and during the sending of the data +test_pingall: 0 + +# int {0|1} +# Do we want to run iperf test on traffic flows specified using traffic_conf parameter? +test_iperf: 1 + +# str path/to/file.conf +# user-specified traffic flow description. +# Format: int job id, source host, destination host, number of bytes to transfer, time in seconds to start the transfer +traffic_conf_file: traffic.conf + +# int {0|1} +# Whether or not to collect switch statistics. +# If yes, a timeseries of queue occupancy and number of dropped packets are written to two separate files. +# This is done for every dst switch (on the interface that connects to src switch) that are specified in link_info above. +monitor_switch_stats: 0 + +# str: tuples separated by semi-colon +links_to_monitor: (s2,s3); + +# float +# collection_frequency (seconds) controls (a) iperf interval and (b) switch statistics polling interval. +collection_frequency: 2.0 + +# float +# CPU and memory utilization polling interval (s). +utilization_monitor_interval = 5.0 + +# str +# Prefix to append before all result files and plots. +result_prefix: 20220217_cubic + +# int {0|1} +# If set to 1, only results processing is done using previously generated output of monitoring. +# If 0, everything is done starting from creating the network, monitoring, and results processing. +only_results_processing = 0 + +# str (FS|No-FS) +# Method to use to compute flow convergence time. +# Compute convergence time by either using max-min fair-shares ('FS') or without using max-min fair shares ('No-FS'). +convergence_time_method: No-FS + +# int +# Size of sliding window used while computing convergence time. +window_size: 10 + +# float +# Threshold to use while computing convergence time. +# Example values: 5.0 (for 'FS' method), 0.03 (for 'No-FS' method). +threshold: 0.03 + +# int +# Number of consecutive windows to compare while computing convergence time. +# NOTE: this parameter is used only if 'convergence_time_method' is set to 'No-FS'. +num_samples: 15 + +# int {0|1} +# If set to 1, a PDF plot is generated for each flow's receiver throughput timeseries. +# If set to 0, only some of the throughput-plots are generated: one for each unique src-dst pair. +plot_each_flow: 0 + diff --git a/tests/bwest/input_routing.json b/tests/bwest/input_routing.json new file mode 100644 index 0000000..8a25b34 --- /dev/null +++ b/tests/bwest/input_routing.json @@ -0,0 +1,572 @@ +{ + "s1": { + "s1": [ + "s1" + ], + "s2": [ + "s1", + "s2" + ], + "h1": [ + "s1", + "h1" + ], + "s3": [ + "s1", + "s2", + "s3" + ], + "h2": [ + "s1", + "s2", + "h2" + ], + "s4": [ + "s1", + "s2", + "s3", + "s4" + ], + "h3": [ + "s1", + "s2", + "s3", + "h3" + ], + "s5": [ + "s1", + "s2", + "s3", + "s4", + "s5" + ], + "h4": [ + "s1", + "s2", + "s3", + "s4", + "h4" + ], + "h5": [ + "s1", + "s2", + "s3", + "s4", + "s5", + "h5" + ] + }, + "s2": { + "s2": [ + "s2" + ], + "s1": [ + "s2", + "s1" + ], + "s3": [ + "s2", + "s3" + ], + "h2": [ + "s2", + "h2" + ], + "h1": [ + "s2", + "s1", + "h1" + ], + "s4": [ + "s2", + "s3", + "s4" + ], + "h3": [ + "s2", + "s3", + "h3" + ], + "s5": [ + "s2", + "s3", + "s4", + "s5" + ], + "h4": [ + "s2", + "s3", + "s4", + "h4" + ], + "h5": [ + "s2", + "s3", + "s4", + "s5", + "h5" + ] + }, + "h1": { + "h1": [ + "h1" + ], + "s1": [ + "h1", + "s1" + ], + "s2": [ + "h1", + "s1", + "s2" + ], + "s3": [ + "h1", + "s1", + "s2", + "s3" + ], + "h2": [ + "h1", + "s1", + "s2", + "h2" + ], + "s4": [ + "h1", + "s1", + "s2", + "s3", + "s4" + ], + "h3": [ + "h1", + "s1", + "s2", + "s3", + "h3" + ], + "s5": [ + "h1", + "s1", + "s2", + "s3", + "s4", + "s5" + ], + "h4": [ + "h1", + "s1", + "s2", + "s3", + "s4", + "h4" + ], + "h5": [ + "h1", + "s1", + "s2", + "s3", + "s4", + "s5", + "h5" + ] + }, + "s3": { + "s3": [ + "s3" + ], + "s2": [ + "s3", + "s2" + ], + "s4": [ + "s3", + "s4" + ], + "h3": [ + "s3", + "h3" + ], + "s1": [ + "s3", + "s2", + "s1" + ], + "h2": [ + "s3", + "s2", + "h2" + ], + "s5": [ + "s3", + "s4", + "s5" + ], + "h4": [ + "s3", + "s4", + "h4" + ], + "h1": [ + "s3", + "s2", + "s1", + "h1" + ], + "h5": [ + "s3", + "s4", + "s5", + "h5" + ] + }, + "h2": { + "h2": [ + "h2" + ], + "s2": [ + "h2", + "s2" + ], + "s1": [ + "h2", + "s2", + "s1" + ], + "s3": [ + "h2", + "s2", + "s3" + ], + "h1": [ + "h2", + "s2", + "s1", + "h1" + ], + "s4": [ + "h2", + "s2", + "s3", + "s4" + ], + "h3": [ + "h2", + "s2", + "s3", + "h3" + ], + "s5": [ + "h2", + "s2", + "s3", + "s4", + "s5" + ], + "h4": [ + "h2", + "s2", + "s3", + "s4", + "h4" + ], + "h5": [ + "h2", + "s2", + "s3", + "s4", + "s5", + "h5" + ] + }, + "s4": { + "s4": [ + "s4" + ], + "s3": [ + "s4", + "s3" + ], + "s5": [ + "s4", + "s5" + ], + "h4": [ + "s4", + "h4" + ], + "s2": [ + "s4", + "s3", + "s2" + ], + "h3": [ + "s4", + "s3", + "h3" + ], + "h5": [ + "s4", + "s5", + "h5" + ], + "s1": [ + "s4", + "s3", + "s2", + "s1" + ], + "h2": [ + "s4", + "s3", + "s2", + "h2" + ], + "h1": [ + "s4", + "s3", + "s2", + "s1", + "h1" + ] + }, + "h3": { + "h3": [ + "h3" + ], + "s3": [ + "h3", + "s3" + ], + "s2": [ + "h3", + "s3", + "s2" + ], + "s4": [ + "h3", + "s3", + "s4" + ], + "s1": [ + "h3", + "s3", + "s2", + "s1" + ], + "h2": [ + "h3", + "s3", + "s2", + "h2" + ], + "s5": [ + "h3", + "s3", + "s4", + "s5" + ], + "h4": [ + "h3", + "s3", + "s4", + "h4" + ], + "h1": [ + "h3", + "s3", + "s2", + "s1", + "h1" + ], + "h5": [ + "h3", + "s3", + "s4", + "s5", + "h5" + ] + }, + "s5": { + "s5": [ + "s5" + ], + "s4": [ + "s5", + "s4" + ], + "h5": [ + "s5", + "h5" + ], + "s3": [ + "s5", + "s4", + "s3" + ], + "h4": [ + "s5", + "s4", + "h4" + ], + "s2": [ + "s5", + "s4", + "s3", + "s2" + ], + "h3": [ + "s5", + "s4", + "s3", + "h3" + ], + "s1": [ + "s5", + "s4", + "s3", + "s2", + "s1" + ], + "h2": [ + "s5", + "s4", + "s3", + "s2", + "h2" + ], + "h1": [ + "s5", + "s4", + "s3", + "s2", + "s1", + "h1" + ] + }, + "h4": { + "h4": [ + "h4" + ], + "s4": [ + "h4", + "s4" + ], + "s3": [ + "h4", + "s4", + "s3" + ], + "s5": [ + "h4", + "s4", + "s5" + ], + "s2": [ + "h4", + "s4", + "s3", + "s2" + ], + "h3": [ + "h4", + "s4", + "s3", + "h3" + ], + "h5": [ + "h4", + "s4", + "s5", + "h5" + ], + "s1": [ + "h4", + "s4", + "s3", + "s2", + "s1" + ], + "h2": [ + "h4", + "s4", + "s3", + "s2", + "h2" + ], + "h1": [ + "h4", + "s4", + "s3", + "s2", + "s1", + "h1" + ] + }, + "h5": { + "h5": [ + "h5" + ], + "s5": [ + "h5", + "s5" + ], + "s4": [ + "h5", + "s5", + "s4" + ], + "s3": [ + "h5", + "s5", + "s4", + "s3" + ], + "h4": [ + "h5", + "s5", + "s4", + "h4" + ], + "s2": [ + "h5", + "s5", + "s4", + "s3", + "s2" + ], + "h3": [ + "h5", + "s5", + "s4", + "s3", + "h3" + ], + "s1": [ + "h5", + "s5", + "s4", + "s3", + "s2", + "s1" + ], + "h2": [ + "h5", + "s5", + "s4", + "s3", + "s2", + "h2" + ], + "h1": [ + "h5", + "s5", + "s4", + "s3", + "s2", + "s1", + "h1" + ] + } +} \ No newline at end of file diff --git a/tests/test_client.py b/tests/test_client.py index 907e4bc..7eba7e2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,7 +29,8 @@ from unittest import mock from alto.client import Client -from alto.model.rfc7285 import ALTO_CTYPE_NM, ALTO_CTYPE_CM +from alto.model.rfc7285 import ALTO_CTYPE_NM, ALTO_CTYPE_CM, CostType, ALTOEndpointCostParam +import json __author__ = "OpenALTO" __copyright__ = "OpenALTO" @@ -105,6 +106,25 @@ def test_client_noimpl(): with pytest.raises(NotImplementedError): ac.get_ird() +def test_model(): + cost_type = CostType('routingcost', 'numerical') + assert cost_type.cost_metric == 'routingcost' + assert cost_type.cost_mode == 'numerical' + + cost_type = CostType.from_json(cost_type.to_json()) + print(cost_type.to_json()) + assert cost_type.cost_metric == 'routingcost' + assert cost_type.cost_mode == 'numerical' + + ecs_params = ALTOEndpointCostParam(CostType('routingcost', 'numerical'), [], None) + d = json.loads(ecs_params.to_json()) + d['endpoint-flows'] = [ {'srcs': ['ipv4:192.168.1.1'], 'dsts': ['ipv4:192.168.1.2']}] + ecs_params = ALTOEndpointCostParam.from_json(json.dumps(d)) + assert len(ecs_params.endpoint_flows) == 1 + assert len(ecs_params.endpoint_flows[0].srcs) == 1 + assert len(ecs_params.endpoint_flows[0].dsts) == 1 + + print(ecs_params.to_json()) def mocked_requests_get(uri, *args, **kwars): class MockResponse: @@ -155,3 +175,5 @@ def test_client_config(*args): assert '198.51.100.2' in dst_costs2 and '198.51.100.254' in dst_costs2 assert dst_costs2['198.51.100.2'] == 1 and dst_costs2['198.51.100.254'] == 5 +if __name__ == '__main__': + test_model()