blob: b44c2cb8d7b4af5d5fa5289616ffc997eaebc6ee [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")
128 return main.FALSE
129 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
237 def buildP4RtTableEntry(self, tableName, actionName, actionParams={},
Daniele Morobef0c7e2022-02-16 17:47:13 -0800238 matchFields={}, priority=0):
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200239 """
240 Build a Table Entry
241 :param tableName: The name of table
242 :param actionName: The name of the action
243 :param actionParams: A dictionary containing name and values for the action parameters
244 :param matchFields: A dictionary containing name and values for the match fields
Daniele Morobef0c7e2022-02-16 17:47:13 -0800245 :param priority: for ternary match entries
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200246 :return: main.TRUE or main.FALSE on error
247 """
248 # TODO: improve error checking when creating the table entry, add
249 # params, and match fields.
250 try:
Daniele Morobef0c7e2022-02-16 17:47:13 -0800251 main.log.debug("%s: Building P4RT Table Entry "
252 "(table=%s, match=%s, priority=%s, action=%s, params=%s)" % (
253 self.name, tableName, matchFields, priority, actionName, actionParams))
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200254 cmd = 'te = table_entry["%s"](action="%s"); ' % (
255 tableName, actionName)
256
257 # Action Parameters
258 for name, value in actionParams.items():
259 cmd += 'te.action["%s"]="%s";' % (name, str(value))
260
261 # Match Fields
262 for name, value in matchFields.items():
263 cmd += 'te.match["%s"]="%s";' % (name, str(value))
264
Daniele Morobef0c7e2022-02-16 17:47:13 -0800265 if priority:
266 cmd += 'te.priority=%s;' % priority
267
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200268 response = self.__clearSendAndExpect(cmd)
269 if "Unknown action" in response:
270 main.log.error("Unknown action: " + response)
271 return main.FALSE
272 if "AttributeError" in response:
273 main.log.error("Wrong action: " + response)
274 return main.FALSE
275 if "Invalid value" in response:
276 main.log.error("Invalid action value: " + response)
277 return main.FALSE
278 if "Action parameter value must be a string" in response:
279 main.log.error(
280 "Action parameter value must be a string: " + response)
281 return main.FALSE
282 if "table" in response and "does not exist" in response:
283 main.log.error("Unknown table: " + response)
284 return main.FALSE
285 if "not a valid match field name" in response:
286 main.log.error("Invalid match field name: " + response)
287 return main.FALSE
288 if "is not a valid" in response:
289 main.log.error("Invalid match field: " + response)
290 return main.FALSE
291 if "Traceback" in response:
292 main.log.error("Error in creating the table entry: " + response)
293 return main.FALSE
294 return main.TRUE
295 except pexpect.TIMEOUT:
296 main.log.exception(self.name + ": Command timed out")
297 return main.FALSE
298 except pexpect.EOF:
299 main.log.exception(self.name + ": connection closed.")
300 main.cleanAndExit()
301 except Exception:
302 main.log.exception(self.name + ": Uncaught exception!")
303 main.cleanAndExit()
304
Daniele Moro954e2282021-09-22 17:32:03 +0200305 def readNumberTableEntries(self, tableName):
306 """
307 Read table entries and return the number of entries present in a table.
308
309 :param tableName: Name of table to read from
310 :return: Number of entries,
311 """
312 try:
313 main.log.debug(self.name + ": Reading table entries from " + tableName)
314 cmd = 'table_entry["%s"].read(lambda te: print(te))' % tableName
315 response = self.__clearSendAndExpect(cmd, clearBufferAfter=True)
316 # Every table entries starts with "table_id: [P4RT obj ID] ("[tableName]")"
317 return response.count("table_id")
318 except pexpect.TIMEOUT:
319 main.log.exception(self.name + ": Command timed out")
320 return main.FALSE
321 except pexpect.EOF:
322 main.log.exception(self.name + ": connection closed.")
323 main.cleanAndExit()
324 except Exception:
325 main.log.exception(self.name + ": Uncaught exception!")
326 main.cleanAndExit()
327
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200328 def disconnect(self):
329 """
330 Called at the end of the test to stop the p4rt CLI component and
331 disconnect the handle.
332 """
333 response = main.TRUE
334 try:
335 if self.handle:
336 self.handle.sendline("")
337 i = self.handle.expect([self.p4rtShPrompt, pexpect.TIMEOUT],
338 timeout=2)
339 if i != 1:
340 # If the p4rtShell is still connected make sure to
341 # disconnect it before
342 self.stopP4RtClient()
343 i = self.handle.expect([self.prompt, pexpect.TIMEOUT],
344 timeout=2)
345 if i == 1:
346 main.log.warn(
347 self.name + ": timeout when waiting for response")
348 main.log.warn(
349 self.name + ": response: " + str(self.handle.before))
350 self.handle.sendline("exit")
351 i = self.handle.expect(["closed", pexpect.TIMEOUT], timeout=2)
352 if i == 1:
353 main.log.warn(
354 self.name + ": timeout when waiting for response")
355 main.log.warn(
356 self.name + ": response: " + str(self.handle.before))
357 return main.TRUE
358 except TypeError:
359 main.log.exception(self.name + ": Object not as expected")
360 response = main.FALSE
361 except pexpect.EOF:
362 main.log.error(self.name + ": EOF exception found")
363 main.log.error(self.name + ": " + self.handle.before)
364 except ValueError:
365 main.log.exception("Exception in disconnect of " + self.name)
366 response = main.TRUE
367 except Exception:
368 main.log.exception(self.name + ": Connection failed to the host")
369 response = main.FALSE
370 return response
371
Daniele Moro954e2282021-09-22 17:32:03 +0200372 def __clearSendAndExpect(self, cmd, clearBufferAfter=False, debug=False):
373 self.clearBuffer(debug)
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200374 self.handle.sendline(cmd)
375 self.handle.expect(self.p4rtShPrompt)
Daniele Moro954e2282021-09-22 17:32:03 +0200376 if clearBufferAfter:
377 return self.clearBuffer()
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200378 return self.handle.before
379
380 def clearBuffer(self, debug=False):
381 """
382 Keep reading from buffer until it's empty
383 """
384 i = 0
385 response = ''
386 while True:
387 try:
388 i += 1
389 # clear buffer
390 if debug:
391 main.log.warn("%s expect loop iteration" % i)
Jon Hall376f5032022-01-27 16:16:29 -0800392 self.handle.expect(self.p4rtShPrompt, timeout=2)
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200393 response += self.cleanOutput(self.handle.before, debug)
394 except pexpect.TIMEOUT:
395 return response