|
13 | 13 | import re |
14 | 14 | import inspect |
15 | 15 | import ipaddress |
| 16 | +import copy |
16 | 17 | from multiprocessing.pool import ThreadPool |
17 | 18 | from datetime import datetime |
18 | 19 |
|
@@ -1302,6 +1303,248 @@ def execute (self, cmd) : |
1302 | 1303 | eval(cmd) |
1303 | 1304 |
|
1304 | 1305 |
|
| 1306 | +class SonicAsic(object): |
| 1307 | + """ This class represents an ASIC on a SONiC host. This class implements wrapper methods for ASIC/namespace related operations. |
| 1308 | + The purpose is to hide the complexity of handling ASIC/namespace specific details. |
| 1309 | + For example, passing asic_id, namespace, instance_id etc. to ansible module to deal with namespaces. |
| 1310 | + """ |
| 1311 | + def __init__(self, sonichost, asic_index): |
| 1312 | + """ Initializing a ASIC on a SONiC host. |
| 1313 | +
|
| 1314 | + Args: |
| 1315 | + sonichost : SonicHost object to which this asic belongs |
| 1316 | + asic_index: ASIC / namespace id for this asic. |
| 1317 | + """ |
| 1318 | + self.sonichost = sonichost |
| 1319 | + self.asic_index = asic_index |
| 1320 | + |
| 1321 | + |
| 1322 | + def bgp_facts(self, *module_args, **complex_args): |
| 1323 | + """ Wrapper method for bgp_facts ansible module. |
| 1324 | + If number of asics in SonicHost are more than 1, then add 'instance_id' param for this Asic |
| 1325 | +
|
| 1326 | + Args: |
| 1327 | + module_args: other ansible module args passed from the caller |
| 1328 | + complex_args: other ansible keyword args |
| 1329 | +
|
| 1330 | + Returns: |
| 1331 | + if SonicHost has only 1 asic, then return the bgp_facts for the global namespace, else bgp_facts for the bgp instance for my asic_index. |
| 1332 | + """ |
| 1333 | + if self.sonichost.facts['num_asic'] != 1: |
| 1334 | + complex_args['instance_id'] = self.asic_index |
| 1335 | + return self.sonichost.bgp_facts(*module_args, **complex_args) |
| 1336 | + |
| 1337 | + |
| 1338 | +class MultiAsicSonicHost(object): |
| 1339 | + """ This class represents a Multi-asic SonicHost It has two attributes: |
| 1340 | + sonic_host: a SonicHost instance. This object is for interacting with the SONiC host through pytest_ansible. |
| 1341 | + asics: a list of SonicAsic instances. |
| 1342 | +
|
| 1343 | + The 'duthost' fixture will return an instance of a MultiAsicSonicHost. |
| 1344 | + So, even a single asic pizza box is represented as a MultiAsicSonicHost with 1 SonicAsic. |
| 1345 | + """ |
| 1346 | + |
| 1347 | + def __init__(self, ansible_adhoc, hostname): |
| 1348 | + """ Initializing a MultiAsicSonicHost. |
| 1349 | +
|
| 1350 | + Args: |
| 1351 | + ansible_adhoc : The pytest-ansible fixture |
| 1352 | + hostname: Name of the host in the ansible inventory |
| 1353 | + """ |
| 1354 | + self.sonichost = SonicHost(ansible_adhoc, hostname) |
| 1355 | + self.asics = [SonicAsic(self.sonichost, asic_index) for asic_index in range(self.sonichost.facts["num_asic"])] |
| 1356 | + |
| 1357 | + def _run_on_asics(self, *module_args, **complex_args): |
| 1358 | + """ Run an asible module on asics based on 'asic_index' keyword in complex_args |
| 1359 | +
|
| 1360 | + Args: |
| 1361 | + module_args: other ansible module args passed from the caller |
| 1362 | + complex_args: other ansible keyword args |
| 1363 | +
|
| 1364 | + Raises: |
| 1365 | + ValueError: if asic_index is specified and it is neither an int or string 'all'. |
| 1366 | + ValueError: if asic_index is specified and is an int, but greater than number of asics in the SonicHost |
| 1367 | +
|
| 1368 | + Returns: |
| 1369 | + if asic_index is not specified, then we return the output of the ansible module on global namespace (using SonicHost) |
| 1370 | + else |
| 1371 | + if asic_index is an int, the output of the ansible module on that asic namespace |
| 1372 | + - for single asic SonicHost this would still be the same as the ansible module on the global namespace |
| 1373 | + else if asic_index is string 'all', then a list of ansible module output for all the asics on the SonicHost |
| 1374 | + - for single asic, this would be a list of size 1. |
| 1375 | + """ |
| 1376 | + if "asic_index" not in complex_args: |
| 1377 | + # Default ASIC/namespace |
| 1378 | + return getattr(self.sonichost, self.multi_asic_attr)(*module_args, **complex_args) |
| 1379 | + else: |
| 1380 | + asic_complex_args = copy.deepcopy(complex_args) |
| 1381 | + asic_index = asic_complex_args.pop("asic_index") |
| 1382 | + if type(asic_index) == int: |
| 1383 | + # Specific ASIC/namespace |
| 1384 | + if self.sonichost.facts['num_asic'] == 1: |
| 1385 | + if asic_index != 0: |
| 1386 | + raise ValueError("Trying to run module '{}' against asic_index '{}' on a single asic dut '{}'".format(self.multi_asic_attr, asic_index, self.sonichost.hostname)) |
| 1387 | + return getattr(self.asics[asic_index], self.multi_asic_attr)(*module_args, **asic_complex_args) |
| 1388 | + elif type(asic_index) == str and asic_index.lower() == "all": |
| 1389 | + # All ASICs/namespace |
| 1390 | + return [getattr(asic, self.multi_asic_attr)(*module_args, **asic_complex_args) for asic in self.asics] |
| 1391 | + else: |
| 1392 | + raise ValueError("Argument 'asic_index' must be an int or string 'all'.") |
| 1393 | + |
| 1394 | + def __getattr__(self, attr): |
| 1395 | + """ To support calling an ansible module on a MultiAsicSonicHost. |
| 1396 | +
|
| 1397 | + Args: |
| 1398 | + attr: attribute to get |
| 1399 | +
|
| 1400 | + Returns: |
| 1401 | + if attr doesn't start with '_' and is a method of SonicAsic, attr will be ansible module that has dependency on ASIC, |
| 1402 | + return the output of the ansible module on asics requested - using _run_on_asics method. |
| 1403 | + else |
| 1404 | + return the attribute from SonicHost. |
| 1405 | + """ |
| 1406 | + sonic_asic_attr = getattr(SonicAsic, attr, None) |
| 1407 | + if not attr.startswith("_") and sonic_asic_attr and callable(sonic_asic_attr): |
| 1408 | + self.multi_asic_attr = attr |
| 1409 | + return self._run_on_asics |
| 1410 | + else: |
| 1411 | + return getattr(self.sonichost, attr) # For backward compatibility |
| 1412 | + |
| 1413 | + |
| 1414 | +class DutHosts(object): |
| 1415 | + """ Represents all the DUTs (nodes) in a testbed. class has 3 important attributes: |
| 1416 | + nodes: List of all the MultiAsicSonicHost instances for all the SONiC nodes (or cards for chassis) in a multi-dut testbed |
| 1417 | + frontend_nodes: subset of nodes and holds list of MultiAsicSonicHost instances for DUTs with front-panel ports (like linecards in chassis |
| 1418 | + supervisor_nodes: subset of nodes and holds list of MultiAsicSonicHost instances for supervisor cards. |
| 1419 | + """ |
| 1420 | + class _Nodes(list): |
| 1421 | + """ Internal class representing a list of MultiAsicSonicHosts """ |
| 1422 | + def _run_on_nodes(self, *module_args, **complex_args): |
| 1423 | + """ Delegate the call to each of the nodes, return the results in a dict.""" |
| 1424 | + return {node.hostname: getattr(node, self.attr)(*module_args, **complex_args) for node in self} |
| 1425 | + |
| 1426 | + def __getattr__(self, attr): |
| 1427 | + """ To support calling ansible modules on a list of MultiAsicSonicHost |
| 1428 | + Args: |
| 1429 | + attr: attribute to get |
| 1430 | +
|
| 1431 | + Returns: |
| 1432 | + a dictionary with key being the MultiAsicSonicHost's hostname, and value being the output of ansible module |
| 1433 | + on that MultiAsicSonicHost |
| 1434 | + """ |
| 1435 | + self.attr = attr |
| 1436 | + return self._run_on_nodes |
| 1437 | + |
| 1438 | + def __eq__(self, o): |
| 1439 | + """ To support eq operator on the DUTs (nodes) in the testbed """ |
| 1440 | + return list.__eq__(o) |
| 1441 | + |
| 1442 | + def __ne__(self, o): |
| 1443 | + """ To support ne operator on the DUTs (nodes) in the testbed """ |
| 1444 | + return list.__ne__(o) |
| 1445 | + |
| 1446 | + def __hash__(self): |
| 1447 | + """ To support hash operator on the DUTs (nodes) in the testbed """ |
| 1448 | + return list.__hash__() |
| 1449 | + |
| 1450 | + def __init__(self, ansible_adhoc, tbinfo): |
| 1451 | + """ Initialize a multi-dut testbed with all the DUT's defined in testbed info. |
| 1452 | +
|
| 1453 | + Args: |
| 1454 | + ansible_adhoc: The pytest-ansible fixture |
| 1455 | + tbinfo - Testbed info whose "duts" holds the hostnames for the DUT's in the multi-dut testbed. |
| 1456 | +
|
| 1457 | + """ |
| 1458 | + # TODO: Initialize the nodes in parallel using multi-threads? |
| 1459 | + self.nodes = self._Nodes([MultiAsicSonicHost(ansible_adhoc, hostname) for hostname in tbinfo["duts"]]) |
| 1460 | + self.supervisor_nodes = self._Nodes([node for node in self.nodes if self._is_supervisor_node(node)]) |
| 1461 | + self.frontend_nodes = self._Nodes([node for node in self.nodes if self._is_frontend_node(node)]) |
| 1462 | + |
| 1463 | + def __getitem__(self, index): |
| 1464 | + """To support operations like duthosts[0] and duthost['sonic1_hostname'] |
| 1465 | +
|
| 1466 | + Args: |
| 1467 | + index (int or string): Index or hostname of a duthost. |
| 1468 | +
|
| 1469 | + Raises: |
| 1470 | + KeyError: Raised when duthost with supplied hostname is not found. |
| 1471 | + IndexError: Raised when duthost with supplied index is not found. |
| 1472 | +
|
| 1473 | + Returns: |
| 1474 | + [MultiAsicSonicHost]: Returns the specified duthost in duthosts. It is an instance of MultiAsicSonicHost. |
| 1475 | + """ |
| 1476 | + if type(index) == int: |
| 1477 | + return self.nodes[index] |
| 1478 | + elif type(index) == str: |
| 1479 | + for node in self.nodes: |
| 1480 | + if node.hostname == index: |
| 1481 | + return node |
| 1482 | + raise KeyError("No node has hostname '{}'".format(index)) |
| 1483 | + else: |
| 1484 | + raise IndexError("Bad index '{}'".format(index)) |
| 1485 | + |
| 1486 | + # Below method are to support treating an instance of DutHosts as a list |
| 1487 | + def __iter__(self): |
| 1488 | + """ To support iteration over all the DUTs (nodes) in the testbed""" |
| 1489 | + return iter(self.nodes) |
| 1490 | + |
| 1491 | + def __len__(self): |
| 1492 | + """ To support length of the number of DUTs (nodes) in the testbed """ |
| 1493 | + return len(self.nodes) |
| 1494 | + |
| 1495 | + def __eq__(self, o): |
| 1496 | + """ To support eq operator on the DUTs (nodes) in the testbed """ |
| 1497 | + return self.nodes.__eq__(o) |
| 1498 | + |
| 1499 | + def __ne__(self, o): |
| 1500 | + """ To support ne operator on the DUTs (nodes) in the testbed """ |
| 1501 | + return self.nodes.__ne__(o) |
| 1502 | + |
| 1503 | + def __hash__(self): |
| 1504 | + """ To support hash operator on the DUTs (nodes) in the testbed """ |
| 1505 | + return self.nodes.__hash__() |
| 1506 | + |
| 1507 | + def __getattr__(self, attr): |
| 1508 | + """To support calling ansible modules directly on all the DUTs (nodes) in the testbed |
| 1509 | + Args: |
| 1510 | + attr: attribute to get |
| 1511 | +
|
| 1512 | + Returns: |
| 1513 | + a dictionary with key being the MultiAsicSonicHost's hostname, and value being the output of ansible module |
| 1514 | + on that MultiAsicSonicHost |
| 1515 | + """ |
| 1516 | + return getattr(self.nodes, attr) |
| 1517 | + |
| 1518 | + def _is_supervisor_node(self, node): |
| 1519 | + """ Is node a supervisor node |
| 1520 | +
|
| 1521 | + Args: |
| 1522 | + node: MultiAsicSonicHost object represent a DUT in the testbed. |
| 1523 | +
|
| 1524 | + Returns: |
| 1525 | + Currently, we are using 'type' in the inventory to make the decision. |
| 1526 | + if 'type' for the node is defined in the inventory, and it is 'supervisor', then return True, else return False |
| 1527 | + In future, we can change this logic if possible to derive it from the DUT. |
| 1528 | + """ |
| 1529 | + if 'type' in node.host.options["inventory_manager"].get_host(node.hostname).get_vars(): |
| 1530 | + card_type = node.host.options["inventory_manager"].get_host(node.hostname).get_vars()["type"] |
| 1531 | + if card_type is not None and card_type == 'supervisor': |
| 1532 | + return True |
| 1533 | + return False |
| 1534 | + |
| 1535 | + def _is_frontend_node(self, node): |
| 1536 | + """ Is not a frontend node |
| 1537 | + Args: |
| 1538 | + node: MultiAsicSonicHost object represent a DUT in the testbed. |
| 1539 | +
|
| 1540 | + Returns: |
| 1541 | + True if it is not any other type of node. |
| 1542 | + Currently, the only other type of node supported is 'supervisor' node. If we add more types of nodes, then |
| 1543 | + we need to exclude them from this method as well. |
| 1544 | + """ |
| 1545 | + return node not in self.supervisor_nodes |
| 1546 | + |
| 1547 | + |
1305 | 1548 | class FanoutHost(object): |
1306 | 1549 | """ |
1307 | 1550 | @summary: Class for Fanout switch |
|
0 commit comments