Developer Guide
Developer Guide extends User Guide with some information, which is relevant for:
- Sipplauncher developers and contributors
- Users, who wish to understand how Sipplauncher works under the hood
1. Initialization¶
-
Set up the signal handler
We need to perform proper cleanup in case if:
- a user interrupts with CTRL+C
- Sipplaucnher is terminated with any other signal
Otherwise, we'll leave stray "dummy" interfaces and partially-configured DUT.
Thus, we set up our signal handler.
There is a tricky place in signal handling - we can't do much inside the signal handler. We can't even log a message, because sometimes it causes a deadlock. The details of this issue are described in
Signals.py
.To work around this issue - we set a global variable inside the signal handler. And later we check this variable from the main code. Therefore, we have interruption points, which are deadlock-free.
-
Parse command-line arguments
To support Command-line arguments, we use the
argparse
library. -
Set up TLS session keys interception
To support Decrypting Diffie-Hellman TLS traffic, we include a 3rd-party library
sipplauncher/sslkeylog.c
. Inmain.py
we set the environment variableLD_PRELOAD
, which points tosslkeylog.so
.When we launch a child SIPp application, Linux checks this environment variable and preloads
sslkeylog.so
before running SIPp. This library overwrites some methods in the standard OpenSSL library. This makes possible to intercept and log TLS session keys.
Then Sipplauncher passes control to Run.run()
, which is defined at Run.py
.
Run.run()
runs in a single thread and performs the following steps:
2. SIPpTest list collection¶
TestPool
instance, defined at TestPool.py
, collects names of subfolders inside a test suite folder.
The names of subfolders are considered the Test names.
Then TestPool
:
- Sorts test names in alphabetical order
- Iterates over the test names.
During iterating, each test name is checked. The test is skipped, if:
- A test name matches the Template folder name.
- The
--pattern-exclude
command-line argument is specified and a test name matches it. - The
--pattern-only
command-line argument is specified and a test name doesn't match it.
If the test isn't skipped, a SIPpTest
is instantiated from a test name and the instance is added to the list.
A SIPpTest
instance, defined at Test.py
, encapsulates everything, needed to execute the test.
A list of SIPpTest
instances, sorted alphabetically by the test name, is returned to Run.run()
.
3. SIPpTest list processing¶
Then the --group
command-line argument is considered.
Run.run()
takes a slice of SIPpTests
, which consists of a --group
of elements, from the beginning of a SIPpTest
list.
This slice is considered a SIPpTest
run group.
Then Run.run()
processes a SIPpTest
run group.
Then Run.run()
takes the next slice from the SIPpTest
list and processes it.
This repeats until all SIPpTests
from the list are processed.
4. SIPpTest run group processing¶
SIPpTest
run group processing could be divided into stages:
- consecutive stages Pre-run and Post-run: they're run in the context of the
Run.run()
thread. - concurrent stage Run: it's run in the context of multiple spawned threads and processes.
During the stages each SIPpTest
transits through states:
State colors:
color | description | displayed to a user |
---|---|---|
green |
state during normal operation | yes |
blue |
state during normal operation | no |
red |
state during error | yes |
Here is the order of these stages:
1. Pre-run¶
All SIPpTests
in a run group start in the CREATED state.
Run.run()
consecutively iterates over SIPpTests
in a run group.
For each of them, SIPpTest.pre_run()
method is executed, which:
- Transits the Test into PREPARING state
-
Assigns dynamic IP addresses
The high-level overview is given in Dynamically assigned IP address. For a developer it's worth to add a few notes:
-
The check if the randomly obtained address is already occupied, is performed through the ARP ping. It's used instead of regular ICMP ping because some hosts in LAN might have ICMP replies disabled. And therefore, if we based on ICMP ping, we could cause Ethernet conflicts. The ARP ping is performed using the Scapy library.
-
A new "dummy" interface is created for each Test. Dummy interface is named with pattern
sipp-<test_run_id>
. Then the randomly generated IP addresses are assigned to the "dummy" interface. This way we do IP aliasing. We do the "dummy" interface approach instead of creating IP aliases via theip address add <addr> dev eth0
approach for the following reasons:- to ease network cleanup: just destroy all interfaces which name matches the
sipp-<>
pattern - to remove the need to specify or calculate network interface on which to create aliases: we rely on the Linux routing system
- to ease network cleanup: just destroy all interfaces which name matches the
-
-
Creates a test run folder
Test run folder is created by copying Test folder to location
/var/tmp/sipplaucnher/<test_name>/<test_run_id>
.Copying is performed using
shutil.copytree()
. -
Sets up logging into Test run folder
Sipplauncher logging facilities and paths are configured in
/usr/local/etc/sipplauncher/sipplauncher.configlog.conf
. Static log location is by default defined there as/tmp/sipplauncher.log
.We have the following requirements:
-
We need to easily separate the logs of different Test runs. Thus, we want to log the execution of each Test into a Test run folder. The log path should look like
/var/tmp/sipplauncher/<test_name>/<test_run_id>/sipplauncher.log
, where<test_run_id>
is random and isn't known beforehand. Therefore, the Test run log file location can't be static. It should be dynamic. -
We want to preserve the possibility to configure log message format in
/usr/local/etc/sipplauncher/sipplauncher.configlog.conf
.
To fulfill the above requirements, we implement the logging class
sipplauncher.utils.Log.DynamicFileHandler
. It should be specified as a loggingclass
for those logs, which need to be stored into a Test run folder.At runtime, we check if the logging class is set to
sipplauncher.utils.Log.DynamicFileHandler
. And if yes, we supply an actual Test run folder path tosipplauncher.utils.Log.DynamicFileHandler
instance. -
-
Replaces keywords using the Template Engine
A list of files, which need to be processed using the Template engine, is collected:
- Scripts: result of
glob.glob("*.sh")
. - SIPp scenarios: the files, which match pattern
^(ua[cs])_(ua[0-9]+).xml$
or^([a-zA-Z0-9]+)_(ua[cs])_(ua[0-9]+).xml$
. - DNS zone description file: a
dns.txt
file.
All these files are rendered by the Jinja2 API
Template.render()
.If the rendering result differs from the original file content - the file is overwritten with the newly rendered content.
- Scripts: result of
-
Generates SSL certificates and keys
The process is described here. Python
OpenSSL
library is used. -
Adds DNS zone description to the embedded DNS server
If the Embedded DNS server hasn't been launched yet, it's launched now.
Then a DNS zone description file is added to the embedded DNS server.
Python
dnslib
library is used. -
Activates pcap sniffing
We use
scapy.sendrecv.AsyncSniffer
to start a new background thread. This thread installs the BPF on all system network interfaces. The BPF matches all traffic regarding Dynamically assigned IP addresses for this particular Test run. The captured traffic is stored in the memory buffer. -
Runs before.sh
If
before.sh
is present in a Test run folder, we execute it withsubprocess.Popen()
API.Then we wait for it to finish and check its exit code.
-
Transits the Test into the READY state
If we got an error at any of the steps above - the TEST gets transited into the NOT READY state. Sipplauncher doesn't move to the next stages in this case.
2. Run¶
Run.run()
iterates over SIPpTests
in a run group and for each of them launches the SIPpTest.run()
method in a dedicated Python Thread
.
Then Run.run()
waits for threads to finish.
SIPpTest.run()
method performs following steps:
-
Transits the Test into STARTING state
-
Forks a new PysippProcess
PysippProcess
is a subclass of themultiprocessing.Process
.We need to fork the
Process
, because we have the requirement to store all the logs, which relate to Test run, in the Test run folder.From this requirement we get the following outcome:
- Some of the SIPp log locations are not configurable and are logged to a current working directory.
Therefore we need to
os.chdir()
into a Test run folder before running SIPp. os.chdir()
changes the current working directory for a whole current process. However, we are going to use it inside the concurrently running Threads. Therefore, this introduces the race condition.- Therefore, we need to launch a child
Process
from aThread
. And then, inside the childProcess
, callos.chdir()
and then run SIPp. This way, we change the working directory inside the child process and avoid the race condition in the parent process.
PysippProcess
determines the SIPp launch order from SIPp scenario file names. This process is described here. ThenPysippProcess
launches SIPp instances using the API of thePysipp
library. - Some of the SIPp log locations are not configurable and are logged to a current working directory.
Therefore we need to
-
Reports test result
SIPpTest.run()
waits forPysippProcess
to finish.The test is reported as FAIL if
exitcode
is non-zero, SUCCESS otherwise.A
SIPpTest.run()
Thread measures time from its begging and reports the amount of time elapsed.
3. Post-run¶
Post-run performs a rollback of actions, which were done in the Pre-run. The rollback is performed in the opposite order of the Pre-run.
Run.run()
consecutively iterates over SIPpTests
in a run group.
For each of them, SIPpTest.post_run()
method is executed, which:
-
Transits the Test into the CLEANING state
-
Runs after.sh
If
after.sh
is present in a Test run folder, we execute it withsubprocess.Popen()
API.Then we wait for it to finish and check its exit code.
-
Deactivates pcap sniffing
We invoke
scapy.sendrecv.AsyncSniffer.stop()
and wait until the backgroundThread
terminates.Then we sort the memory buffer with pcap frames by the frame timestamp. This is needed, because in case if traffic goes through different network interfaces, it could appear in a slightly wrong order inside the memory buffer.
Then we store the sorted memory buffer in file
sipp-<test_run_id>.pcap
in a Test run folder. -
Removes DNS zone description from the embedded DNS server
-
Removes a test run folder
We remove it with
shutil.rmtree()
, unless the--leave-temp
command-line argument was provided. -
Removes dynamic IP addresses
We remove a "dummy" pseudo-interface with name
sipp-<test_run_id>
. -
Transits the Test into the CLEAN state
If we got an error at any of the steps above - the TEST gets transited into the DIRTY state.