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
9from autotest_lib.client.common_lib.cros import dev_server
10from autotest_lib.server import utils
11
12
13"""Module containing base class and methods for working with scheduler events.
14
15@var _SECTION_SUFFIX: suffix of config file sections that apply to derived
16                      classes of TimedEvent.
17"""
18
19
20_SECTION_SUFFIX = '_params'
21
22# Pattern of latest Launch Control build for a branch and a target.
23_LATEST_LAUNCH_CONTROL_BUILD_FMT = '%s/%s/LATEST'
24
25def SectionName(keyword):
26    """Generate a section name for a *Event config stanza.
27
28    @param keyword: Name of the event, e.g., nightly, weekly etc.
29    """
30    return keyword + _SECTION_SUFFIX
31
32
33def HonoredSection(section):
34    """Returns True if section is something _ParseConfig() might consume.
35
36    @param section: Name of the config section.
37    """
38    return section.endswith(_SECTION_SUFFIX)
39
40
41def BuildName(board, type, milestone, manifest):
42    """Format a build name, given board, type, milestone, and manifest number.
43
44    @param board: board the manifest is for, e.g. x86-alex.
45    @param type: one of 'release', 'factory', or 'firmware'
46    @param milestone: (numeric) milestone the manifest was associated with.
47    @param manifest: manifest number, e.g. '2015.0.0'
48    @return a build name, e.g. 'x86-alex-release/R20-2015.0.0'
49    """
50    return '%s-%s/R%s-%s' % (board, type, milestone, manifest)
51
52
53class BaseEvent(object):
54    """Represents a supported scheduler event.
55
56    @var PRIORITY: The priority of suites kicked off by this event.
57    @var TIMEOUT: The max lifetime of suites kicked off by this event.
58
59    @var _keyword: the keyword/name of this event, e.g. new_build, nightly.
60    @var _mv: ManifestVersions instance used to query for new builds, etc.
61    @var _always_handle: whether to make ShouldHandle always return True.
62    @var _tasks: set of Task instances that run on this event.
63                 Use a set so that instances that encode logically equivalent
64                 Tasks get de-duped before we even try to schedule them.
65    """
66
67
68    PRIORITY = priorities.Priority.DEFAULT
69    TIMEOUT = 24  # Hours
70
71
72    @classmethod
73    def CreateFromConfig(cls, config, manifest_versions):
74        """Instantiate a cls object, options from |config|.
75
76        @param config: A ForgivingConfigParser instance.
77        @param manifest_versions: ManifestVersions instance used to query for
78                new builds, etc.
79        """
80        return cls(manifest_versions, **cls._ParseConfig(config))
81
82
83    @classmethod
84    def _ParseConfig(cls, config):
85        """Parse config and return a dict of parameters for this event.
86
87        Uses cls.KEYWORD to determine which section to look at, and parses
88        the following options:
89          always_handle: If True, ShouldHandle() must always return True.
90
91        @param config: a ForgivingConfigParser instance.
92        """
93        section = SectionName(cls.KEYWORD)
94        return {'always_handle': config.getboolean(section, 'always_handle')}
95
96
97    def __init__(self, keyword, manifest_versions, always_handle):
98        """Constructor.
99
100        @param keyword: the keyword/name of this event, e.g. nightly.
101        @param manifest_versions: ManifestVersions instance to use for querying.
102        @param always_handle: If True, make ShouldHandle() always return True.
103        """
104        self._keyword = keyword
105        self._mv = manifest_versions
106        self._tasks = set()
107        self._always_handle = always_handle
108
109
110    @property
111    def keyword(self):
112        """Getter for private |self._keyword| property."""
113        return self._keyword
114
115
116    @property
117    def tasks(self):
118        """Getter for private |self._tasks| property."""
119        return self._tasks
120
121
122    @property
123    def launch_control_branches_targets(self):
124        """Get a dict of branch:targets for Launch Control from all tasks.
125
126        branch: Name of a Launch Control branch.
127        targets: A list of targets for the given branch.
128        """
129        branches = {}
130        for task in self._tasks:
131            for branch in task.launch_control_branches:
132                branches.setdefault(branch, []).extend(
133                        task.launch_control_targets)
134        return branches
135
136
137    @tasks.setter
138    def tasks(self, iterable_of_tasks):
139        """Set the tasks property with an iterable.
140
141        @param iterable_of_tasks: list of Task instances that can fire on this.
142        """
143        self._tasks = set(iterable_of_tasks)
144
145
146    def Merge(self, to_merge):
147        """Merge this event with to_merge, changing all mutable properties.
148
149        keyword remains unchanged; the following take on values from to_merge:
150          _tasks
151          _mv
152          _always_handle
153
154        @param to_merge: A BaseEvent instance to merge into this instance.
155        """
156        self.tasks = to_merge.tasks
157        self._mv = to_merge._mv
158        self._always_handle = to_merge._always_handle
159
160
161    def Prepare(self):
162        """Perform any one-time setup that must occur before [Should]Handle().
163
164        Must be implemented by subclasses.
165        """
166        raise NotImplementedError()
167
168
169    def GetBranchBuildsForBoard(self, board):
170        """Get per-branch, per-board builds since last run of this event.
171
172        @param board: the board whose builds we want.
173        @return {branch: [build-name]}
174
175        Must be implemented by subclasses.
176        """
177        raise NotImplementedError()
178
179
180    def GetLaunchControlBuildsForBoard(self, board):
181        """Get per-branch, per-board builds since last run of this event.
182
183        @param board: the board whose builds we want.
184
185        @return: A list of Launch Control builds for the given board, e.g.,
186                ['git_mnc_release/shamu-eng/123',
187                 'git_mnc_release/shamu-eng/124'].
188
189        Must be implemented by subclasses.
190        """
191        raise NotImplementedError()
192
193
194    def ShouldHandle(self):
195        """Returns True if this BaseEvent should be Handle()'d, False if not.
196
197        Must be extended by subclasses.
198        """
199        return self._always_handle
200
201
202    def UpdateCriteria(self):
203        """Updates internal state used to decide if this event ShouldHandle()
204
205        Must be implemented by subclasses.
206        """
207        raise NotImplementedError()
208
209
210    def FilterTasks(self):
211        """Filter the tasks to only return tasks should run now.
212
213        One use case is that Nightly task can run at each hour. The override of
214        this function in Nightly class will only return the tasks set to run in
215        current hour.
216
217        @return: A list of tasks can run now.
218        """
219        return list(self.tasks)
220
221
222    def Handle(self, scheduler, branch_builds, board, force=False,
223               launch_control_builds=None):
224        """Runs all tasks in self._tasks that if scheduled, can be
225        successfully run.
226
227        @param scheduler: an instance of DedupingScheduler, as defined in
228                          deduping_scheduler.py
229        @param branch_builds: a dict mapping branch name to the build to
230                              install for that branch, e.g.
231                              {'R18': ['x86-alex-release/R18-1655.0.0'],
232                               'R19': ['x86-alex-release/R19-2077.0.0']
233                               'factory': ['x86-alex-factory/R19-2077.0.5']}
234        @param board: the board against which to Run() all of self._tasks.
235        @param force: Tell every Task to always Run().
236        @param launch_control_builds: A list of Launch Control builds.
237        """
238        logging.info('Handling %s for %s', self.keyword, board)
239        # we need to iterate over an immutable copy of self._tasks
240        tasks = list(self.tasks) if force else self.FilterTasks()
241        for task in tasks:
242            if task.AvailableHosts(scheduler, board):
243                if not task.Run(scheduler, branch_builds, board, force,
244                                self._mv, launch_control_builds):
245                    self._tasks.remove(task)
246            elif task.ShouldHaveAvailableHosts():
247                logging.warning('Skipping %s on %s, due to lack of hosts.',
248                                task, board)
249
250
251    def _LatestLaunchControlBuilds(self, board):
252        """Get latest per-branch, per-board builds.
253
254        @param board: the board whose builds we want, e.g., shamu.
255
256        @return: A list of Launch Control builds for the given board, e.g.,
257                ['git_mnc_release/shamu-eng/123',
258                 'git_mnc_release/shamu-eng/124'].
259        """
260        # Translate board name to the actual board name in build target.
261        board = utils.ANDROID_BOARD_TO_TARGET_MAP.get(board, board)
262        # Pick a random devserver based on tick, this is to help load balancing
263        # across all devservers.
264        devserver = dev_server.AndroidBuildServer.random()
265        builds = []
266        for branch, targets in self.launch_control_branches_targets.items():
267            # targets is a list of Launch Control targets, e.g., shamu-eng.
268            # The first part should match the board name.
269            match_targets = [
270                    t for t in targets
271                    if board == utils.parse_launch_control_target(t)[0]]
272            for target in match_targets:
273                latest_build = (_LATEST_LAUNCH_CONTROL_BUILD_FMT %
274                                (branch, target))
275                try:
276                    builds.append(devserver.translate(latest_build))
277                except Exception as e:
278                    logging.warning('Error happens in translating %s on %s: %s',
279                                    latest_build, devserver.hostname, str(e))
280
281        return builds
282