# pyportus This module provides a python interface to the Portus CCP implementation. ## Setup These python bindings are available [on pyPI](https://pypi.org/project/pyportus/), `pip install pyportus`. To build manually, this project uses [maturin](https://github.com/PyO3/maturin). To test: 1. make a virtual environment 2. `maturin develop` To install: 1. `maturin build` 2. `pip install` the resulting .whl To then run/test, `cd examples && .//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 current `Cwnd` and `Rate`. Suppose your program defines just a single variable: `(def (acked 0))`, where `acked` adds up the total bytes acked since the last report. This value can be accessed as `r.acked`. Similarly, you can access the cwnd or rate as `r.Cwnd` and `r.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: 1. `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. 2. `datapath.update_field(field, val)`, which takes a variable in the `Report` scope of your datapath program and sets the value to `val`. For example, to update just the cwnd, you could use `datapath.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, in `on_create` to set an initial cwnd based on the datapath's `mss`) * `sock_id`: unique id of this flow in the datapath * `init_cwnd`: the initial congestion window this flow will have until you set it * `src_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: ```python 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 1. 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 handling `on_report`. 2. If you want to print anything, you should use `sys.stderr.write()` (note that you need to `import sys` and that it doesn't automatically add new lines for you like `print` does). 3. You *must* store a reference to `datapath` in `self` 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 mac * `class`: your algorithm class, e.g. `SampleCCAlg` * `debug (bool)`: if true, the CCP will log all messages passed between the ccp and datapath * `blocking (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`.