files: .. _classes:
  
  ===============
      Classes
  ===============
  
  
  Class Structure
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Here's the overview of core classes::
  
      Common
      ├── Tree
      ├── Core
      │   ├── Plan
      │   ├── Story
      │   └── Test
      ├── Step
      │   ├── Discover
      │   ├── Execute
      │   ├── Finish
      │   ├── Prepare
      │   ├── Provision
      │   └── Report
      ├── Plugin
      │   ├── DiscoverPlugin
      │   │   ├── DiscoverFmf
      │   │   └── DiscoverShell
      │   ├── ExecutePlugin
      │   │   ├── ExecuteDetach
      │   │   └── ExecuteInternal
      │   ├── FinishPlugin
      │   │   └── FinishShell
      │   ├── PreparePlugin
      │   │   ├── PrepareAnsible
      │   │   ├── PrepareInstall
      │   │   └── PrepareShell
      │   ├── ProvisionPlugin
      │   │   ├── ProvisionConnect
      │   │   ├── ProvisionLocal
      │   │   ├── ProvisionMinute
      │   │   ├── ProvisionPodman
      │   │   └── ProvisionTestcloud
      │   └── ReportPlugin
      │       ├── ReportDisplay
      │       └── ReportHtml
      ├── Guest
      │   ├── GuestContainer
      │   ├── GuestLocal
      │   ├── GuestMinute
      │   └── GuestTestcloud
      ├── Run
      ├── Status
      └── Clean
  
  
  Attributes
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Object hierarchy is following: ``Run`` -> ``Plans`` -> ``Steps``
  -> ``Plugins``, where the ``Run`` is on the top of this hierarchy.
  The objects have the ``parent`` attribute, that is pointing to
  the parent in which the current instance is contained.
  
  The ``node`` attribute of ``Test``, ``Plan`` and ``Story``
  instances references the original leaf node of the fmf metadata
  tree from which the respective test, plan or story have been
  created.
  
  In a similar way, the ``tree`` property of the ``Tree`` instance
  points to the original ``fmf.Tree`` from which it was initialized.
  
  
  Class Conversions
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Various internal objects and classes often need to be converted
  from their Python nature to data that can be saved, loaded or
  exported in different
  
  [... the rest is omitted ...]
files: docs/classes.rst
files: .. _classes:
  
  ===============
      Classes
  ===============
  
  
  Class Structure
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Here's the overview of core classes::
  
      Common
      ├── Tree
      ├── Core
      │   ├── Plan
      │   ├── Story
      │   └── Test
      ├── Step
      │   ├── Discover
      │   ├── Execute
      │   ├── Finish
      │   ├── Prepare
      │   ├── Provision
      │   └── Report
      ├── Plugin
      │   ├── DiscoverPlugin
      │   │   ├── DiscoverFmf
      │   │   └── DiscoverShell
      │   ├── ExecutePlugin
      │   │   ├── ExecuteDetach
      │   │   └── ExecuteInternal
      │   ├── FinishPlugin
      │   │   └── FinishShell
      │   ├── PreparePlugin
      │   │   ├── PrepareAnsible
      │   │   ├── PrepareInstall
      │   │   └── PrepareShell
      │   ├── ProvisionPlugin
      │   │   ├── ProvisionConnect
      │   │   ├── ProvisionLocal
      │   │   ├── ProvisionMinute
      │   │   ├── ProvisionPodman
      │   │   └── ProvisionTestcloud
      │   └── ReportPlugin
      │       ├── ReportDisplay
      │       └── ReportHtml
      ├── Guest
      │   ├── GuestContainer
      │   ├── GuestLocal
      │   ├── GuestMinute
      │   └── GuestTestcloud
      ├── Run
      ├── Status
      └── Clean
  
  
  Attributes
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  Object hierarchy is following: ``Run`` -> ``Plans`` -> ``Steps``
  -> ``Plugins``, where the ``Run`` is on the top of this hierarchy.
  The objects have the ``parent`` attribute, that is pointing to
  the parent in which the current instance is contained.
  
  The ``node`` attribute of ``Test``, ``Plan`` and ``Story``
  instances references the original leaf node of the fmf metadata
  tree from which the respective test, plan or story have been
  created.
  
  In a similar way, the ``tree`` property of the ``Tree`` instance
  points to the original ``fmf.Tree`` from which it was initialized.
  
  
  Essential Classes
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  .. automodule:: tmt
      :members:
      :undoc-members:
  
  
  [... the rest is omitted ...]
