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