Skip to content

Commit f5b414d

Browse files
authored
Implementation of multi-DUT and multi-ASIC as per PR 2347 (#2417)
PR sonic-net/SONiC#644 introduced the HLD to support multi ASIC. In the future, multi DUT or Chassis will be supported by SONiC as well. The test infrastructure and some of the customized ansible modules need to be updated to support testing of the upcoming new architectures. This PR is implementation of PR 2347 which tried to propose how to improve the current test infrastructure to support multi-DUT and multi-ASIC systems. The target is to ensure that the existing test scripts are not broken and we can update the tests in incremental way. This change is the implementation of PR 2347 - Add proposal for multi-DUT and multi-ASIC testing support - Added the classes described in the PR: - SonicAsic - represents an asic, and implements the asic/namespace related operations to hide the complexity of handling the asic/namespace specific details. - For now, have added bgp_facts as an example to add 'instance_id' to the bgp_facts module call on a SonicHost. - MutliAsicSonicHost - a host with one or more SonicAsics. - DutHosts - represents all the DUT's in a testbed. - has 'nodes' list to represent each DUT in the testbed. - Update duthosts fixture to return an instance of DutHosts instead of a list of SonicHosts - Modify duthost fixture to return a MultiAsicSonicHost from duthosts.nodes
1 parent 72c0282 commit f5b414d

File tree

2 files changed

+246
-2
lines changed

2 files changed

+246
-2
lines changed

tests/common/devices.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import re
1414
import inspect
1515
import ipaddress
16+
import copy
1617
from multiprocessing.pool import ThreadPool
1718
from datetime import datetime
1819

@@ -1302,6 +1303,248 @@ def execute (self, cmd) :
13021303
eval(cmd)
13031304

13041305

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+
13051548
class FanoutHost(object):
13061549
"""
13071550
@summary: Class for Fanout switch

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
from collections import defaultdict
1919
from datetime import datetime
2020
from tests.common.fixtures.conn_graph_facts import conn_graph_facts
21-
from tests.common.devices import SonicHost, Localhost
21+
from tests.common.devices import Localhost
2222
from tests.common.devices import PTFHost, EosHost, FanoutHost
2323
from tests.common.helpers.constants import ASIC_PARAM_TYPE_ALL, ASIC_PARAM_TYPE_FRONTEND, DEFAULT_ASIC_ID
2424
from tests.common.helpers.dut_ports import encode_dut_port_name
25+
from tests.common.devices import DutHosts
2526

2627
logger = logging.getLogger(__name__)
2728

@@ -214,7 +215,7 @@ def fixture_duthosts(ansible_adhoc, tbinfo):
214215
mandatory argument for the class constructors.
215216
@param tbinfo: fixture provides information about testbed.
216217
"""
217-
return [SonicHost(ansible_adhoc, dut) for dut in tbinfo["duts"]]
218+
return DutHosts(ansible_adhoc, tbinfo)
218219

219220

220221
@pytest.fixture(scope="session")

0 commit comments

Comments
 (0)