files: docs/classes.rst
files: 
  """ Base Metadata Classes """
  
  import copy
  import dataclasses
  import enum
  import functools
  import os
  import re
  import shutil
  import subprocess
  import sys
  import time
  from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union, cast
  
  import click
  import fmf
  import fmf.base
  from click import confirm, echo, style
  from fmf.utils import listed
  from ruamel.yaml.error import MarkedYAMLError
  
  import tmt.export
  import tmt.identifier
  import tmt.steps
  import tmt.steps.discover
  import tmt.steps.execute
  import tmt.steps.finish
  import tmt.steps.prepare
  import tmt.steps.provision
  import tmt.steps.report
  import tmt.templates
  import tmt.utils
  from tmt.utils import FmfContextType, verdict
  
  if sys.version_info >= (3, 8):
      from typing import Literal, TypedDict
  else:
      from typing_extensions import Literal, TypedDict
  
  # Default test duration is 5m for individual tests discovered from L1
  # metadata and 1h for scripts defined directly in plans (L2 metadata).
  DEFAULT_TEST_DURATION_L1 = '5m'
  DEFAULT_TEST_DURATION_L2 = '1h'
  DEFAULT_ORDER = 50
  
  # How many already existing lines should tmt run --follow show
  FOLLOW_LINES = 10
  
  # Obsoleted test keys
  OBSOLETED_TEST_KEYS = "relevancy coverage".split()
  
  # Unofficial temporary test keys
  EXTRA_TEST_KEYS = (
      "extra-nitrate extra-hardware extra-pepa "
      "extra-summary extra-task id".split())
  
  # Unofficial temporary story keys
  EXTRA_STORY_KEYS = ("id".split())
  
  SECTIONS_HEADINGS = {
      'Setup': ['<h1>Setup</h1>'],
      'Test': ['<h1>Test</h1>',
               '<h1>Test .*</h1>'],
      'Step': ['<h2>Step</h2>',
               '<h2>Test Step</h2>'],
      'Expect': ['<h2>Expect</h2>',
                 '<h2>Result</h2>',
                 '<h2>Expected Result</h2>'],
      'Cleanup': ['<h1>Cleanup</h1>']
      }
  
  # Extra keys used for identification in Result class
  EXTRA_RESULT_IDENTIFICATION_KEYS = ['extra-nitrate', 'extra-task']
  
  
  #
  # fmf
  
  [... the rest is omitted ...]
files: tmt/base.py
files: 
  """ Base Metadata Classes """
  
  import copy
  import dataclasses
  import enum
  import functools
  import os
  import re
  import shutil
  import subprocess
  import sys
  import time
  from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union, cast
  
  import click
  import fmf
  import fmf.base
  from click import confirm, echo, style
  from fmf.utils import listed
  from ruamel.yaml.error import MarkedYAMLError
  
  import tmt.export
  import tmt.identifier
  import tmt.steps
  import tmt.steps.discover
  import tmt.steps.execute
  import tmt.steps.finish
  import tmt.steps.prepare
  import tmt.steps.provision
  import tmt.steps.report
  import tmt.templates
  import tmt.utils
  from tmt.utils import FmfContextType, verdict
  
  if sys.version_info >= (3, 8):
      from typing import Literal, TypedDict
  else:
      from typing_extensions import Literal, TypedDict
  
  # Default test duration is 5m for individual tests discovered from L1
  # metadata and 1h for scripts defined directly in plans (L2 metadata).
  DEFAULT_TEST_DURATION_L1 = '5m'
  DEFAULT_TEST_DURATION_L2 = '1h'
  DEFAULT_ORDER = 50
  
  # How many already existing lines should tmt run --follow show
  FOLLOW_LINES = 10
  
  # Obsoleted test keys
  OBSOLETED_TEST_KEYS = "relevancy coverage".split()
  
  # Unofficial temporary test keys
  EXTRA_TEST_KEYS = (
      "extra-nitrate extra-hardware extra-pepa "
      "extra-summary extra-task id".split())
  
  # Unofficial temporary story keys
  EXTRA_STORY_KEYS = ("id".split())
  
  SECTIONS_HEADINGS = {
      'Setup': ['<h1>Setup</h1>'],
      'Test': ['<h1>Test</h1>',
               '<h1>Test .*</h1>'],
      'Step': ['<h2>Step</h2>',
               '<h2>Test Step</h2>'],
      'Expect': ['<h2>Expect</h2>',
                 '<h2>Result</h2>',
                 '<h2>Expected Result</h2>'],
      'Cleanup': ['<h1>Cleanup</h1>']
      }
  
  # Extra keys used for identification in Result class
  EXTRA_RESULT_IDENTIFICATION_KEYS = ['extra-nitrate', 'extra-task']
  
  
  #
  # fmf
  
  [... the rest is omitted ...]
