Skip to content

Change architecture to separate parser logic from server logic (untested) #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ install_requires =
importlib-metadata; python_version<"3.8"
requests
pytricia
cvxopt
dataclasses-json
pytest


[options.packages.find]
Expand Down
121 changes: 121 additions & 0 deletions src/alto/app/estimator.py
Original file line number Diff line number Diff line change
@@ -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))
39 changes: 39 additions & 0 deletions src/alto/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
# Authors:
# - Jensen Zhang <jingxuan.n.zhang@gmail.com>
# - Kai Gao <emiapwil@gmail.com>
# - Jacob Dunefsky <jacob.dunefsky@yale.edu>

import logging

from typing import List, Dict

import requests

from alto.config import Config
from alto.model import ALTONetworkMap, ALTOCostMap

Expand Down Expand Up @@ -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)
66 changes: 49 additions & 17 deletions src/alto/model/rfc7285.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,36 @@
# - Kai Gao <emiapwil@gmail.com>


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):

Expand Down Expand Up @@ -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
Expand All @@ -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', {}))

Expand Down Expand Up @@ -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', [])]
Expand All @@ -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)
Loading