blob: e2ef0c81972f2216c2ca47e595e75a1485e8488b [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)
111 startP4RtShLine = "python3 -m p4runtime_sh --grpc-addr " + grpcAddr + \
112 " --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)
122 self.preDisconnect = self.stopP4RtClient
123 except pexpect.TIMEOUT:
124 main.log.exception(self.name + ": Command timed out")
125 return main.FALSE
126 except pexpect.EOF:
127 main.log.exception(self.name + ": connection closed.")
128 main.cleanAndExit()
129 except Exception:
130 main.log.exception(self.name + ": Uncaught exception!")
131 main.cleanAndExit()
132
133 def stopP4RtClient(self):
134 """
135 Exit the P4Runtime shell CLI
136 """
137 try:
138 main.log.debug(self.name + ": Stopping P4Runtime Shell CLI")
139 self.handle.sendline("exit")
140 self.handle.expect(self.prompt)
141 return main.TRUE
142 except pexpect.TIMEOUT:
143 main.log.exception(self.name + ": Command timed out")
144 return main.FALSE
145 except pexpect.EOF:
146 main.log.exception(self.name + ": connection closed.")
147 main.cleanAndExit()
148 except Exception:
149 main.log.exception(self.name + ": Uncaught exception!")
150 main.cleanAndExit()
151
152 def pushTableEntry(self, tableEntry=None, debug=True):
153 """
154 Push a table entry with either the given table entry or use the saved
155 table entry in the variable 'te'.
156
157 Example of a valid tableEntry string:
158 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
159
160 :param tableEntry: the string table entry, if None it uses the table
161 entry saved in the 'te' variable
162 :param debug: True to enable debug logging, False otherwise
163 :return: main.TRUE or main.FALSE on error
164 """
165 try:
166 main.log.debug(self.name + ": Pushing Table Entry")
167 if debug:
168 self.handle.sendline("te")
169 self.handle.expect(self.p4rtShPrompt)
170 pushCmd = ""
171 if tableEntry:
172 pushCmd = tableEntry + ";"
173 pushCmd += "te.insert()"
174 response = self.__clearSendAndExpect(pushCmd)
175 if "Traceback" in response or "Error" in response:
176 # TODO: other possibile errors?
177 # NameError...
178 main.log.error(
179 self.name + ": Error in pushing table entry: " + response)
180 return main.FALSE
181 return main.TRUE
182 except pexpect.TIMEOUT:
183 main.log.exception(self.name + ": Command timed out")
184 return main.FALSE
185 except pexpect.EOF:
186 main.log.exception(self.name + ": connection closed.")
187 main.cleanAndExit()
188 except Exception:
189 main.log.exception(self.name + ": Uncaught exception!")
190 main.cleanAndExit()
191
192 def deleteTableEntry(self, tableEntry=None, debug=True):
193 """
194 Deletes a table entry with either the given table entry or use the saved
195 table entry in the variable 'te'.
196
197 Example of a valid tableEntry string:
198 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
199
200 :param tableEntry: the string table entry, if None it uses the table
201 entry saved in the 'te' variable
202 :param debug: True to enable debug logging, False otherwise
203 :return: main.TRUE or main.FALSE on error
204 """
205 try:
206 main.log.debug(self.name + ": Deleting Table Entry")
207 if debug:
208 self.__clearSendAndExpect("te")
209 pushCmd = ""
210 if tableEntry:
211 pushCmd = tableEntry + ";"
212 pushCmd += "te.delete()"
213 response = self.__clearSendAndExpect(pushCmd)
214 main.log.debug(
215 self.name + ": Delete table entry response: {}".format(
216 response))
217 if "Traceback" in response or "Error" in response:
218 # TODO: other possibile errors?
219 # NameError...
220 main.log.error(
221 self.name + ": Error in deleting table entry: " + response)
222 return main.FALSE
223 return main.TRUE
224 except pexpect.TIMEOUT:
225 main.log.exception(self.name + ": Command timed out")
226 return main.FALSE
227 except pexpect.EOF:
228 main.log.exception(self.name + ": connection closed.")
229 main.cleanAndExit()
230 except Exception:
231 main.log.exception(self.name + ": Uncaught exception!")
232 main.cleanAndExit()
233
234 def buildP4RtTableEntry(self, tableName, actionName, actionParams={},
235 matchFields={}):
236 """
237 Build a Table Entry
238 :param tableName: The name of table
239 :param actionName: The name of the action
240 :param actionParams: A dictionary containing name and values for the action parameters
241 :param matchFields: A dictionary containing name and values for the match fields
242 :return: main.TRUE or main.FALSE on error
243 """
244 # TODO: improve error checking when creating the table entry, add
245 # params, and match fields.
246 try:
247 main.log.debug(self.name + ": Building P4RT Table Entry")
248 cmd = 'te = table_entry["%s"](action="%s"); ' % (
249 tableName, actionName)
250
251 # Action Parameters
252 for name, value in actionParams.items():
253 cmd += 'te.action["%s"]="%s";' % (name, str(value))
254
255 # Match Fields
256 for name, value in matchFields.items():
257 cmd += 'te.match["%s"]="%s";' % (name, str(value))
258
259 response = self.__clearSendAndExpect(cmd)
260 if "Unknown action" in response:
261 main.log.error("Unknown action: " + response)
262 return main.FALSE
263 if "AttributeError" in response:
264 main.log.error("Wrong action: " + response)
265 return main.FALSE
266 if "Invalid value" in response:
267 main.log.error("Invalid action value: " + response)
268 return main.FALSE
269 if "Action parameter value must be a string" in response:
270 main.log.error(
271 "Action parameter value must be a string: " + response)
272 return main.FALSE
273 if "table" in response and "does not exist" in response:
274 main.log.error("Unknown table: " + response)
275 return main.FALSE
276 if "not a valid match field name" in response:
277 main.log.error("Invalid match field name: " + response)
278 return main.FALSE
279 if "is not a valid" in response:
280 main.log.error("Invalid match field: " + response)
281 return main.FALSE
282 if "Traceback" in response:
283 main.log.error("Error in creating the table entry: " + response)
284 return main.FALSE
285 return main.TRUE
286 except pexpect.TIMEOUT:
287 main.log.exception(self.name + ": Command timed out")
288 return main.FALSE
289 except pexpect.EOF:
290 main.log.exception(self.name + ": connection closed.")
291 main.cleanAndExit()
292 except Exception:
293 main.log.exception(self.name + ": Uncaught exception!")
294 main.cleanAndExit()
295
Daniele Moro954e2282021-09-22 17:32:03 +0200296 def readNumberTableEntries(self, tableName):
297 """
298 Read table entries and return the number of entries present in a table.
299
300 :param tableName: Name of table to read from
301 :return: Number of entries,
302 """
303 try:
304 main.log.debug(self.name + ": Reading table entries from " + tableName)
305 cmd = 'table_entry["%s"].read(lambda te: print(te))' % tableName
306 response = self.__clearSendAndExpect(cmd, clearBufferAfter=True)
307 # Every table entries starts with "table_id: [P4RT obj ID] ("[tableName]")"
308 return response.count("table_id")
309 except pexpect.TIMEOUT:
310 main.log.exception(self.name + ": Command timed out")
311 return main.FALSE
312 except pexpect.EOF:
313 main.log.exception(self.name + ": connection closed.")
314 main.cleanAndExit()
315 except Exception:
316 main.log.exception(self.name + ": Uncaught exception!")
317 main.cleanAndExit()
318
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200319 def disconnect(self):
320 """
321 Called at the end of the test to stop the p4rt CLI component and
322 disconnect the handle.
323 """
324 response = main.TRUE
325 try:
326 if self.handle:
327 self.handle.sendline("")
328 i = self.handle.expect([self.p4rtShPrompt, pexpect.TIMEOUT],
329 timeout=2)
330 if i != 1:
331 # If the p4rtShell is still connected make sure to
332 # disconnect it before
333 self.stopP4RtClient()
334 i = self.handle.expect([self.prompt, pexpect.TIMEOUT],
335 timeout=2)
336 if i == 1:
337 main.log.warn(
338 self.name + ": timeout when waiting for response")
339 main.log.warn(
340 self.name + ": response: " + str(self.handle.before))
341 self.handle.sendline("exit")
342 i = self.handle.expect(["closed", pexpect.TIMEOUT], timeout=2)
343 if i == 1:
344 main.log.warn(
345 self.name + ": timeout when waiting for response")
346 main.log.warn(
347 self.name + ": response: " + str(self.handle.before))
348 return main.TRUE
349 except TypeError:
350 main.log.exception(self.name + ": Object not as expected")
351 response = main.FALSE
352 except pexpect.EOF:
353 main.log.error(self.name + ": EOF exception found")
354 main.log.error(self.name + ": " + self.handle.before)
355 except ValueError:
356 main.log.exception("Exception in disconnect of " + self.name)
357 response = main.TRUE
358 except Exception:
359 main.log.exception(self.name + ": Connection failed to the host")
360 response = main.FALSE
361 return response
362
Daniele Moro954e2282021-09-22 17:32:03 +0200363 def __clearSendAndExpect(self, cmd, clearBufferAfter=False, debug=False):
364 self.clearBuffer(debug)
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200365 self.handle.sendline(cmd)
366 self.handle.expect(self.p4rtShPrompt)
Daniele Moro954e2282021-09-22 17:32:03 +0200367 if clearBufferAfter:
368 return self.clearBuffer()
Daniele Moroc9b4afe2021-08-26 18:07:01 +0200369 return self.handle.before
370
371 def clearBuffer(self, debug=False):
372 """
373 Keep reading from buffer until it's empty
374 """
375 i = 0
376 response = ''
377 while True:
378 try:
379 i += 1
380 # clear buffer
381 if debug:
382 main.log.warn("%s expect loop iteration" % i)
383 self.handle.expect(self.p4rtShPrompt, timeout=5)
384 response += self.cleanOutput(self.handle.before, debug)
385 except pexpect.TIMEOUT:
386 return response