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) |
| 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 Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame^] | 296 | 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 Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 319 | 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 Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame^] | 363 | def __clearSendAndExpect(self, cmd, clearBufferAfter=False, debug=False): |
| 364 | self.clearBuffer(debug) |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 365 | self.handle.sendline(cmd) |
| 366 | self.handle.expect(self.p4rtShPrompt) |
Daniele Moro | 954e228 | 2021-09-22 17:32:03 +0200 | [diff] [blame^] | 367 | if clearBufferAfter: |
| 368 | return self.clearBuffer() |
Daniele Moro | c9b4afe | 2021-08-26 18:07:01 +0200 | [diff] [blame] | 369 | 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 |