Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 1 | """ |
| 2 | Copyright 2021 Open Networking Foundation (ONF) |
| 3 | |
| 4 | Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>, |
| 5 | the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>, |
| 6 | or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg> |
| 7 | |
| 8 | """ |
| 9 | |
| 10 | import pexpect |
| 11 | import os |
| 12 | from drivers.common.clidriver import CLI |
| 13 | |
| 14 | |
| 15 | class 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 Moro | 54fd002 | 2021-10-15 17:40:27 +0200 | [diff] [blame] | 111 | startP4RtShLine = "python3 -m p4runtime_sh -v --grpc-addr " + grpcAddr + \ |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 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) |
Daniele Moro | 54fd002 | 2021-10-15 17:40:27 +0200 | [diff] [blame] | 122 | if "CRITICAL" in response: |
| 123 | main.log.exception(self.name + ": Connection error.") |
| 124 | main.cleanAndExit() |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 125 | 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 Moro | bef0c7e | 2022-02-16 17:47:13 -0800 | [diff] [blame] | 238 | matchFields={}, priority=0): |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 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 |
Daniele Moro | bef0c7e | 2022-02-16 17:47:13 -0800 | [diff] [blame] | 245 | :param priority: for ternary match entries |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 246 | :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 Moro | bef0c7e | 2022-02-16 17:47:13 -0800 | [diff] [blame] | 251 | 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 Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 254 | 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 Moro | bef0c7e | 2022-02-16 17:47:13 -0800 | [diff] [blame] | 265 | if priority: |
| 266 | cmd += 'te.priority=%s;' % priority |
| 267 | |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 268 | 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 Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame] | 305 | 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 Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 328 | 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 Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame] | 372 | def __clearSendAndExpect(self, cmd, clearBufferAfter=False, debug=False): |
| 373 | self.clearBuffer(debug) |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 374 | self.handle.sendline(cmd) |
| 375 | self.handle.expect(self.p4rtShPrompt) |
Daniele Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame] | 376 | if clearBufferAfter: |
| 377 | return self.clearBuffer() |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 378 | 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 Hall | 376f503 | 2022-01-27 16:16:29 -0800 | [diff] [blame] | 392 | self.handle.expect(self.p4rtShPrompt, timeout=2) |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 393 | response += self.cleanOutput(self.handle.before, debug) |
| 394 | except pexpect.TIMEOUT: |
| 395 | return response |