class Protocol(object):
    """
    Similar to a Connection object, provides a transport layer for the dispatcher.
    Uses a pre-defined API instead of pexpect using Shell.

    Testing a protocol involves either basing the protocol on SocketServer and using threading
    or adding a main function in the protocol python file and including a demo server script which
    can be run on the command line - using a different port to the default. However, this is likely
    to be of limited use because testing the actual API calls will need a functional test.

    If a Protocol requires another Protocol to be available in order to run, the depending
    Protocol *must* specify a higher level. All Protocol objects of a lower level are setup and
    run before Protocol objects of a higher level. Protocols with the same level can be setup or run
    in an arbitrary order (as the original source data is a dictionary).
    """
    name = 'protocol'
    level = 0

    def __init__(self, parameters, job_id):
        self.logger = logging.getLogger("dispatcher")
        self.poll_timeout = Timeout(self.name)
        self.parameters = None
        self.__errors__ = []
        self.parameters = parameters
        self.configured = False
        self.job_id = job_id

    @classmethod
    def select_all(cls, parameters):
        """
        Multiple protocols can apply to the same job, each with their own parameters.
        Jobs may have zero or more protocols selected.
        """
        candidates = cls.__subclasses__()  # pylint: disable=no-member
        return [(c, c.level) for c in candidates if c.accepts(parameters)]

    @property
    def errors(self):
        return self.__errors__

    @errors.setter
    def errors(self, error):
        self.__errors__.append(error)

    @property
    def valid(self):
        return len([x for x in self.errors if x]) == 0

    def set_up(self):
        raise LAVABug("'set_up' not implemented")

    def configure(self, device, job):  # pylint: disable=unused-argument
        self.configured = True

    def finalise_protocol(self, device=None):
        raise LAVABug("'finalise_protocol' not implemented")

    def check_timeout(self, duration, data):  # pylint: disable=unused-argument,no-self-use
        """
        Use if particular protocol calls can require a connection timeout
        larger than the default_connection_duration.
        :param duration: A minimum number of seconds
        :param data: the API call
        :return: True if checked, False if no limit is specified by the protocol.
        raises JobError if the API call is invalid.
        """
        return False

    def _api_select(self, data, action=None):  # pylint: disable=no-self-use
        if not data:
            return None
        raise LAVABug("'_api_select' not implemented")

    def __call__(self, *args, **kwargs):  # pylint: disable=no-self-use
        """ Makes the Protocol callable so that actions can send messages just using the protocol.
        This function may block until the specified API call returns. Some API calls may involve a
        substantial period of polling.
        :param args: arguments of the API call to make
        :return: A Python object containing the reply dict from the API call
        """
        # implementations will usually need a try: except: block around _api.select()
        return self._api_select(args, action=None)

    def collate(self, reply_dict, params_dict):  # pylint: disable=unused-argument,no-self-use
        return None class InfrastructureError(LAVAError):
    """
    Exceptions based on an error raised by a component of the
    test which is neither the LAVA dispatcher code nor the
    code being executed on the device under test. This includes
    errors arising from the device (like the arndale SD controller
    issue) and errors arising from the hardware to which the device
    is connected (serial console connection, ethernet switches or
    internet connection beyond the control of the device under test).

    Use LAVABug for errors arising from bugs in LAVA code.
    """
    error_help = "InfrastructureError: The Infrastructure is not working " \
                 "correctly. Please report this error to LAVA admins."
    error_type = "Infrastructure" class LAVABug(LAVAError):
    """
    An error that is raised when an un-expected error is catched. Only happen
    when a bug is encountered.
    """
    error_help = "LAVABug: This is probably a bug in LAVA, please report it."
    error_type = "Bug" class TestError(LAVAError):
    """
    An error in the operation of the test definition, e.g.
    in parsing measurements or commands which fail.
    Always ensure TestError is caught, logged and cleared. It is not fatal.
    """
    error_help = "TestError: A test failed to run, look at the error message."
    error_type = "Test" class JobError(LAVAError):
    """
    An Error arising from the information supplied as part of the TestJob
    e.g. HTTP404 on a file to be downloaded as part of the preparation of
    the TestJob or a download which results in a file which tar or gzip
    does not recognise.
    """
    error_help = "JobError: Your job cannot terminate cleanly."
    error_type = "Job" class Timeout(object):
    """
    The Timeout class is a declarative base which any actions can use. If an Action has
    a timeout, that timeout name and the duration will be output as part of the action
    description and the timeout is then exposed as a modifiable value via the device_type,
    device or even job inputs. (Some timeouts may be deemed "protected" which may not be
    altered by the job. All timeouts are subject to a hardcoded maximum duration which
    cannot be exceeded by device_type, device or job input, only by the Action initialising
    the timeout.
    If a connection is set, this timeout is used per pexpect operation on that connection.
    If a connection is not set, this timeout applies for the entire run function of the action.
    """
    def __init__(self, name, duration=ACTION_TIMEOUT, exception=JobError):
        self.name = name
        self.start = 0
        self.elapsed_time = -1
        self.duration = duration  # Actions can set timeouts higher than the clamp.
        self.exception = exception

    @classmethod
    def default_duration(cls):
        return ACTION_TIMEOUT

    @classmethod
    def parse(cls, data):
        """
        Parsed timeouts can be set in device configuration or device_type configuration
        and can therefore exceed the clamp.
        """
        if not isinstance(data, dict):
            raise ConfigurationError("Invalid timeout data")
        duration = datetime.timedelta(days=data.get('days', 0),
                                      hours=data.get('hours', 0),
                                      minutes=data.get('minutes', 0),
                                      seconds=data.get('seconds', 0))
        if not duration:
            return Timeout.default_duration()
        return int(duration.total_seconds())

    def _timed_out(self, signum, frame):  # pylint: disable=unused-argument
        duration = int(time.time() - self.start)
        raise self.exception("%s timed out after %s seconds" % (self.name, duration))

    @contextmanager
    def __call__(self, action_max_end_time=None):
        self.start = time.time()
        if action_max_end_time is None:
            # action_max_end_time is None when cleaning the pipeline after a
            # timeout.
            # In this case, the job timeout is not taken into account.
            max_end_time = self.start + self.duration
        else:
            max_end_time = min(action_max_end_time, self.start + self.duration)

        duration = int(max_end_time - self.start)
        if duration <= 0:
            # If duration is lower than 0, then the timeout should be raised now.
            # Calling signal.alarm in this case will only deactivate the alarm
            # (by passing 0 or the unsigned value).
            # Deactivate any previous alarm and set elapse_time prior to raise
            signal.alarm(0)
            self.elapsed_time = 0
            self._timed_out(None, None)

        signal.signal(signal.SIGALRM, self._timed_out)
        signal.alarm(duration)

        try:
            yield max_end_time
        finally:
            # clear the timeout alarm, the action has returned
            signal.alarm(0)
            self.elapsed_time = time.time() - self.start LAVA_LXC_TIMEOUT = 90 LXC_PATH = "/var/lib/lxc" UDEV_RULES_DIR = '/etc/udev/rules.d/' def lxc_path(dispatcher_config):
    """
    Returns LXC_PATH which is a constant, unless a dispatcher specific path is
    configured via lxc_path key in dispatcher_config.
    """
    try:
        return dispatcher_config["lxc_path"]
    except (KeyError, TypeError):
        return LXC_PATH
import re
import os
import yaml
import logging
import traceback
import subprocess
from lava_dispatcher.connection import Protocol
from lava_dispatcher.action import (
    InfrastructureError,
    LAVABug,
    TestError,
    JobError,
    Timeout,
)
from lava_dispatcher.utils.constants import (
    LAVA_LXC_TIMEOUT,
    LXC_PATH,
    UDEV_RULES_DIR,
)
from lava_dispatcher.utils.filesystem import lxc_path
# Copyright (C) 2015 Linaro Limited
#
# Author: Senthil Kumaran S <senthil.kumaran@linaro.org>
#
# This file is part of LAVA Dispatcher.
#
# LAVA Dispatcher is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LAVA Dispatcher is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along
# with this program; if not, see <http://www.gnu.org/licenses>.




class LxcProtocol(Protocol):  # pylint: disable=too-many-instance-attributes
    """
    Lxc API protocol.
    """
    name = "lava-lxc"

    def __init__(self, parameters, job_id):
        super(LxcProtocol, self).__init__(parameters, job_id)

        self.system_timeout = Timeout('system', LAVA_LXC_TIMEOUT)