1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6
7import common
8from autotest_lib.client.common_lib import priorities
9
10
11"""Module containing base class and methods for working with scheduler events.
12
13@var _SECTION_SUFFIX: suffix of config file sections that apply to derived
14                      classes of TimedEvent.
15"""
16
17
18_SECTION_SUFFIX = '_params'
19
20
21def SectionName(keyword):
22    """Generate a section name for a *Event config stanza."""
23    return keyword + _SECTION_SUFFIX
24
25
26def HonoredSection(section):
27    """Returns True if section is something _ParseConfig() might consume."""
28    return section.endswith(_SECTION_SUFFIX)
29
30
31def BuildName(board, type, milestone, manifest):
32    """Format a build name, given board, type, milestone, and manifest number.
33
34    @param board: board the manifest is for, e.g. x86-alex.
35    @param type: one of 'release', 'factory', or 'firmware'
36    @param milestone: (numeric) milestone the manifest was associated with.
37    @param manifest: manifest number, e.g. '2015.0.0'
38    @return a build name, e.g. 'x86-alex-release/R20-2015.0.0'
39    """
40    return '%s-%s/R%s-%s' % (board, type, milestone, manifest)
41
42
43class BaseEvent(object):
44    """Represents a supported scheduler event.
45
46    @var PRIORITY: The priority of suites kicked off by this event.
47    @var TIMEOUT: The max lifetime of suites kicked off by this event.
48
49    @var _keyword: the keyword/name of this event, e.g. new_build, nightly.
50    @var _mv: ManifestVersions instance used to query for new builds, etc.
51    @var _always_handle: whether to make ShouldHandle always return True.
52    @var _tasks: set of Task instances that run on this event.
53                 Use a set so that instances that encode logically equivalent
54                 Tasks get de-duped before we even try to schedule them.
55    """
56
57
58    PRIORITY = priorities.Priority.DEFAULT
59    TIMEOUT = 24  # Hours
60
61
62    @classmethod
63    def CreateFromConfig(cls, config, manifest_versions):
64        """Instantiate a cls object, options from |config|."""
65        return cls(manifest_versions, **cls._ParseConfig(config))
66
67
68    @classmethod
69    def _ParseConfig(cls, config):
70        """Parse config and return a dict of parameters for this event.
71
72        Uses cls.KEYWORD to determine which section to look at, and parses
73        the following options:
74          always_handle: If True, ShouldHandle() must always return True.
75
76        @param config: a ForgivingConfigParser instance.
77        """
78        section = SectionName(cls.KEYWORD)
79        return {'always_handle': config.getboolean(section, 'always_handle')}
80
81
82    def __init__(self, keyword, manifest_versions, always_handle):
83        """Constructor.
84
85        @param keyword: the keyword/name of this event, e.g. nightly.
86        @param manifest_versions: ManifestVersions instance to use for querying.
87        @param always_handle: If True, make ShouldHandle() always return True.
88        """
89        self._keyword = keyword
90        self._mv = manifest_versions
91        self._tasks = set()
92        self._always_handle = always_handle
93
94
95    @property
96    def keyword(self):
97        """Getter for private |self._keyword| property."""
98        return self._keyword
99
100
101    @property
102    def tasks(self):
103        return self._tasks
104
105
106    @tasks.setter
107    def tasks(self, iterable_of_tasks):
108        """Set the tasks property with an iterable.
109
110        @param iterable_of_tasks: list of Task instances that can fire on this.
111        """
112        self._tasks = set(iterable_of_tasks)
113
114
115    def Merge(self, to_merge):
116        """Merge this event with to_merge, changing all mutable properties.
117
118        keyword remains unchanged; the following take on values from to_merge:
119          _tasks
120          _mv
121          _always_handle
122
123        @param to_merge: A BaseEvent instance to merge into this instance.
124        """
125        self.tasks = to_merge.tasks
126        self._mv = to_merge._mv
127        self._always_handle = to_merge._always_handle
128
129
130    def Prepare(self):
131        """Perform any one-time setup that must occur before [Should]Handle().
132
133        Must be implemented by subclasses.
134        """
135        raise NotImplementedError()
136
137
138    def GetBranchBuildsForBoard(self, board):
139        """Get per-branch, per-board builds since last run of this event.
140
141        @param board: the board whose builds we want.
142        @return {branch: [build-name]}
143
144        Must be implemented by subclasses.
145        """
146        raise NotImplementedError()
147
148
149    def ShouldHandle(self):
150        """Returns True if this BaseEvent should be Handle()'d, False if not.
151
152        Must be extended by subclasses.
153        """
154        return self._always_handle
155
156
157    def UpdateCriteria(self):
158        """Updates internal state used to decide if this event ShouldHandle()
159
160        Must be implemented by subclasses.
161        """
162        raise NotImplementedError()
163
164
165    def FilterTasks(self):
166        """Filter the tasks to only return tasks should run now.
167
168        One use case is that Nightly task can run at each hour. The override of
169        this function in Nightly class will only return the tasks set to run in
170        current hour.
171
172        @return: A list of tasks can run now.
173        """
174        return list(self.tasks)
175
176
177    def Handle(self, scheduler, branch_builds, board, force=False):
178        """Runs all tasks in self._tasks that if scheduled, can be
179        successfully run.
180
181        @param scheduler: an instance of DedupingScheduler, as defined in
182                          deduping_scheduler.py
183        @param branch_builds: a dict mapping branch name to the build to
184                              install for that branch, e.g.
185                              {'R18': ['x86-alex-release/R18-1655.0.0'],
186                               'R19': ['x86-alex-release/R19-2077.0.0']
187                               'factory': ['x86-alex-factory/R19-2077.0.5']}
188        @param board: the board against which to Run() all of self._tasks.
189        @param force: Tell every Task to always Run().
190        """
191        logging.info('Handling %s for %s', self.keyword, board)
192        # we need to iterate over an immutable copy of self._tasks
193        for task in self.FilterTasks():
194            if task.AvailableHosts(scheduler, board):
195                if not task.Run(scheduler, branch_builds, board, force,
196                                self._mv):
197                    self._tasks.remove(task)
198            elif task.ShouldHaveAvailableHosts():
199                logging.warning('Skipping %s on %s, due to lack of hosts.',
200                                task, board)
201