files: tmt/base.py
files: 
  """ Step Classes """
  
  import dataclasses
  import re
  import sys
  import textwrap
  import warnings
  from typing import (TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type,
                      TypeVar, Union, cast, overload)
  
  if sys.version_info >= (3, 8):
      from typing import TypedDict
  else:
      from typing_extensions import TypedDict
  
  import click
  from click import echo
  
  import tmt.options
  import tmt.utils
  from tmt.options import show_step_method_hints
  
  if TYPE_CHECKING:
      import tmt.base
      from tmt.base import Plan
      from tmt.steps.provision import Guest
  
  
  DEFAULT_PLUGIN_METHOD_ORDER: int = 50
  
  
  # Supported steps and actions
  STEPS = ['discover', 'provision', 'prepare', 'execute', 'report', 'finish']
  ACTIONS = ['login', 'reboot']
  
  # Step phase order
  PHASE_START = 10
  PHASE_BASE = 50
  PHASE_END = 90
  
  
  class Phase(tmt.utils.Common):
      """ A phase of a step """
  
      def __init__(
              self,
              order: int = tmt.utils.DEFAULT_PLUGIN_ORDER,
              *args: Any,
              **kwargs: Any):
          super().__init__(*args, **kwargs)
          self.order: int = order
  
      def enabled_on_guest(self, guest: 'Guest') -> bool:
          """ Phases are enabled across all guests by default """
          return True
  
      @property
      def is_in_standalone_mode(self) -> bool:
          """
          True if the phase is in stand-alone mode.
  
          Stand-alone mode means that only this phase should be run as a part
          of the run (and not any other even if requested using --all).
          This is useful as some plugin options may completely change its
          behaviour from the regular behaviour based on options
          (e.g. listing images inside a provision plugin).
          """
          return False
  
  
  # A variable used to describe a generic type for all classes derived from Phase
  PhaseT = TypeVar('PhaseT', bound=Phase)
  
  
  _RawStepData = TypedDict('_RawStepData', {
      'how': str,
      'name': str
  }, total=False)
  
  RawStepDataArgument = Union[_RawStepData, List[_RawStepData]]
  
  
  T = TypeVar('T', bound='StepData')
  
  
  @dataclasses.dataclass
  class StepData(
          tmt.utils.SpecBasedContainer,
          tmt.utils.NormalizeKeysMixin,
          tmt.utils.SerializableContainer):
      """
      Keys necessary to
  
  [... the rest is omitted ...]
files: tmt/steps/__init__.py
files: 
  """ Step Classes """
  
  import dataclasses
  import re
  import sys
  import textwrap
  import warnings
  from typing import (TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type,
                      TypeVar, Union, cast, overload)
  
  if sys.version_info >= (3, 8):
      from typing import TypedDict
  else:
      from typing_extensions import TypedDict
  
  import click
  from click import echo
  
  import tmt.options
  import tmt.utils
  from tmt.options import show_step_method_hints
  
  if TYPE_CHECKING:
      import tmt.base
      from tmt.base import Plan
      from tmt.steps.provision import Guest
  
  
  DEFAULT_PLUGIN_METHOD_ORDER: int = 50
  
  
  # Supported steps and actions
  STEPS = ['discover', 'provision', 'prepare', 'execute', 'report', 'finish']
  ACTIONS = ['login', 'reboot']
  
  # Step phase order
  PHASE_START = 10
  PHASE_BASE = 50
  PHASE_END = 90
  
  
  class Phase(tmt.utils.Common):
      """ A phase of a step """
  
      def __init__(
              self,
              order: int = tmt.utils.DEFAULT_PLUGIN_ORDER,
              *args: Any,
              **kwargs: Any):
          super().__init__(*args, **kwargs)
          self.order: int = order
  
      def enabled_on_guest(self, guest: 'Guest') -> bool:
          """ Phases are enabled across all guests by default """
          return True
  
      @property
      def is_in_standalone_mode(self) -> bool:
          """
          True if the phase is in stand-alone mode.
  
          Stand-alone mode means that only this phase should be run as a part
          of the run (and not any other even if requested using --all).
          This is useful as some plugin options may completely change its
          behaviour from the regular behaviour based on options
          (e.g. listing images inside a provision plugin).
          """
          return False
  
  
  # A variable used to describe a generic type for all classes derived from Phase
  PhaseT = TypeVar('PhaseT', bound=Phase)
  
  
  _RawStepData = TypedDict('_RawStepData', {
      'how': str,
      'name': str
  }, total=False)
  
  RawStepDataArgument = Union[_RawStepData, List[_RawStepData]]
  
  
  T = TypeVar('T', bound='StepData')
  
  
  @dataclasses.dataclass
  class StepData(tmt.utils.NormalizeKeysMixin, tmt.utils.SerializableContainer):
      """
      Keys necessary to describe, create, save and restore a step.
  
      Very
  
  [... the rest is omitted ...]
