blob: 6728cbaf3efeba44dfedcfb7e0ecb85788c4b063 [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
296 def disconnect(self):
297 """
298 Called at the end of the test to stop the p4rt CLI component and
299 disconnect the handle.
300 """
301 response = main.TRUE
302 try:
303 if self.handle:
304 self.handle.sendline("")
305 i = self.handle.expect([self.p4rtShPrompt, pexpect.TIMEOUT],
306 timeout=2)
307 if i != 1:
308 # If the p4rtShell is still connected make sure to
309 # disconnect it before
310 self.stopP4RtClient()
311 i = self.handle.expect([self.prompt, pexpect.TIMEOUT],
312 timeout=2)
313 if i == 1:
314 main.log.warn(
315 self.name + ": timeout when waiting for response")
316 main.log.warn(
317 self.name + ": response: " + str(self.handle.before))
318 self.handle.sendline("exit")
319 i = self.handle.expect(["closed", pexpect.TIMEOUT], timeout=2)
320 if i == 1:
321 main.log.warn(
322 self.name + ": timeout when waiting for response")
323 main.log.warn(
324 self.name + ": response: " + str(self.handle.before))
325 return main.TRUE
326 except TypeError:
327 main.log.exception(self.name + ": Object not as expected")
328 response = main.FALSE
329 except pexpect.EOF:
330 main.log.error(self.name + ": EOF exception found")
331 main.log.error(self.name + ": " + self.handle.before)
332 except ValueError:
333 main.log.exception("Exception in disconnect of " + self.name)
334 response = main.TRUE
335 except Exception:
336 main.log.exception(self.name + ": Connection failed to the host")
337 response = main.FALSE
338 return response
339
340 def __clearSendAndExpect(self, cmd):
341 self.clearBuffer()
342 self.handle.sendline(cmd)
343 self.handle.expect(self.p4rtShPrompt)
344 return self.handle.before
345
346 def clearBuffer(self, debug=False):
347 """
348 Keep reading from buffer until it's empty
349 """
350 i = 0
351 response = ''
352 while True:
353 try:
354 i += 1
355 # clear buffer
356 if debug:
357 main.log.warn("%s expect loop iteration" % i)
358 self.handle.expect(self.p4rtShPrompt, timeout=5)
359 response += self.cleanOutput(self.handle.before, debug)
360 except pexpect.TIMEOUT:
361 return response