Revert only the latest snapshot that matches the test requirements

- add pytest marks 'revert_snapshot' to all required fixtures instead
  of reverting the snapshots inside them
- add get_top_fixtures_marks() that extracts all the marks
  'revert_snapshot' from the test and it's fixtures, order the marks
  in the same way as the fixtures depends on each other,
- in the fixture 'revert_snapshot' try to find the most suitable
  snapshot for reverting, from latest to earliest.

Co-Authored-By: Dmitry Tyzhnenko <dtyzhnenko@mirantis.com>
diff --git a/tcp_tests/fixtures/common_services_fixtures.py b/tcp_tests/fixtures/common_services_fixtures.py
index ed00574..2547a08 100644
--- a/tcp_tests/fixtures/common_services_fixtures.py
+++ b/tcp_tests/fixtures/common_services_fixtures.py
@@ -36,6 +36,7 @@
     return common_services_manager.CommonServicesManager(config, underlay)
 
 
+@pytest.mark.revert_snapshot(ext.SNAPSHOT.common_services_deployed)
 @pytest.fixture(scope='function')
 def common_services_deployed(revert_snapshot, request, config,
                              hardware, underlay, salt_deployed,
@@ -67,15 +68,6 @@
     If you want to revert 'common_services_deployed' snapshot, please use mark:
     @pytest.mark.revert_snapshot("common_services_deployed")
     """
-    # If no snapshot was reverted, then try to revert the snapshot
-    # that belongs to the fixture.
-    # Note: keep fixtures in strict dependences from each other!
-    if not revert_snapshot:
-        if hardware.has_snapshot(ext.SNAPSHOT.common_services_deployed) and \
-                hardware.has_snapshot_config(
-                    ext.SNAPSHOT.common_services_deployed):
-            hardware.revert_snapshot(ext.SNAPSHOT.common_services_deployed)
-
     # Create Salt cluster
     if not config.common_services.common_services_installed:
         steps_path = config.common_services_deploy.common_services_steps_path
diff --git a/tcp_tests/fixtures/openstack_fixtures.py b/tcp_tests/fixtures/openstack_fixtures.py
index c7e648e..f0a9024 100644
--- a/tcp_tests/fixtures/openstack_fixtures.py
+++ b/tcp_tests/fixtures/openstack_fixtures.py
@@ -38,6 +38,7 @@
     return openstack_manager.OpenstackManager(config, underlay)
 
 
+@pytest.mark.revert_snapshot(ext.SNAPSHOT.openstack_deployed)
 @pytest.fixture(scope='function')
 def openstack_deployed(revert_snapshot, request, config,
                        hardware, underlay, common_services_deployed,
@@ -66,14 +67,6 @@
     If you want to revert 'openstack_deployed' snapshot, please use mark:
     @pytest.mark.revert_snapshot("openstack_deployed")
     """
-    # If no snapshot was reverted, then try to revert the snapshot
-    # that belongs to the fixture.
-    # Note: keep fixtures in strict dependences from each other!
-    if not revert_snapshot:
-        if hardware.has_snapshot(ext.SNAPSHOT.openstack_deployed) and \
-                hardware.has_snapshot_config(ext.SNAPSHOT.openstack_deployed):
-            hardware.revert_snapshot(ext.SNAPSHOT.openstack_deployed)
-
     # Create Salt cluster
     if not config.openstack.openstack_installed:
         steps_path = config.openstack_deploy.openstack_steps_path
diff --git a/tcp_tests/fixtures/salt_fixtures.py b/tcp_tests/fixtures/salt_fixtures.py
index 047f106..977b38d 100644
--- a/tcp_tests/fixtures/salt_fixtures.py
+++ b/tcp_tests/fixtures/salt_fixtures.py
@@ -36,6 +36,7 @@
     return saltmanager.SaltManager(config, underlay)
 
 
+@pytest.mark.revert_snapshot(ext.SNAPSHOT.salt_deployed)
 @pytest.fixture(scope='function')
 def salt_deployed(revert_snapshot, request, config,
                   hardware, underlay, salt_actions):
@@ -63,14 +64,6 @@
     If you want to revert 'salt_deployed' snapshot, please use mark:
     @pytest.mark.revert_snapshot("salt_deployed")
     """
-    # If no snapshot was reverted, then try to revert the snapshot
-    # that belongs to the fixture.
-    # Note: keep fixtures in strict dependences from each other!
-    if not revert_snapshot:
-        if hardware.has_snapshot(ext.SNAPSHOT.salt_deployed) and \
-                hardware.has_snapshot_config(ext.SNAPSHOT.salt_deployed):
-            hardware.revert_snapshot(ext.SNAPSHOT.salt_deployed)
-
     # Create Salt cluster
     if config.salt.salt_master_host == '0.0.0.0':
         with underlay.yaml_editor(
diff --git a/tcp_tests/fixtures/underlay_fixtures.py b/tcp_tests/fixtures/underlay_fixtures.py
index a19b6d1..d7351bf 100644
--- a/tcp_tests/fixtures/underlay_fixtures.py
+++ b/tcp_tests/fixtures/underlay_fixtures.py
@@ -16,6 +16,7 @@
 from datetime import datetime
 
 from tcp_tests.helpers import ext
+from tcp_tests.helpers import utils
 from tcp_tests import logger
 from tcp_tests import settings
 from tcp_tests.managers import envmanager_devops
@@ -25,20 +26,6 @@
 LOG = logger.logger
 
 
-def extract_name_from_mark(mark):
-    """Simple function to extract name from mark
-
-    :param mark: pytest.mark.MarkInfo
-    :rtype: string or None
-    """
-    if mark:
-        if len(mark.args) > 0:
-            return mark.args[0]
-        elif 'name' in mark.kwargs:
-            return mark.kwargs['name']
-    return None
-
-
 @pytest.fixture(scope="session")
 def hardware(request, config):
     """Fixture for manage the hardware layer.
@@ -110,17 +97,19 @@
 
     :rtype string: name of the reverted snapshot or None
     """
-    revert_snapshot = request.keywords.get('revert_snapshot', None)
-    snapshot_name = extract_name_from_mark(revert_snapshot)
+    top_fixtures_snapshots = utils.get_top_fixtures_marks(
+        request, 'revert_snapshot')
 
-    if snapshot_name and \
-            hardware.has_snapshot(snapshot_name) and \
-            hardware.has_snapshot_config(snapshot_name):
-        hardware.revert_snapshot(snapshot_name)
-        return snapshot_name
-    else:
-        hardware.revert_snapshot(ext.SNAPSHOT.hardware)
-        return None
+    # Try to revert the best matches snapshot for the test
+    for snapshot_name in top_fixtures_snapshots:
+        if hardware.has_snapshot(snapshot_name) and \
+                hardware.has_snapshot_config(snapshot_name):
+            hardware.revert_snapshot(snapshot_name)
+            return snapshot_name
+
+    # Fallback to the basic snapshot
+    hardware.revert_snapshot(ext.SNAPSHOT.hardware)
+    return None
 
 
 @pytest.fixture(scope='function', autouse=True)
@@ -146,7 +135,7 @@
                                         request.node.function.__name__)
         if hasattr(request.node, 'rep_call') and request.node.rep_call.passed \
                 and snapshot_needed:
-            snapshot_name = extract_name_from_mark(snapshot_needed) or \
+            snapshot_name = utils.extract_name_from_mark(snapshot_needed) or \
                 "{}_passed".format(default_snapshot_name)
             hardware.create_snapshot(snapshot_name)
 
@@ -163,6 +152,7 @@
     request.addfinalizer(test_fin)
 
 
+@pytest.mark.revert_snapshot(ext.SNAPSHOT.underlay)
 @pytest.fixture(scope="function")
 def underlay(revert_snapshot, config, hardware):
     """Fixture that should provide SSH access to underlay objects.
@@ -179,14 +169,6 @@
                                - provide SSH access to underlay nodes using
                                  node names or node IPs.
     """
-    # If no snapshot was reverted, then try to revert the snapshot
-    # that belongs to the fixture.
-    # Note: keep fixtures in strict dependences from each other!
-    if not revert_snapshot:
-        if hardware.has_snapshot(ext.SNAPSHOT.underlay) and \
-                hardware.has_snapshot_config(ext.SNAPSHOT.underlay):
-            hardware.revert_snapshot(ext.SNAPSHOT.underlay)
-
     # Create Underlay
     if not config.underlay.ssh:
         # If config.underlay.ssh wasn't provided from external config, then