files: tmt/steps/__init__.py
files: import dataclasses
  import glob
  import os
  import re
  import shutil
  import subprocess
  from typing import List, Optional, cast
  
  import click
  import fmf
  
  import tmt
  import tmt.base
  import tmt.beakerlib
  import tmt.options
  import tmt.steps
  import tmt.steps.discover
  import tmt.utils
  
  
  @dataclasses.dataclass
  class DiscoverFmfStepData(tmt.steps.discover.DiscoverStepData):
      url: Optional[str] = None
      ref: Optional[str] = None
      path: Optional[str] = None
      test: List[str] = dataclasses.field(default_factory=list)
      link: List[str] = dataclasses.field(default_factory=list)
      filter: List[str] = dataclasses.field(default_factory=list)
      modified_only: bool = False
      modified_url: Optional[str] = None
      modified_ref: Optional[str] = None
      dist_git_init: bool = False
      dist_git_remove_fmf_root: bool = False
      dist_git_merge: bool = False
      dist_git_extract: Optional[str] = None
      fmf_id: bool = False
      exclude: List[str] = dataclasses.field(default_factory=list)
  
      # Legacy fields
      repository: Optional[str] = None
      revision: Optional[str] = None
  
      _normalize_test = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_link = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_filter = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_exclude = tmt.utils.NormalizeKeysMixin._normalize_string_list
  
      def post_normalization(self, raw_data: tmt.steps._RawStepData,
                             logger: tmt.utils.Common) -> None:
          super().post_normalization(raw_data, logger)
  
          if self.repository:
              self.url = self.repository
  
          if self.revision:
              self.ref = self.revision
  
  
  @tmt.steps.provides_method('fmf')
  class DiscoverFmf(tmt.steps.discover.DiscoverPlugin):
      """
      Discover available tests from fmf metadata
  
      By default all available tests from the current repository are used
      so the minimal configuration looks like this:
  
          discover:
              how: fmf
  
      Full config example:
  
          discover:
              how: fmf
              url: https://github.com/teemtee/tmt
              ref: main
              path: /fmf/root
              test: /tests
  
  [... the rest is omitted ...]
files: tmt/steps/discover/fmf.py
files: import dataclasses
  import glob
  import os
  import re
  import shutil
  import subprocess
  from typing import List, Optional, cast
  
  import click
  import fmf
  
  import tmt
  import tmt.base
  import tmt.beakerlib
  import tmt.options
  import tmt.steps
  import tmt.steps.discover
  import tmt.utils
  
  
  @dataclasses.dataclass
  class DiscoverFmfStepData(tmt.steps.discover.DiscoverStepData):
      url: Optional[str] = None
      ref: Optional[str] = None
      path: Optional[str] = None
      test: List[str] = dataclasses.field(default_factory=list)
      link: List[str] = dataclasses.field(default_factory=list)
      filter: List[str] = dataclasses.field(default_factory=list)
      modified_only: bool = False
      modified_url: Optional[str] = None
      modified_ref: Optional[str] = None
      dist_git_init: bool = False
      dist_git_remove_fmf_root: bool = False
      dist_git_merge: bool = False
      dist_git_extract: Optional[str] = None
      fmf_id: bool = False
      exclude: List[str] = dataclasses.field(default_factory=list)
  
      # Legacy fields
      repository: Optional[str] = None
      revision: Optional[str] = None
  
      _normalize_test = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_link = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_filter = tmt.utils.NormalizeKeysMixin._normalize_string_list
      _normalize_exclude = tmt.utils.NormalizeKeysMixin._normalize_string_list
  
      def post_normalization(self, raw_data: tmt.steps._RawStepData,
                             logger: tmt.utils.Common) -> None:
          super().post_normalization(raw_data, logger)
  
          if self.repository:
              self.url = self.repository
  
          if self.revision:
              self.ref = self.revision
  
  
  @tmt.steps.provides_method('fmf')
  class DiscoverFmf(tmt.steps.discover.DiscoverPlugin):
      """
      Discover available tests from fmf metadata
  
      By default all available tests from the current repository are used
      so the minimal configuration looks like this:
  
          discover:
              how: fmf
  
      Full config example:
  
          discover:
              how: fmf
              url: https://github.com/teemtee/tmt
              ref: main
              path: /fmf/root
              test: /tests
  
  [... the rest is omitted ...]
