3 releases (breaking)
0.6.0 | Nov 9, 2022 |
---|---|
0.5.0 | Jul 15, 2021 |
0.4.0 | Oct 30, 2018 |
#28 in #control-plane
300KB
7K
SLoC
pyportus
This module provides a python interface to the Portus CCP implementation.
Setup
These python bindings are available on pyPI, pip install pyportus
.
To build manually, this project uses maturin.
To test:
- make a virtual environment
maturin develop
To install:
maturin build
pip install
the resulting .whl
To then run/test, cd examples && ./<venv>/python3 aimd.py
(may need to run as root)
Writing Algorithms
Overview
An algorithm in portus is represented by a Python class and an instance of this class represents a single TCP flow. A new instance is created for each flow.
This class must be a subclass of portus.AlgBase
and must implement the two
following method signatures:
on_create(self)
on_report(self, r)
r
is a Report object containing all the fields defined in your datapath program, as well as the currentCwnd
andRate
. Suppose your program defines just a single variable:(def (acked 0))
, whereacked
adds up the total bytes acked since the last report. This value can be accessed asr.acked
. Similarly, you can access the cwnd or rate asr.Cwnd
andr.Rate
(captialization important!).
Each instantiation of the class will automatically have two fields inside self:
self.datapath
is a pointer to the datapath object that can be used to install new datapath programs. It has two available methods:datapath.install( str )
, which takes a datapath program as a string. It compiles the program and installs it in the datapath. It does not return anything, though it may raise an exception if your program fails to compile.datapath.update_field(field, val)
, which takes a variable in theReport
scope of your datapath program and sets the value toval
. For example, to update just the cwnd, you could usedatapath.update_field("Cwnd", 10000)
(note: cwnd is denoted in bytes, not packets).
self.datapath_info
is a struct containing fields about this particular flow from the datapath (this could be used, for example, inon_create
to set an initial cwnd based on the datapath'smss
)sock_id
: unique id of this flow in the datapathinit_cwnd
: the initial congestion window this flow will have until you set itsrc_ip
,src_port
,dst_ip
,dst_port
: the ip address and port of the source and destination for the flow
Datapath Programs
Datapath programs are used to (1) define which statistics to send back to your usespace program and how often and (2) set the congestion window and/or pacing rate. A datapath program is written in a very simple lisp-like dialect and consists of a single variable definition line followed by any number of when clauses:
(def ( ... ) ( ... ))
(when (event) (
do_stuff ...
)
(when (other_event) (
do_other_stuff ...
)
NOTE: the following info is out of date as the datapath program API has been
updated
1. Report Variable Definitions
Example: (def (Report.acked 0) (Report.rtt 0) (Report.timeout false))
This line defines the names and initial values of variables in the report scope. Calling (report)
in your datapath program results in a call to your algorithm's on_report
function with the current value of these variables. After the call these variables are reset back to their initial value.
NOTE: Variables in datapath programs are written as {scope}.{name}
. For example, the acked
variable in the Report
scope is written as Report.acked
. Therefore, all variables defined in this line must start with Report.
However, when you access them in on_report
, you just provide the variable name. In our example above, Report.rtt
defines the variable rtt
in the Report
scope. If we want to access this value in on_report(r)
, we'd use r.rtt
(i.e. not r.Report.rtt
).
2. When Clauses
When clauses consist of a boolean expression and a set of instructions. On each ack, the datapath checks the boolean expression, and if it evaluates to true
, it runs the set of instructions. For example, the following when clause sends a report (i.e. calls the on_report
function) once every rtt:
(when (> Micros Flow.rtt_sample_us)
(report)
)
Putting it all together
A sample algorithm definition showing the full API:
import portus
# Class must sublcass portus.AlgBase
class SampleCCAlg(portus.AlgBase):
# Init must take exactly these parameters
def __init__(self, datapath, datapath_info):
# Store a copy of the datapath and info for later
self.datapath = datapath
self.datapath_info = datapath_info
# Internally store an initial cwnd value
self.cwnd = 10 * self.datapath_info.mss
# Install an initial datapath program to keep track of the RTT and report it once per RTT
# The first when clause is true on every single ack,
# which means the 'Report.rtt' field will always keep the latest rtt sample
# The second when clause is true once one rtt's worth of time has passed,
# at which point it will trigger on_report, and Micros (and Report.rtt) will be reset to 0
self.datapath.install("""\
(def
(Report.rtt 0)
)
(when true
(:= Report.rtt Flow.rtt_sample_us)
(fallthrough)
)
(when (> Micros Flow.rtt_sample_us)
(report)
)
""")
# This function will be called once per RTT, and the report struct `r` will contain:
# "rtt", "Cwnd", and "Rate"
def on_report(self, r):
# Compute new cwnd internally
# If the rtt has decreased, increase the cwnd by 1 packet, else decrease by 1 packet
if self.last_rtt < r.rtt:
self.cwnd += self.datapath_info.mss
else:
self.cwnd -= self.datapath_info.mss
self.last_rtt = r.rtt
# Send this new value of cwnd to the datapath
self.datapath.update_field("Cwnd", self.cwnd)
Important Notes
- You should install an initial datapath program in your
__init__
implementation, otherwise you will not receive any reports and nothing else will happen. You can always install a different datapath program later when handlingon_report
. - If you want to print anything, you should use
sys.stderr.write()
(note that you need toimport sys
and that it doesn't automatically add new lines for you likeprint
does). - You must store a reference to
datapath
inself
called "datapath" (i.e.self.datapath = datapath
), because the library internally uses this to access the datapath struct as well.
Starting CCP
The CCP entry point is portus.connect(ipc_type, class, debug, blocking)
:
ipc_type (string)
: (netlink | unix | char) on linux or (unix) on macclass
: your algorithm class, e.g.SampleCCAlg
debug (bool)
: if true, the CCP will log all messages passed between the ccp and datapathblocking (bool)
: if true, use blocking ipc reads, otherwise use non-blocking
For example: portus.connect("netlink", SampleCCAlg, debug=True, blocking=True)
.
Regardless of whether you use blocking or non-blocking sockets, connect
will block forever (to stop the CCP just send ctrl+c or kill the process).
Example
For a full working example of both defining an algorithm and running the CCP, see the simple AIMD scheme in ./aimd.py
and try running it: sudo python aimd.py
.
Dependencies
~10–16MB
~221K SLoC