Contents
- Introduction
- Problem description.
- Discovery and research phase
- Plan of action
- Writing tests
- Closing thoughts
- References
- GitHub repository with code for this post
Introduction
In this post we're going to write Python program that generates DSCP to ToS conversion table while avoiding hardcoding values as much as possible. We will then save the final table to csv file with pre-defined column headers.
I got the idea for this blog article from the tweet posted the other day by Nick Russo. I thought it is an interesting problem to tackle as similar ones pop up all the time during early stages of Network Automation journey. What makes this challenge great is that it requires us to carry out tasks that apply to writing larger programs.
- We need to understand the problem and possibly do some research.
- We have to come up with plan of action.
- We need to break down larger tasks into smaller pieces.
- We need to implement all of the pieces we identified.
- Finally we have to put pieces back together and make sure final product works.
Before you continue reading this post I encourage you to try to implement solution to this problem yourself. Programming really is one of those skills that you learn best by doing. Once you've got something in place you can come back and see how I tackled this.
Full disclosure, I posted my hastily put together initial solution on Twitter but I made a lot of tweaks since then. It was lunch time and your first solution will rarely be the final one :) You can find it on Github, with few revisions due to several mistakes, and see how it evolved into solution I'm presenting here.
Problem description.
Our goal is to produce DSCP to ToS conversion table, additionally we need to provide multiple representations and bit values for each DSCP and ToS code. The end result should resemble the following CSV:
Discovery and research phase
There is quite a lot going on here but few patterns become apparent quite quickly.
We can see that some columns show the same value with the only change being number base. DSCP and ToS codes are shown in binary, decimal and hexadecimal bases. This is something we should be able to tackle even without understanding much about meaning of DSCP and ToS codes.
Looking at the entries it also appears that ToS = DSCP x 4. But we should not take this for granted, we should do research and refer to documents defining DSCP and ToS. Humans have tendency to see patterns everywhere and this can sometimes make us come up with good solution to a wrong problem.
With that being said I searched for RFC that can help us with understanding what DSCP and ToS are.
Below ToS definition is taken from RFC795:
The IP Type of Service has the following fields:
Bits 0-2: Precedence.
Bit 3: 0 = Normal Delay, 1 = Low Delay.
Bits 4: 0 = Normal Throughput, 1 = High Throughput.
Bits 5: 0 = Normal Relibility, 1 = High Relibility.
Bit 6-7: Reserved for Future Use.
0 1 2 3 4 5 6 7
+-----+-----+-----+-----+-----+-----+-----+-----+
| | | | | | |
| PRECEDENCE | D | T | R | 0 | 0 |
| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
111 - Network Control
110 - Internetwork Control
101 - CRITIC/ECP
100 - Flash Override
011 - Flash
010 - Immediate
001 - Priority
000 - Routine
You might have noticed that some items here match elements in our table. Precedence bits 0-2 match binary and decimal ToS Precedence columns as well as the ToS String Format. Bits 3, 4 and 5 match ToS Delay, Throughput and Reliability.
So we figured quite a lot already by noticing that same value is represented in different base number bases and then by looking at one RFC describing ToS.
We now need to figure out DSCP class codepoints (1st column) and understand how does DSCP map to ToS.
And to do that we'll look at more RFCs.
RFC2474 says this about DSCP:
A replacement header field, called the DS field, is defined, which is
intended to supersede the existing definitions of the IPv4 TOS octet
[RFC791] and the IPv6 Traffic Class octet [IPv6].
Six bits of the DS field are used as a codepoint (DSCP) to select the
PHB a packet experiences at each node. A two-bit currently unused
(CU) field is reserved and its definition and interpretation are
outside the scope of this document. The value of the CU bits are
ignored by differentiated services-compliant nodes when determining
the per-hop behavior to apply to a received packet.
The DS field structure is presented below:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| DSCP | CU |
+---+---+---+---+---+---+---+---+
DSCP: differentiated services codepoint
CU: currently unused
Aha! So we now know that ToS uses 8 bits and DSCP uses just first 6 bits of the same byte in the IP header. From that follows that when converting from DSCP to ToS we will have to shift our number by two bits to the left.
But what does that mean? Well, each bit we shift our value by equals to multiplying by 2. In example below we shift binary number 0010 (decimal 2) to the left by 2 bits with resulting number being binary 1000 (decimal 8).
bin 0010 - dec 2
shift by 2 bits to the left
bin 1000 - dec 8
Hopefully now you can see that our initial hunch was correct, ToS = DSCP x 4, but now you also know why, which is much more important.
Great, so we have pretty much all pieces of the puzzle now. Let's find out what are the names of DSCP codepoints.
RFC4594 has this to say on the topic:
------------------------------------------------------------------
| Service | DSCP | DSCP | Application |
| Class Name | Name | Value | Examples |
|===============+=========+=============+==========================|
|Network Control| CS6 | 110000 | Network routing |
|---------------+---------+-------------+--------------------------|
| Telephony | EF | 101110 | IP Telephony bearer |
|---------------+---------+-------------+--------------------------|
| Signaling | CS5 | 101000 | IP Telephony signaling |
|---------------+---------+-------------+--------------------------|
| Multimedia |AF41,AF42|100010,100100| H.323/V2 video |
| Conferencing | AF43 | 100110 | conferencing (adaptive) |
|---------------+---------+-------------+--------------------------|
| Real-Time | CS4 | 100000 | Video conferencing and |
| Interactive | | | Interactive gaming |
|---------------+---------+-------------+--------------------------|
| Multimedia |AF31,AF32|011010,011100| Streaming video and |
| Streaming | AF33 | 011110 | audio on demand |
|---------------+---------+-------------+--------------------------|
|Broadcast Video| CS3 | 011000 |Broadcast TV & live events|
|---------------+---------+-------------+--------------------------|
| Low-Latency |AF21,AF22|010010,010100|Client/server transactions|
| Data | AF23 | 010110 | Web-based ordering |
|---------------+---------+-------------+--------------------------|
| OAM | CS2 | 010000 | OAM&P |
|---------------+---------+-------------+--------------------------|
|High-Throughput|AF11,AF12|001010,001100| Store and forward |
| Data | AF13 | 001110 | applications |
|---------------+---------+-------------+--------------------------|
| Standard | DF (CS0)| 000000 | Undifferentiated |
| | | | applications |
|---------------+---------+-------------+--------------------------|
| Low-Priority | CS1 | 001000 | Any flow that has no BW |
| Data | | | assurance |
------------------------------------------------------------------
Figure 3. DSCP to Service Class Mapping
Just what we needed! All of the DSCP names and corresponding values in one place.
This has been great but notice that we didn't write a single line of code! And this is how it should be. Often you will feel like jumping right in and getting those Python statements flowing. I would however advise you to spend some time to understand the problem first, you might find out that once you have better grasp of the task it will make it easier to write your code.
With that said, I think we're ready to get our hands dirty :)
Plan of action
I decided to split this problem by treating value generation for each column as a separate task to implement. I'm also separating generation of values from rendering of the whole table and from writing the table to the CSV file.
Here's my high level plan of action.
- Decide on what are the initial values.
- Generate each of the resulting values from initial values.
- Put values together to form table row.
- Generate final conversion table.
- Write conversion table to CSV file.
Initial values
I'm going to start with choosing initial values from which we will derive all of the other items. Natural choice here will be DSCP or ToS codes. You can't go wrong with either but I'll pick DSCP because numbers are smaller and they look more familiar to me.
Looking at DSCP codes in the table from the beginning of the post we have 0, then jump to 8, and then numbers increase by 2 all the way to 40. So that's a nice pattern. Finally we have numbers 46, 48 and 56.
From that we could define a list and type all of the numbers by hand, like so:
dscps_dec = [0, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 46, 48, 56]
This is a bit verbose though and our original problem asked to use logic where possible, within reason as we want this code to be easy to understand.
Below is what I ended up doing:
dscps_dec = (0, *range(8, 41, 2), 46, 48, 56)
I created a tuple, with 0, 46, 48, 56 typed manually and the numbers between 8 and 40 are unpacked from sequence returned by range().
DSCP to ToS conversion and different number bases
Now that we have our DSCP values we can start looking into generating remaining elements.
First let's get on with DSCP to ToS conversion and representations in different number bases. This should be a good warm up before more involving tasks.
For converting to binary and hexadecimal we'll employ Python's string format()
method:
"{:06b}".format(dscp)
"{:#04x}".format(dscp)
And a little test:
>>> dscp = 22
>>> print("{:06b}".format(dscp))
010110
>>> print("{:#04x}".format(dscp))
0x16
That's looking good. Let's look more closely at what's happening here.
-
"{:06b}"
-b
means take provided number and display it in binary base,6
makes field 6 characters wide and0
adds 0s in the front if the resulting number has less than 6 digits. -
{:#04x}
-x
means display number in hexadecimal base,4
makes field 4 characters wide,0
prepends 0s if needed and#
asks for display of base indicator0x
.
Not too bad. We've got 3 columns sorted out, now we'll tackle ToS numbers.
We already found out that we need to shift DSCP number by two bits to arrive at ToS value. We could multiply DSCP by 4 but to emphasize our intent here we'll use Python's bitwise operator <<
.
tos_dec = dscp << 2
And some tests:
>>> dscp = 16
>>> tos_dec = dscp << 2
>>> tos_dec
64
>>> dscp = 28
>>> tos_dec = dscp << 2
>>> tos_dec
112
With that in hand we can now get bin and hex values, we'll use similar string formatting as we did for DSCP but we'll make binary string field 8 characters wide as ToS values use all 8 bits.
"{:#04x}".format(tos_dec)
"{:08b}".format(tos_dec)
>>> tos_dec = 112
>>> print("{:08b}".format(tos_dec))
01110000
>>> print("{:#04x}".format(tos_dec))
0x70
Perfect, two more columns ticked off.
ToS Precedence, Delay, Throughput and Reliability
Let's move onto ToS Precedence and Delay, Throughput and Reliability bits. We know that Precedence is encoded by bits 0-2. And we just computed binary number so perhaps we can reuse that?
Here's what I did:
tos_bin = "{:08b}".format(tos_dec)
tos_prec_bin = tos_bin[:3]
And a little test:
>>> tos_dec = 112
>>> tos_bin = "{:08b}".format(tos_dec)
>>> tos_prec_bin = tos_bin[:3]
>>> tos_prec_bin
'011'
Since we'll be using binary ToS value on it's own and to help us get ToS Precedence value, I assigned result of binary base conversion to a variable. That variable now holds a reference to a string from which we need first 3 digits for ToS Precedence. We can use list slicing here, and assign the slice to a new variable.
We got ToS precedence in binary but we also need it in decimal form. We can take advantage of built-in int()
function that can take string representation of number with optional base, and will give use back decimal.
int(tos_prec_bin, 2)
We feed it result from our previous assignment of tos_prec_bin
:
>>> int(tos_prec_bin, 2)
3
Hey, it works! This is going quite well so far.
We still got 3 single bits to work out. We can see in RFC795 that we need to check if bit at given position is set or not. We will also need to check bits in 3 different places, so it makes sense to encapsulate this logic in the separate function.
To check if bit is set we should look at bitwise AND which in Python is done with &
operator.
Ok, but what do we AND with what? On one side we will have our ToS number but we need to figure the other side.
Looking again at the diagram taken from RFC we see that bits are counted from left to right, starting with bit 0.
0 1 2 3 4 5 6 7
+-----+-----+-----+-----+-----+-----+-----+-----+
| | | | | | |
| PRECEDENCE | D | T | R | 0 | 0 |
| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
To check bits 3, 4 and 5 we could use corresponding binary numbers:
0b00010000
0b00001000
0b00000100
If we apply bitwise AND to first binary number 0b00010000
and whatever value we pass, we will get 0b00010000
only if passed number has bit 3 set to 1. Otherwise end result will be 0.
Here's an example of how that would work:
>>> tos_bin = 0b0011_0000
>>> bw_and_res = tos_bin & 0b0001_0000
>>> "{:#010b}".format(bw_and_res)
'0b00010000'
Hopefully now you can see that bitwise AND operator can help us check if given bit is set or not.
There's one slight problem though, we want to get back number 1 if bit is set or 0 if it is not. Right now we get back 8-bit binary number. We'll now fix it and encapsulate the logic in the function named kth_bit8_val
.
def kth_bit8_val(byte, k):
"""
Returns value of k-th bit
Most Significant Bit, bit-0, on the Left
:param byte: 8 bit integer to check
:param k: bit value to return
:return: 1 if bit is set, 0 if not
"""
return 1 if byte & (0b1000_0000 >> k) else 0
Our function is essentially one liner that does the following:
-
Takes provided number in
byte
argument and k-th bit to check ink
argument. -
Shifts binary number
0b10000000
to right by k bits with(0b1000_0000 >> k)
. This prepares our mask for bitwise AND operation. If we wanted to check bit 3 we'd get0b00010000
. Remember we count from 0. -
With mask in place we can AND our number with the mask
(byte & (0b1000_0000 >> k)
. Result will be either binary number 0 or binary number equal to our mask if the required bit is set, e.g.0b00010000
. -
Because we don't want the 8-bit binary number but just 1 or 0, we write short
if..else
statement that returns 0 if binary number is 0 or 1 if it's anything else.
Here's an example of our function in action:
>>> tos_val = 152
>>> kth_bit8_val(tos_val, 3)
1
>>> kth_bit8_val(tos_val, 4)
1
>>> kth_bit8_val(tos_val, 5)
0
With our function in place we can now get values for ToS Delay, Throughput and Reliability bits:
kth_bit8_val(tos_dec, 3)
kth_bit8_val(tos_dec, 4)
kth_bit8_val(tos_dec, 5)
Let's try these out:
>>> tos_dec = 88
>>> kth_bit8_val(tos_dec, 3)
1
>>> kth_bit8_val(tos_dec, 4)
1
>>> kth_bit8_val(tos_dec, 5)
0
>>> f"{tos_dec:08b}"
'01011000'
As you can see we got correct values for bits 3, 4 and 5. Another task completed :).
ToS string formats
We now only have 2 tasks left, getting DSCP class and ToS string format. These will require some manual typing but we can still have decent amount of logic.
First we'll get ToS string format out of the way.
If you look at RFC795 excerpt again you should see that ToS strings depend on the value of ToS Precedence field.
0 1 2 3 4 5 6 7
+-----+-----+-----+-----+-----+-----+-----+-----+
| | | | | | |
| PRECEDENCE | D | T | R | 0 | 0 |
| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
111 - Network Control
110 - Internetwork Control
101 - CRITIC/ECP
100 - Flash Override
011 - Flash
010 - Immediate
001 - Priority
000 - Routine
And we already have that, we used int(tos_prec_bin, 2)
before to get value of ToS Precedence field in decimal. Let's assign this to a variable.
Next we'll create a dictionary with keys being all possible ToS numbers and values in dictionary being corresponding strings.
tos_prec_dec = int(tos_prec_bin, 2)
TOS_STRING_TBL = {
0: "Routine",
1: "Priority",
2: "Immediate",
3: "Flash",
4: "FlashOverride",
5: "Critical",
6: "Internetwork Control",
7: "Network Control",
}
With tos_prec_dec
variable in place we can use its value to get from dictionary corresponding string format.
TOS_STRING_TBL[tos_prec_dec]
Example use:
>>> tos_prec_bin = "110"
>>> tos_prec_dec = int(tos_prec_bin, 2)
>>> TOS_STRING_TBL[tos_prec_dec]
'Internetwork Control'
>>> tos_prec_bin = "010"
>>> tos_prec_dec = int(tos_prec_bin, 2)
>>> TOS_STRING_TBL[tos_prec_dec]
'Immediate'
That seems to be working fine, awesome! Only one more task left. Getting DSCP class.
DSCP classes
Let's have a look again at what RFC2579 and RFC4594 say about DSCP classes.
RFC2597:
The RECOMMENDED values of the AF codepoints are as follows: AF11 = '
001010', AF12 = '001100', AF13 = '001110', AF21 = '010010', AF22 = '
010100', AF23 = '010110', AF31 = '011010', AF32 = '011100', AF33 = '
011110', AF41 = '100010', AF42 = '100100', and AF43 = '100110'. The
table below summarizes the recommended AF codepoint values.
Class 1 Class 2 Class 3 Class 4
+----------+----------+----------+----------+
Low Drop Prec | 001010 | 010010 | 011010 | 100010 |
Medium Drop Prec | 001100 | 010100 | 011100 | 100100 |
High Drop Prec | 001110 | 010110 | 011110 | 100110 |
+----------+----------+----------+----------+
RFC4594 has this to say on the topic:
------------------------------------------------------------------
| Service | DSCP | DSCP | Application |
| Class Name | Name | Value | Examples |
|===============+=========+=============+==========================|
|Network Control| CS6 | 110000 | Network routing |
|---------------+---------+-------------+--------------------------|
| Telephony | EF | 101110 | IP Telephony bearer |
|---------------+---------+-------------+--------------------------|
| Signaling | CS5 | 101000 | IP Telephony signaling |
|---------------+---------+-------------+--------------------------|
| Multimedia |AF41,AF42|100010,100100| H.323/V2 video |
| Conferencing | AF43 | 100110 | conferencing (adaptive) |
|---------------+---------+-------------+--------------------------|
| Real-Time | CS4 | 100000 | Video conferencing and |
| Interactive | | | Interactive gaming |
|---------------+---------+-------------+--------------------------|
| Multimedia |AF31,AF32|011010,011100| Streaming video and |
| Streaming | AF33 | 011110 | audio on demand |
|---------------+---------+-------------+--------------------------|
|Broadcast Video| CS3 | 011000 |Broadcast TV & live events|
|---------------+---------+-------------+--------------------------|
| Low-Latency |AF21,AF22|010010,010100|Client/server transactions|
| Data | AF23 | 010110 | Web-based ordering |
|---------------+---------+-------------+--------------------------|
| OAM | CS2 | 010000 | OAM&P |
|---------------+---------+-------------+--------------------------|
|High-Throughput|AF11,AF12|001010,001100| Store and forward |
| Data | AF13 | 001110 | applications |
|---------------+---------+-------------+--------------------------|
| Standard | DF (CS0)| 000000 | Undifferentiated |
| | | | applications |
|---------------+---------+-------------+--------------------------|
| Low-Priority | CS1 | 001000 | Any flow that has no BW |
| Data | | | assurance |
------------------------------------------------------------------
Figure 3. DSCP to Service Class Mapping
So it looks like we can build our logic by looking at first 3 bits and then 2 bits after that. Last bit is always set to 0 so we can ignore it.
Now, if you remember, bits 0-2 correspond to ToS Precedence value, and we already worked that out. Bits 3 and 4 are ToS Delay and Throughput bits, which we know already as well.
It looks like we can pass the values we previously computed to a function and then inside of the function we'll work out the DSCP name.
Below is what I came up with:
def dscp_class(bits_0_2, bit_3, bit_4):
"""
Takes values of DSCP bits and computes dscp class
Bits 0-2 decide major class
Bit 3-4 decide drop precedence
:param bits_0_2: int: decimal value of bits 0-2
:param bit_3: int: value of bit 3
:param bit_4: int: value of bit 4
:return: DSCP class name
"""
bits_3_4 = (bit_3 << 1) + bit_4
if bits_3_4 == 0:
dscp_cl = "cs{}".format(bits_0_2)
elif (bits_0_2, bits_3_4) == (5, 3):
dscp_cl = "ef"
else:
dscp_cl = "af{}{}".format(bits_0_2, bits_3_4)
return dscp_cl
We take 3 arguments since we will have those available from earlier computations. So we get separately bits 0-2, bit 3 and bit 4.
First we compute decimal value held by bits 3 and 4 as this decides precedence drop.
bits_3_4 = (bit_3 << 1) + bit_4
With that in hand we can start building if..else
logic.
We know that if bits 3 and 4 are equal to 0 we're dealing with cs
class. The actual number of the cs
class is decided by bits 0-2, and we know value of those. So we can insert that value into the string:
if bits_3_4 == 0:
dscp_cl = "cs{}".format(bits_0_2)
Next off we're dealing with an exception, ef
class. Here we need to check if bits 0-2 equal to 5 and bits 3-4 equal to 3 as per RFC table:
| Telephony | EF | 101110 | IP Telephony bearer |
elif (bits_0_2, bits_3_4) == (5, 3):
dscp_cl = "ef"
Finally to build string for af
class we append value held in bits 0-2 followed by value held in bits 3-4:
else:
dscp_cl = "af{}{}".format(bits_0_2, bits_3_4)
Let's see the completed function in action:
>>> tos_prec = 0b001
>>> tos_del = 0
>>> tos_thr = 0
>>> dscp_class(tos_prec, tos_del, tos_thr)
'cs1'
>>> tos_prec = 0b010
>>> tos_del = 1
>>> tos_thr = 1
>>> dscp_class(tos_prec, tos_del, tos_thr)
'af23'
>>> tos_prec = 0b101
>>> tos_del = 1
>>> tos_thr = 1
>>> dscp_class(tos_prec, tos_del, tos_thr)
'ef'
Things are looking good indeed.
And that's it, we've got all of the components in place. Now we need to put them together.
Generating conversion table row
I decided to move building of all of the values for given DSCP code into its own function. It's easier for me to reason about this code, as well as test it later, when it is separated. I named this function dscp_conv_tbl_row
:
def dscp_conv_tbl_row(dscp):
"""
Generates DSCP to ToS conversion values as well as different representations
:param dscp: int: decimal DSCP code
:return: dict with each value assigned to value name
"""
tos_dec = dscp << 2
tos_bin = "{:08b}".format(tos_dec)
tos_prec_bin = tos_bin[0:3]
tos_prec_dec = int(tos_prec_bin, 2)
tos_del_fl = kth_bit8_val(tos_dec, 3)
tos_thr_fl = kth_bit8_val(tos_dec, 4)
tos_rel_fl = kth_bit8_val(tos_dec, 5)
dscp_cl = dscp_class(tos_prec_dec, tos_del_fl, tos_thr_fl)
tbl_row_vals = (
dscp_cl,
"{:06b}".format(dscp),
"{:#04x}".format(dscp),
dscp,
tos_dec,
"{:#04x}".format(tos_dec),
tos_bin,
tos_prec_bin,
tos_prec_dec,
tos_del_fl,
tos_thr_fl,
tos_rel_fl,
TOS_STRING_TBL[tos_prec_dec],
)
return tbl_row_vals
We've already seen most of this code, it's just now put together. We're taking DSCP value and computing values for different components that we need in our conversion table. We finally return all of the items in a tuple.
This is how it looks like when we run it:
>>> dscp_conv_tbl_row(28)
('af32', '011100', '0x1c', 28, 112, '0x70', '01110000', '011', 3, 1, 0, 0, 'Flash')
All of the values for one table row. Now we need to write code that computes these tuples for each of DSCP codes we require. Our end goal is to have full table that is saved to CSV. This is a good place to think about how to store the resulting rows.
Building final table
We know we want to save final product in CSV format so generating one big string is probably not a good idea. Either list of dictionary would work best here.
Python comes with built-in library for working with CSV files, named appropriately enough csv
. We can use either of its methods writer()
or DictWriter()
to write our data to file. I opted for DictWriter()
as this allows me to automatically map my data structure onto columns in resulting file. I suggest you try out both and see what works better for you.
Now that I know how I want to write to CSV file I know that my rows will be recorded as list of dictionaries. Time to write function that encapsulates this logic:
def gen_dscp_conversion_table():
"""
Generates DSCP to TOS conversion table with rows for selected DSCP codes
Final table is a list of dictionaries to make writing CSV easier
:return: list(dict): final DSCP to TOS conversion table
"""
column_names = (
"DSCP Class",
"DSCP (bin)",
"DSCP (hex)",
"DSCP (dec)",
"ToS (dec)",
"ToS (hex)",
"ToS (bin)",
"ToS Prec. (bin)",
"ToS Prec. (dec)",
"ToS Delay Flag",
"ToS Throughput Flag",
"ToS Reliability Flag",
"TOS String Format",
)
# These are the DSCP codes we're interested in
dscps_dec = (0, *range(8, 41, 2), 46, 48, 56)
conv_tbl = [dict(zip(column_names, dscp_conv_tbl_row(dscp))) for dscp in dscps_dec]
return conv_tbl
As you can see we first define tuple with column names, these match what will end up in the the CSV file.
Then we create tuple with DSCP values and finally we create list comprehension inside of which we create our dictionaries.
This list comprehension is composed of few elements so I'm going to break it down for you.
-
for dscp in dscps_dec
- provides decimal dscp values for which we generate table rows. -
dscp_conv_tbl_row(dscp)
- returns tuple with all values for our table row for given dscp. -
zip(column_names, dscp_conv_tbl_row(dscp))
- goes over both column names and table row values and pairs 1st item from one with 1st item from second, then it moves to 2nd pair and so on. Result is a sequence of tuples in(column_name, dscp_tbl_row)
format. -
dict(zip(column_names, dscp_conv_tbl_row(dscp)))
- takes sequence of tuples and turns them into dictionary with 1st element being turned into key and 2nd element becoming value.
The end result of this list comprehension is a list of dictionaries, each dictionary having column names as keys with corresponding values mapped to them.
Writing table to CSV file
We're almost there. All that's left for us to do is to write fruits of our hard labor into the file.
def main():
dscp_conversion_table = gen_dscp_conversion_table()
with Path("dscp_tos_conv_table.csv").open(mode="w", newline="") as fout:
dict_writer = csv.DictWriter(f=fout, fieldnames=dscp_conversion_table[0].keys())
dict_writer.writeheader()
dict_writer.writerows(dscp_conversion_table)
-
We're generating the full table first. Then we open file we want to write to, we specify write mode and newline is set to
""
as required by csv library. -
Next we create
csv.DictWriter()
object giving it our file. We set fieldnames argument to keys from the first dictionary in our list. All dictionaries have the same keys so it doesn't matter which one we take. -
Finally we ask
DictWriter
to write header to the file, and then we write to the same file all of the entries in one go. We gaveDictWriter
dictionary keys so it's able to map all of the row values automatically.
And with that we came to an end. Or have we?
Yes and no. We built program generating DSCP to ToS conversion table and we wrote the table to CSV file. But, do we know if this works correctly? Do we know if the values it produces are correct? And even if they are correct now, can we be certain that they will stay correct if we modify the program?
The answer is: we don't really know if any of this is correct. Maybe we checked the wrong bit somewhere or maybe there's a typo in a DSCP class name. Even a program that can fit on two pages of text can introduce a fair number of bugs.
Writing tests
You might already think what I'm thinking: testing. We need testing.
So testing we shall have.
In the course of writing our program we created 4 functions:
dscp_class
kth_bit8_val
dscp_conv_tbl_row
gen_dscp_conversion_table
I want to write tests for 3 of these but not gen_dscp_conversion_table
. This one mostly relies on values generated by other 3 and I don't feel like we need to have coverage here.
So, how do we test?
We should look at each of these functions and think of values that will give us good confidence in our code being correct. For example dscp_class
has if..else
statement so it would be a good idea to test it with values that will make code take all possible paths. On top of that we want to add few cases that follow the most common path.
For my tests I'm using pytest
and pytest.mark.parametrize()
decorator. This allows me to define multiple test cases and pass them as parameters to our test function. Without it we'd have to write multiple assert
statements.
Testing dscp_class
For testing dscp_class
I chose values that should result in csX
, afXY
and ef
DSCP classes. This accounts for all branches of if..else
statement. To make sure our logic is correct I got few different examples for each of the major classes.
Here are the final test values and test code that I came up with:
@pytest.mark.parametrize(
"bits_0_2, bit_3, bit_4, res_class",
[
(0b000, 0, 0, "cs0"),
(0b001, 0, 1, "af11"),
(0b011, 0, 0, "cs3"),
(0b011, 1, 0, "af32"),
(0b100, 1, 1, "af43"),
(0b101, 1, 1, "ef"),
(0b111, 0, 0, "cs7"),
]
)
def test_dscp_class(bits_0_2, bit_3, bit_4, res_class):
assert dscp_class(bits_0_2, bit_3, bit_4) == res_class
Testing kth_bit8_val
For kth_bit8_val
I decided to choose just few values. Corner case here is checking first and last bits, so we need to make sure these are handled correctly. Also each bit can be either set or unset (0 or 1) so we test if both 0 and 1 bit values are picked up.
Here is our test code:
@pytest.mark.parametrize(
"byte, k_bit, bit_val",
[
(0b1000_0000, 0, 1),
(0b1110_1111, 3, 0),
(0b0100_0110, 5, 1),
(0b0000_0001, 7, 1),
]
)
def test_kth_bit8_val(byte, k_bit, bit_val):
assert kth_bit8_val(byte, k_bit) == bit_val
Testing test_gen_table_row
Lastly, we have test_gen_table_row
. Here I chose values for 7 different ToS strings as well as values corresponding to different DSCP classes. We also want to test if values are returned in the correct order. If my function passes all of these test I will be fairly confident that it is working correctly.
Testing code:
@pytest.mark.parametrize(
"dscp_code, conv_row_values",
[
(0, ("cs0", "000000", "0x00", 0, 0, "0x00", "00000000", "000", 0, 0, 0, 0, "Routine")),
(8, ("cs1", "001000", "0x08", 8, 32, "0x20", "00100000", "001", 1, 0, 0, 0, "Priority")),
(12, ("af12", "001100", "0x0c", 12, 48, "0x30", "00110000", "001", 1, 1, 0, 0, "Priority")),
(22, ("af23", "010110", "0x16", 22, 88, "0x58", "01011000", "010", 2, 1, 1, 0, "Immediate")),
(26, ("af31", "011010", "0x1a", 26, 104, "0x68", "01101000", "011", 3, 0, 1, 0, "Flash")),
(40, ("cs5", "101000", "0x28", 40, 160, "0xa0", "10100000", "101", 5, 0, 0, 0, "Critical")),
(46, ("ef", "101110", "0x2e", 46, 184, "0xb8", "10111000", "101", 5, 1, 1, 0, "Critical")),
(48, ("cs6", "110000", "0x30", 48, 192, "0xc0", "11000000", "110", 6, 0, 0, 0, "Internetwork Control")),
(56, ("cs7", "111000", "0x38", 56, 224, "0xe0", "11100000", "111", 7, 0, 0, 0, "Network Control")),
]
)
def test_gen_table_row(dscp_code, conv_row_values):
assert dscp_conv_tbl_row(dscp_code) == conv_row_values
Running tests
Right, we have test functions, we have test values, time to actually run these tests against our codebase:
F:\projects\dscp_tos_conv
(venv) λ pytest -v
=================== test session starts ======================
platform win32 -- Python 3.8.2, pytest-6.0.1, py-1.9.0, \
pluggy-0.13.1 -- f:\projects\dscp_tos_conv\venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: F:\projects\dscp_tos_conv
collected 20 items
test_dscp_tos_table.py::test_dscp_class[0-0-0-cs0] PASSED
[ 5%] test_dscp_tos_table.py::test_dscp_class[1-0-1-af11] PASSED
[ 10%] test_dscp_tos_table.py::test_dscp_class[3-0-0-cs3] PASSED
[ 15%] test_dscp_tos_table.py::test_dscp_class[3-1-0-af32] PASSED
[ 20%] test_dscp_tos_table.py::test_dscp_class[4-1-1-af43] PASSED
[ 25%] test_dscp_tos_table.py::test_dscp_class[5-1-1-ef] PASSED
[ 30%] test_dscp_tos_table.py::test_dscp_class[7-0-0-cs7] PASSED
[ 35%] test_dscp_tos_table.py::test_kth_bit8_val[128-0-1] PASSED
[ 40%] test_dscp_tos_table.py::test_kth_bit8_val[239-3-0] PASSED
[ 45%] test_dscp_tos_table.py::test_kth_bit8_val[70-5-1] PASSED
[ 50%] test_dscp_tos_table.py::test_kth_bit8_val[1-7-1] PASSED
[ 55%] test_dscp_tos_table.py::test_gen_table_row[0-conv_row_values0] PASSED
[ 60%] test_dscp_tos_table.py::test_gen_table_row[8-conv_row_values1] PASSED
[ 65%] test_dscp_tos_table.py::test_gen_table_row[12-conv_row_values2] PASSED
[ 70%] test_dscp_tos_table.py::test_gen_table_row[22-conv_row_values3] PASSED
[ 75%] test_dscp_tos_table.py::test_gen_table_row[26-conv_row_values4] PASSED
[ 80%] test_dscp_tos_table.py::test_gen_table_row[40-conv_row_values5] PASSED
[ 85%] test_dscp_tos_table.py::test_gen_table_row[46-conv_row_values6] PASSED
[ 90%] test_dscp_tos_table.py::test_gen_table_row[48-conv_row_values7] PASSED
[ 95%] test_dscp_tos_table.py::test_gen_table_row[56-conv_row_values8] PASSED
[100%]
=================== 20 passed in 0.07s ================================
Awesome, all of the tests passed!
You might think this looks great but I did make some mistakes in my code before showing you the final run, that's the whole idea of testing, to give you confidence that your code works they way you expect it to. And when it doesn't you go back and fix it :)
Without tests you could miss bugs lurking somewhere so it really is a good idea to get used to creating them alongside your main program. Hopefully I showed you here that no code is too small to have a few tests thrown in for good measure, the future you will be thankful!
Closing thoughts
I had fun working on this challenge. It really is a good use case for problem decomposition, learning string formatting and bit-wise operators. And despite not looking like a tough one at first it did require me to dig a bit deeper and make sure that I understand the problem I'm trying to solve.
Finally after making few silly mistakes during refactoring I was reminded that testing is rarely, if ever, optional. You can get away with not writing tests for long time but sooner or later you'll wish you'd learned it earlier!
I hope you learned something new and enjoyed this post as much as I did writing it :)
References
- Python
csv
library: https://docs.python.org/3/library/csv.html - Pytest parametrize: https://docs.pytest.org/en/stable/parametrize.html
- Python string formatting guide: https://realpython.com/python-formatted-output/
- Service Mappings: https://tools.ietf.org/html/rfc795
- Definition of DS Field: https://tools.ietf.org/html/rfc2474
- Assured Forwarding PHB Group: https://tools.ietf.org/html/rfc2597
- Configuration Guidelines for DiffServ: https://tools.ietf.org/html/rfc4594
- GitHub repo with source code for this post: GitHub repository with code for this post