files: tmt/steps/discover/fmf.py
files: import copy
  import dataclasses
  import os
  from typing import (TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar,
                      Union, cast)
  
  import click
  import fmf
  
  import tmt
  import tmt.base
  import tmt.steps
  import tmt.steps.discover
  import tmt.utils
  
  if TYPE_CHECKING:
      import tmt.base
  
  
  T = TypeVar('T', bound='TestDescription')
  
  
  @dataclasses.dataclass
  class TestDescription(
          tmt.utils.SpecBasedContainer,
          tmt.utils.NormalizeKeysMixin,
          tmt.utils.SerializableContainer):
      """
      Keys necessary to describe a shell-based test.
  
      Provides basic functionality for tansition between "raw" step data representation,
      which consists of keys and values given by fmf tree and CLI options, and this
      container representation for internal use.
      """
  
      name: str
  
      # TODO: following keys are copy & pasted from base.Test. It would be much, much better
      # to re-use the definitions from base.Test instead copying them here, but base.Test
      # does not support save/load operations. This is a known issue, introduced by a patch
      # transitioning step data to data classes, it is temporary, and it will be fixed as
      # soon as possible - nobody want's to keep two very same lists of attributes.
      test: str
  
      # Core attributes (supported across all levels)
      summary: Optional[str] = None
      description: Optional[str] = None
      enabled: bool = True
      # TODO: ugly circular dependency (see tmt.base.DEFAULT_ORDER)
      order: int = 50
      link: Optional['tmt.base.Links'] = None
      id: Optional[str] = None
      tag: List[str] = dataclasses.field(default_factory=list)
      tier: Optional[str] = None
      adjust: Optional[List['tmt.base._RawAdjustRule']] = None
  
      # Basic test information
      contact: List[str] = dataclasses.field(default_factory=list)
      component: List[str] = dataclasses.field(default_factory=list)
  
      # Test execution data
      path: Optional[str] = None
      framework: Optional[str] = None
      manual: bool = False
      require: List[Union[str, 'tmt.base.FmfId']] = dataclasses.field(default_factory=list)
      recommend: List[str] = dataclasses.field(default_factory=list)
      environment: tmt.utils.EnvironmentType = dataclasses.field(default_factory=dict)
      duration: str = '1h'
      result
  
  [... the rest is omitted ...]
