blob: b2b175639d25e53ce9878e104ba949b2cbdded89 [file] [log] [blame]
Daniele Moroc9b4afe2021-08-26 18:07:01 +02001"""
2Copyright 2021 Open Networking Foundation (ONF)
3
4Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
5the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
6or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
7
8"""
9
10import pexpect
11import os
12from drivers.common.clidriver import CLI
13
14
15class P4RuntimeCliDriver(CLI):
16 """
17 Implements a P4Runtime Client CLI-based driver to control devices that uses
18 the P4Runtime Protocol, using the P4Runtime shell CLI.
19
20 This driver requires that P4Runtime CLI is configured to not generated colored
21 text. To do so, add the following lines to a file in
22 ~/.ipython/profile_default/ipython_config.py:
23 c.InteractiveShell.color_info = False
24 c.InteractiveShell.colors = 'NoColor'
25 c.TerminalInteractiveShell.color_info = False
26 c.TerminalInteractiveShell.colors = 'NoColor'
27 """
28
29 def __init__(self):
30 """
31 Initialize client
32 """
33 super(P4RuntimeCliDriver, self).__init__()
34 self.name = None
35 self.handle = None
36 self.p4rtAddress = "localhost"
37 self.p4rtPort = "9559" # Default P4RT server port
38 self.prompt = "\$"
39 self.p4rtShPrompt = ">>>"
40 self.p4rtDeviceId = "1"
41 self.p4rtElectionId = "0,100" # (high,low)
42 self.p4rtConfig = None # Can be used to pass a path to the P4Info and pipeline config
43
44 def connect(self, **connectargs):
45 """
46 Creates the ssh handle for the P4Runtime CLI
47 The ip_address would come from the topo file using the host tag, the
48 value can be an environment variable as well as a "localhost" to get
49 the ip address needed to ssh to the "bench"
50 """
51 try:
52 for key in connectargs:
53 vars(self)[key] = connectargs[key]
54 self.name = self.options.get("name", "")
55 self.p4rtAddress = self.options.get("p4rt_address", "localhost")
56 self.p4rtPort = self.options.get("p4rt_port", "9559")
57 self.p4rtShPrompt = self.options.get("p4rt_sh_prompt", ">>>")
58 self.p4rtDeviceId = self.options.get("p4rt_device_id", "1")
59 self.p4rtElectionId = self.options.get("p4rt_election_id", "0,100")
60 self.p4rtConfig = self.options.get("p4rt_config", None)
61 try:
62 if os.getenv(str(self.ip_address)) is not None:
63 self.ip_address = os.getenv(str(self.ip_address))
64 else:
65 main.log.info(self.name + ": ip set to " + self.ip_address)
66 except KeyError:
67 main.log.info(self.name + ": Invalid host name," +
68 "defaulting to 'localhost' instead")
69 self.ip_address = 'localhost'
70 except Exception as inst:
71 main.log.error("Uncaught exception: " + str(inst))
72
73 self.handle = super(P4RuntimeCliDriver, self).connect(
74 user_name=self.user_name,
75 ip_address=self.ip_address,
76 port=None,
77 pwd=self.pwd)
78 if self.handle:
79 main.log.info("Connection successful to the host " +
80 self.user_name +
81 "@" +
82 self.ip_address)
83 self.handle.sendline("")
84 self.handle.expect(self.prompt)
85 return main.TRUE
86 else:
87 main.log.error("Connection failed to " +
88 self.user_name +
89 "@" +
90 self.ip_address)
91 return main.FALSE
92 except pexpect.EOF:
93 main.log.error(self.name + ": EOF exception found")
94 main.log.error(self.name + ": " + self.handle.before)
95 main.cleanAndExit()
96 except Exception:
97 main.log.exception(self.name + ": Uncaught exception!")
98 main.cleanAndExit()
99
100 def startP4RtClient(self, pushConfig=False):
101 """
102 Start the P4Runtime shell CLI client
103
104 :param pushConfig: True if you want to push the pipeline config, False otherwise
105 requires the p4rt_config configuration parameter to be set
106 :return:
107 """
108 try:
109 main.log.debug(self.name + ": Starting P4Runtime Shell CLI")
110 grpcAddr = "%s:%s" % (self.p4rtAddress, self.p4rtPort)
Daniele Moro54fd0022021-10-15 17:40:27 +0200111 startP4RtShLine = "python3 -m p4runtime_sh -v --grpc-addr " + grpcAddr + \
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200112 " --device-id " + self.p4rtDeviceId + \
113 " --election-id " + self.p4rtElectionId
114 if pushConfig:
115 if self.p4rtConfig:
116 startP4RtShLine += " --config " + self.p4rtConfig
117 else:
118 main.log.error(
119 "You should provide a P4 Runtime config to push!")
120 main.cleanAndExit()
121 response = self.__clearSendAndExpect(startP4RtShLine)
Daniele Moro54fd0022021-10-15 17:40:27 +0200122 if "CRITICAL" in response:
123 main.log.exception(self.name + ": Connection error.")
124 main.cleanAndExit()
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200125 self.preDisconnect = self.stopP4RtClient
126 except pexpect.TIMEOUT:
127 main.log.exception(self.name + ": Command timed out")
Jon Halla4a79312022-01-25 17:16:53 -0800128 main.cleanAndExit()
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200129 except pexpect.EOF:
130 main.log.exception(self.name + ": connection closed.")
131 main.cleanAndExit()
132 except Exception:
133 main.log.exception(self.name + ": Uncaught exception!")
134 main.cleanAndExit()
135
136 def stopP4RtClient(self):
137 """
138 Exit the P4Runtime shell CLI
139 """
140 try:
141 main.log.debug(self.name + ": Stopping P4Runtime Shell CLI")
142 self.handle.sendline("exit")
143 self.handle.expect(self.prompt)
144 return main.TRUE
145 except pexpect.TIMEOUT:
146 main.log.exception(self.name + ": Command timed out")
147 return main.FALSE
148 except pexpect.EOF:
149 main.log.exception(self.name + ": connection closed.")
150 main.cleanAndExit()
151 except Exception:
152 main.log.exception(self.name + ": Uncaught exception!")
153 main.cleanAndExit()
154
155 def pushTableEntry(self, tableEntry=None, debug=True):
156 """
157 Push a table entry with either the given table entry or use the saved
158 table entry in the variable 'te'.
159
160 Example of a valid tableEntry string:
161 te = table_entry["FabricIngress.forwarding.routing_v4"](action="set_next_id_routing_v4"); te.action["next_id"] = "10"; te.match["ipv4_dst"] = "10.0.0.0" # nopep8
162
163 :param tableEntry: the string table entry, if None it uses the table
164 entry saved in the 'te' variable
165 :param debug: True to enable debug logging, False otherwise
166 :return: main.TRUE or main.FALSE on error
167 """
168 try:
169 main.log.debug(self.name + ": Pushing Table Entry")
170 if debug:
171 self.handle.sendline("te")
172 self.handle.expect(self.p4rtShPrompt)
173 pushCmd = ""
174 if tableEntry:
175 pushCmd = tableEntry + ";"
176 pushCmd += "te.insert()"
177 response = self.__clearSendAndExpect(pushCmd)
178 if "Traceback" in response or "Error" in response:
179 # TODO: other possibile errors?
180 # NameError...
181 main.log.error(
182 self.name + ": Error in pushing table entry: " + response)
183 return main.FALSE
184 return main.TRUE
185 except pexpect.TIMEOUT:
186 main.log.exception(self.name + ": Command timed out")
187 return main.FALSE
188 except pexpect.EOF:
189 main.log.exception(self.name + ": connection closed.")
190 main.cleanAndExit()
191 except Exception:
192 main.log.exception(self.name + ": Uncaught exception!")
193 main.cleanAndExit()
194
195 def deleteTableEntry(self, tableEntry=None, debug=True):
196 """
197 Deletes a table entry with either the given table entry or use the saved
198 table entry in the variable 'te'.
199
200 Example of a valid tableEntry string:
201 te = table_entry["FabricIngress.forwarding.routing_v4"](action="set_next_id_routing_v4"); te.action["next_id"] = "10"; te.match["ipv4_dst"] = "10.0.0.0" # nopep8
202
203 :param tableEntry: the string table entry, if None it uses the table
204 entry saved in the 'te' variable
205 :param debug: True to enable debug logging, False otherwise
206 :return: main.TRUE or main.FALSE on error
207 """
208 try:
209 main.log.debug(self.name + ": Deleting Table Entry")
210 if debug:
211 self.__clearSendAndExpect("te")
212 pushCmd = ""
213 if tableEntry:
214 pushCmd = tableEntry + ";"
215 pushCmd += "te.delete()"
216 response = self.__clearSendAndExpect(pushCmd)
217 main.log.debug(
218 self.name + ": Delete table entry response: {}".format(
219 response))
220 if "Traceback" in response or "Error" in response:
221 # TODO: other possibile errors?
222 # NameError...
223 main.log.error(
224 self.name + ": Error in deleting table entry: " + response)
225 return main.FALSE
226 return main.TRUE
227 except pexpect.TIMEOUT:
228 main.log.exception(self.name + ": Command timed out")
229 return main.FALSE
230 except pexpect.EOF:
231 main.log.exception(self.name + ": connection closed.")
232 main.cleanAndExit()
233 except Exception:
234 main.log.exception(self.name + ": Uncaught exception!")
235 main.cleanAndExit()
236
Daniele Morob8404e82022-02-25 00:17:28 +0100237 def modifyMeterEntry(self, meterEntry=None, debug=True):
238 """
239 Modify a meter entry with either the given meter entry or use the saved
240 meter entry in the variable 'me'.
241
242 Example of a valid tableEntry string:
243 me = meter_entry["FabricIngress.upf.app_meter"]; me.cir = 1; me.cburst=1; me.pir=1; me.pburst=1; # nopep8
244
245 :param meterEntry: the string meter entry, if None it uses the meter
246 entry saved in the 'me' variable
247 :param debug: True to enable debug logging, False otherwise
248 :return: main.TRUE or main.FALSE on error
249 """
250 try:
251 main.log.debug(self.name + ": Pushing Meter Entry")
252 if debug:
253 self.handle.sendline("me")
254 self.handle.expect(self.p4rtShPrompt)
255 pushCmd = ""
256 if meterEntry:
257 pushCmd = meterEntry + ";"
258 pushCmd += "me.modify()"
259 response = self.__clearSendAndExpect(pushCmd)
260 if "Traceback" in response or "Error" in response or "INVALID_ARGUMENT" in response:
261 # TODO: other possibile errors?
262 # NameError...
263 main.log.error(
264 self.name + ": Error in modifying meter entry: " + response)
265 return main.FALSE
266 return main.TRUE
267 except pexpect.TIMEOUT:
268 main.log.exception(self.name + ": Command timed out")
269 return main.FALSE
270 except pexpect.EOF:
271 main.log.exception(self.name + ": connection closed.")
272 main.cleanAndExit()
273 except Exception:
274 main.log.exception(self.name + ": Uncaught exception!")
275 main.cleanAndExit()
276
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200277 def buildP4RtTableEntry(self, tableName, actionName, actionParams={},
Daniele Morobef0c7e2022-02-16 17:47:13 -0800278 matchFields={}, priority=0):
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200279 """
280 Build a Table Entry
281 :param tableName: The name of table
282 :param actionName: The name of the action
283 :param actionParams: A dictionary containing name and values for the action parameters
284 :param matchFields: A dictionary containing name and values for the match fields
Daniele Morobef0c7e2022-02-16 17:47:13 -0800285 :param priority: for ternary match entries
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200286 :return: main.TRUE or main.FALSE on error
287 """
288 # TODO: improve error checking when creating the table entry, add
289 # params, and match fields.
290 try:
Daniele Morobef0c7e2022-02-16 17:47:13 -0800291 main.log.debug("%s: Building P4RT Table Entry "
292 "(table=%s, match=%s, priority=%s, action=%s, params=%s)" % (
Daniele Morob8404e82022-02-25 00:17:28 +0100293 self.name, tableName, matchFields, priority,
294 actionName, actionParams))
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200295 cmd = 'te = table_entry["%s"](action="%s"); ' % (
296 tableName, actionName)
297
298 # Action Parameters
299 for name, value in actionParams.items():
300 cmd += 'te.action["%s"]="%s";' % (name, str(value))
301
302 # Match Fields
303 for name, value in matchFields.items():
304 cmd += 'te.match["%s"]="%s";' % (name, str(value))
305
Daniele Morobef0c7e2022-02-16 17:47:13 -0800306 if priority:
307 cmd += 'te.priority=%s;' % priority
308
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200309 response = self.__clearSendAndExpect(cmd)
310 if "Unknown action" in response:
311 main.log.error("Unknown action: " + response)
312 return main.FALSE
313 if "AttributeError" in response:
314 main.log.error("Wrong action: " + response)
315 return main.FALSE
316 if "Invalid value" in response:
317 main.log.error("Invalid action value: " + response)
318 return main.FALSE
319 if "Action parameter value must be a string" in response:
320 main.log.error(
321 "Action parameter value must be a string: " + response)
322 return main.FALSE
323 if "table" in response and "does not exist" in response:
324 main.log.error("Unknown table: " + response)
325 return main.FALSE
326 if "not a valid match field name" in response:
327 main.log.error("Invalid match field name: " + response)
328 return main.FALSE
329 if "is not a valid" in response:
330 main.log.error("Invalid match field: " + response)
331 return main.FALSE
332 if "Traceback" in response:
333 main.log.error("Error in creating the table entry: " + response)
334 return main.FALSE
335 return main.TRUE
336 except pexpect.TIMEOUT:
337 main.log.exception(self.name + ": Command timed out")
338 return main.FALSE
339 except pexpect.EOF:
340 main.log.exception(self.name + ": connection closed.")
341 main.cleanAndExit()
342 except Exception:
343 main.log.exception(self.name + ": Uncaught exception!")
344 main.cleanAndExit()
345
Daniele Morob8404e82022-02-25 00:17:28 +0100346 def buildP4RtMeterEntry(self, meterName, index, cir=None, cburst=None, pir=None,
347 pburst=None):
348 # TODO: improve error checking
349 try:
350 main.log.debug(
351 "%s: Building P4RT Meter Entry (meter=%s, index=%d, cir=%s, "
352 "cburst=%s, pir=%s, pburst=%s)" % (
353 self.name, meterName, index, str(cir), str(cburst), str(pir), str(pburst)
354 )
355 )
356 cmd = 'me = meter_entry["%s"]; ' % meterName
357 cmd += 'me.index=%d; ' % index
358 if cir is not None:
359 cmd += 'me.cir=%d; ' % cir
360 if cburst is not None:
361 cmd += 'me.cburst=%d; ' % cburst
362 if pir is not None:
363 cmd += 'me.pir=%d; ' % pir
364 if pburst is not None:
365 cmd += 'me.pburst=%d; ' % pburst
366
367 response = self.__clearSendAndExpect(cmd)
368 if "meter" in response and "does not exist" in response:
369 main.log.error("Unknown meter: " + response)
370 return main.FALSE
371 if "UNIMPLEMENTED" in response:
372 main.log.error("Error in creating the meter entry: " + response)
373 return main.FALSE
374 if "Traceback" in response:
375 main.log.error("Error in creating the meter entry: " + response)
376 return main.FALSE
377 return main.TRUE
378 except pexpect.TIMEOUT:
379 main.log.exception(self.name + ": Command timed out")
380 return main.FALSE
381 except pexpect.EOF:
382 main.log.exception(self.name + ": connection closed.")
383 main.cleanAndExit()
384 except Exception:
385 main.log.exception(self.name + ": Uncaught exception!")
386 main.cleanAndExit()
387
Daniele Moro954e2282021-09-22 17:32:03 +0200388 def readNumberTableEntries(self, tableName):
389 """
390 Read table entries and return the number of entries present in a table.
391
392 :param tableName: Name of table to read from
393 :return: Number of entries,
394 """
395 try:
Daniele Morob8404e82022-02-25 00:17:28 +0100396 main.log.debug(
397 self.name + ": Reading table entries from " + tableName)
Daniele Moro954e2282021-09-22 17:32:03 +0200398 cmd = 'table_entry["%s"].read(lambda te: print(te))' % tableName
399 response = self.__clearSendAndExpect(cmd, clearBufferAfter=True)
400 # Every table entries starts with "table_id: [P4RT obj ID] ("[tableName]")"
401 return response.count("table_id")
402 except pexpect.TIMEOUT:
403 main.log.exception(self.name + ": Command timed out")
404 return main.FALSE
405 except pexpect.EOF:
406 main.log.exception(self.name + ": connection closed.")
407 main.cleanAndExit()
408 except Exception:
409 main.log.exception(self.name + ": Uncaught exception!")
410 main.cleanAndExit()
411
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200412 def disconnect(self):
413 """
414 Called at the end of the test to stop the p4rt CLI component and
415 disconnect the handle.
416 """
417 response = main.TRUE
418 try:
419 if self.handle:
420 self.handle.sendline("")
421 i = self.handle.expect([self.p4rtShPrompt, pexpect.TIMEOUT],
422 timeout=2)
423 if i != 1:
424 # If the p4rtShell is still connected make sure to
425 # disconnect it before
426 self.stopP4RtClient()
427 i = self.handle.expect([self.prompt, pexpect.TIMEOUT],
428 timeout=2)
429 if i == 1:
430 main.log.warn(
431 self.name + ": timeout when waiting for response")
432 main.log.warn(
433 self.name + ": response: " + str(self.handle.before))
434 self.handle.sendline("exit")
435 i = self.handle.expect(["closed", pexpect.TIMEOUT], timeout=2)
436 if i == 1:
437 main.log.warn(
438 self.name + ": timeout when waiting for response")
439 main.log.warn(
440 self.name + ": response: " + str(self.handle.before))
441 return main.TRUE
442 except TypeError:
443 main.log.exception(self.name + ": Object not as expected")
444 response = main.FALSE
445 except pexpect.EOF:
446 main.log.error(self.name + ": EOF exception found")
447 main.log.error(self.name + ": " + self.handle.before)
448 except ValueError:
449 main.log.exception("Exception in disconnect of " + self.name)
450 response = main.TRUE
451 except Exception:
452 main.log.exception(self.name + ": Connection failed to the host")
453 response = main.FALSE
454 return response
455
Daniele Moro954e2282021-09-22 17:32:03 +0200456 def __clearSendAndExpect(self, cmd, clearBufferAfter=False, debug=False):
457 self.clearBuffer(debug)
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200458 self.handle.sendline(cmd)
459 self.handle.expect(self.p4rtShPrompt)
Daniele Moro954e2282021-09-22 17:32:03 +0200460 if clearBufferAfter:
461 return self.clearBuffer()
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200462 return self.handle.before
463
464 def clearBuffer(self, debug=False):
465 """
466 Keep reading from buffer until it's empty
467 """
468 i = 0
469 response = ''
470 while True:
471 try:
472 i += 1
473 # clear buffer
474 if debug:
475 main.log.warn("%s expect loop iteration" % i)
Jon Hall376f5032022-01-27 16:16:29 -0800476 self.handle.expect(self.p4rtShPrompt, timeout=2)
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200477 response += self.cleanOutput(self.handle.before, debug)
478 except pexpect.TIMEOUT:
479 return response