files: tmt/steps/discover/shell.py
files: import copy
  import dataclasses
  import os
  from typing import (TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar,
                      Union, cast)
  
  import click
  import fmf
  
  import tmt
  import tmt.base
  import tmt.steps
  import tmt.steps.discover
  import tmt.utils
  
  if TYPE_CHECKING:
      import tmt.base
  
  
  T = TypeVar('T', bound='TestDescription')
  
  
  @dataclasses.dataclass
  class TestDescription(tmt.utils.NormalizeKeysMixin, tmt.utils.SerializableContainer):
      """
      Keys necessary to describe a shell-based test.
  
      Provides basic functionality for tansition between "raw" step data representation,
      which consists of keys and values given by fmf tree and CLI options, and this
      container representation for internal use.
      """
  
      name: str
  
      # TODO: following keys are copy & pasted from base.Test. It would be much, much better
      # to re-use the definitions from base.Test instead copying them here, but base.Test
      # does not support save/load operations. This is a known issue, introduced by a patch
      # transitioning step data to data classes, it is temporary, and it will be fixed as
      # soon as possible - nobody want's to keep two very same lists of attributes.
      test: str
  
      # Core attributes (supported across all levels)
      summary: Optional[str] = None
      description: Optional[str] = None
      enabled: bool = True
      # TODO: ugly circular dependency (see tmt.base.DEFAULT_ORDER)
      order: int = 50
      link: Optional['tmt.base.Links'] = None
      id: Optional[str] = None
      tag: List[str] = dataclasses.field(default_factory=list)
      tier: Optional[str] = None
      adjust: Optional[List['tmt.base._RawAdjustRule']] = None
  
      # Basic test information
      contact: List[str] = dataclasses.field(default_factory=list)
      component: List[str] = dataclasses.field(default_factory=list)
  
      # Test execution data
      path: Optional[str] = None
      framework: Optional[str] = None
      manual: bool = False
      require: List[Union[str, 'tmt.base.FmfId']] = dataclasses.field(default_factory=list)
      recommend: List[str] = dataclasses.field(default_factory=list)
      environment: tmt.utils.EnvironmentType = dataclasses.field(default_factory=dict)
      duration: str = '1h'
      result: str = 'respect'
  
      # We can't even
  
  [... the rest is omitted ...]
files: tmt/steps/discover/shell.py
files: # coding: utf-8
  
  import dataclasses
  import datetime
  import os
  import platform
  import re
  import time
  import types
  from typing import Any, List, Optional, cast
  
  import click
  import fmf
  import requests
  
  import tmt
  import tmt.steps
  import tmt.steps.provision
  import tmt.utils
  from tmt.utils import WORKDIR_ROOT, ProvisionError, retry_session
  
  libvirt: Optional[types.ModuleType] = None
  testcloud: Optional[types.ModuleType] = None
  
  
  def import_testcloud() -> None:
      """
      Import testcloud module only when needed
  
      Until we have a separate package for each plugin.
      """
      global testcloud
      global libvirt
      try:
          import libvirt
          import testcloud.image
          import testcloud.instance
          import testcloud.util
      except ImportError:
          raise ProvisionError(
              "Install 'testcloud' to provision using this method.")
  
  
  # Testcloud cache to our tmt's workdir root
  TESTCLOUD_DATA = os.path.join(WORKDIR_ROOT, 'testcloud')
  TESTCLOUD_IMAGES = os.path.join(TESTCLOUD_DATA, 'images')
  
  # Userdata for cloud-init
  USER_DATA = """#cloud-config
  chpasswd:
    list: |
      {user_name}:%s
    expire: false
  users:
    - default
    - name: {user_name}
  ssh_authorized_keys:
    - {public_key}
  ssh_pwauth: true
  disable_root: false
  runcmd:
    - sed -i -e '/^.*PermitRootLogin/s/^.*$/PermitRootLogin yes/'
      -e '/^.*UseDNS/s/^.*$/UseDNS no/'
      -e '/^.*GSSAPIAuthentication/s/^.*$/GSSAPIAuthentication no/'
      /etc/ssh/sshd_config
    - systemctl reload sshd
    - [sh, -c, 'if [ ! -f /etc/systemd/network/20-tc-usernet.network ] &&
    systemctl status systemd-networkd | grep -q "enabled;\\svendor\\spreset:\\senabled";
    then mkdir -p /etc/systemd/network/ &&
    echo "[Match]" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "Name=en*" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "[Network]" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "DHCP=yes" >>
  
  [... the rest is omitted ...]
files: tmt/steps/provision/testcloud.py
files: # coding: utf-8
  
  import dataclasses
  import datetime
  import os
  import platform
  import re
  import time
  import types
  from typing import Any, List, Optional, cast
  
  import click
  import fmf
  import requests
  
  import tmt
  import tmt.steps
  import tmt.steps.provision
  import tmt.utils
  from tmt.utils import WORKDIR_ROOT, ProvisionError, retry_session
  
  libvirt: Optional[types.ModuleType] = None
  testcloud: Optional[types.ModuleType] = None
  
  
  def import_testcloud() -> None:
      """
      Import testcloud module only when needed
  
      Until we have a separate package for each plugin.
      """
      global testcloud
      global libvirt
      try:
          import libvirt
          import testcloud.image
          import testcloud.instance
          import testcloud.util
      except ImportError:
          raise ProvisionError(
              "Install 'testcloud' to provision using this method.")
  
  
  # Testcloud cache to our tmt's workdir root
  TESTCLOUD_DATA = os.path.join(WORKDIR_ROOT, 'testcloud')
  TESTCLOUD_IMAGES = os.path.join(TESTCLOUD_DATA, 'images')
  
  # Userdata for cloud-init
  USER_DATA = """#cloud-config
  chpasswd:
    list: |
      {user_name}:%s
    expire: false
  users:
    - default
    - name: {user_name}
  ssh_authorized_keys:
    - {public_key}
  ssh_pwauth: true
  disable_root: false
  runcmd:
    - sed -i -e '/^.*PermitRootLogin/s/^.*$/PermitRootLogin yes/'
      -e '/^.*UseDNS/s/^.*$/UseDNS no/'
      -e '/^.*GSSAPIAuthentication/s/^.*$/GSSAPIAuthentication no/'
      /etc/ssh/sshd_config
    - systemctl reload sshd
    - [sh, -c, 'if [ ! -f /etc/systemd/network/20-tc-usernet.network ] &&
    systemctl status systemd-networkd | grep -q "enabled;\\svendor\\spreset:\\senabled";
    then mkdir -p /etc/systemd/network/ &&
    echo "[Match]" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "Name=en*" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "[Network]" >> /etc/systemd/network/20-tc-usernet.network &&
    echo "DHCP=yes" >>
  
  [... the rest is omitted ...]
files: tmt/steps/provision/testcloud.py
files: 
  """ Test Metadata Utilities """
  
  import contextlib
  import copy
  import dataclasses
  import datetime
  import functools
  import glob
  import io
  import os
  import pprint
  import re
  import shlex
  import shutil
  import subprocess
  import sys
  import time
  import unicodedata
  from collections import OrderedDict
  from functools import lru_cache
  from pathlib import Path
  from threading import Thread
  from typing import (IO, TYPE_CHECKING, Any, Callable, Dict, Generator,
                      Iterable, List, NamedTuple, Optional, Pattern, Tuple, Type,
                      TypeVar, Union, cast, overload)
  
  import click
  import fmf
  import jsonschema
  import pkg_resources
  import requests
  import requests.adapters
  import requests.packages.urllib3.util.retry
  import urllib3.exceptions
  from click import echo, style, wrap_text
  from ruamel.yaml import YAML, scalarstring
  from ruamel.yaml.comments import CommentedMap
  from ruamel.yaml.parser import ParserError
  
  if sys.version_info >= (3, 8):
      from typing import Literal, Protocol
  else:
      from typing_extensions import Literal, Protocol
  
  
  if TYPE_CHECKING:
      import tmt.base
      import tmt.steps
  
  log = fmf.utils.Logging('tmt').logger
  
  # Default workdir root and max
  WORKDIR_ROOT = '/var/tmp/tmt'
  WORKDIR_MAX = 1000
  
  # Log in workdir
  LOG_FILENAME = 'log.txt'
  
  # Maximum number of lines of stdout/stderr to show upon errors
  OUTPUT_LINES = 100
  # Default output width
  OUTPUT_WIDTH = 79
  
  # Hierarchy indent
  INDENT = 4
  
  # Default name and order for step plugins
  DEFAULT_NAME = 'default'
  DEFAULT_PLUGIN_ORDER = 50
  DEFAULT_PLUGIN_ORDER_MULTIHOST = 10
  DEFAULT_PLUGIN_ORDER_REQUIRES = 70
  DEFAULT_PLUGIN_ORDER_RECOMMENDS = 75
  
  # Config directory
  CONFIG_PATH = '~/.config/tmt'
  
  # Special process return code
  PROCESS_TIMEOUT = 124
  
  # Default select.select(timeout) in seconds
  DEFAULT_SELECT_TIMEOUT = 5
  
  # Default shell and options to be set for all shell scripts
  DEFAULT_SHELL = "/bin/bash"
  SHELL_OPTIONS = 'set -eo pipefail'
  
  # Defaults for HTTP/HTTPS retries and timeouts (see `retry_session()`).
  DEFAULT_RETRY_SESSION_RET
  
  [... the rest is omitted ...]
files: tmt/utils.py
files: 
  """ Test Metadata Utilities """
  
  import contextlib
  import copy
  import dataclasses
  import datetime
  import functools
  import glob
  import io
  import os
  import pprint
  import re
  import shlex
  import shutil
  import subprocess
  import sys
  import time
  import unicodedata
  from collections import OrderedDict
  from functools import lru_cache
  from pathlib import Path
  from threading import Thread
  from typing import (IO, TYPE_CHECKING, Any, Callable, Dict, Generator,
                      Iterable, List, NamedTuple, Optional, Pattern, Tuple, Type,
                      TypeVar, Union, cast, overload)
  
  import click
  import fmf
  import jsonschema
  import pkg_resources
  import requests
  import requests.adapters
  import requests.packages.urllib3.util.retry
  import urllib3.exceptions
  from click import echo, style, wrap_text
  from ruamel.yaml import YAML, scalarstring
  from ruamel.yaml.comments import CommentedMap
  from ruamel.yaml.parser import ParserError
  
  if sys.version_info >= (3, 8):
      from typing import Literal, Protocol
  else:
      from typing_extensions import Literal, Protocol
  
  
  if TYPE_CHECKING:
      import tmt.base
      import tmt.steps
  
  log = fmf.utils.Logging('tmt').logger
  
  # Default workdir root and max
  WORKDIR_ROOT = '/var/tmp/tmt'
  WORKDIR_MAX = 1000
  
  # Log in workdir
  LOG_FILENAME = 'log.txt'
  
  # Maximum number of lines of stdout/stderr to show upon errors
  OUTPUT_LINES = 100
  # Default output width
  OUTPUT_WIDTH = 79
  
  # Hierarchy indent
  INDENT = 4
  
  # Default name and order for step plugins
  DEFAULT_NAME = 'default'
  DEFAULT_PLUGIN_ORDER = 50
  DEFAULT_PLUGIN_ORDER_MULTIHOST = 10
  DEFAULT_PLUGIN_ORDER_REQUIRES = 70
  DEFAULT_PLUGIN_ORDER_RECOMMENDS = 75
  
  # Config directory
  CONFIG_PATH = '~/.config/tmt'
  
  # Special process return code
  PROCESS_TIMEOUT = 124
  
  # Default select.select(timeout) in seconds
  DEFAULT_SELECT_TIMEOUT = 5
  
  # Default shell and options to be set for all shell scripts
  DEFAULT_SHELL = "/bin/bash"
  SHELL_OPTIONS = 'set -eo pipefail'
  
  # Defaults for HTTP/HTTPS retries and timeouts (see `retry_session()`).
  DEFAULT_RETRY_SESSION_RET
  
  [... the rest is omitted ...]
files: tmt/utils.py