1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16
17from twisted.internet import defer
18from twisted.python import log
19
20from buildbot.data import base
21from buildbot.data import resultspec
22from buildbot.data import types
23from buildbot.process.results import RETRY
24from buildbot.util import epoch2datetime
25
26# time, in minutes, after which a master that hasn't checked in will be
27# marked as inactive
28EXPIRE_MINUTES = 10
29
30
31def _db2data(master):
32    return dict(masterid=master['id'],
33                name=master['name'],
34                active=master['active'],
35                last_active=master['last_active'])
36
37
38class MasterEndpoint(base.Endpoint):
39
40    isCollection = False
41    pathPatterns = """
42        /masters/n:masterid
43        /builders/n:builderid/masters/n:masterid
44    """
45
46    @defer.inlineCallbacks
47    def get(self, resultSpec, kwargs):
48        # if a builder is given, only return the master if it's associated with
49        # this builder
50        if 'builderid' in kwargs:
51            builder = yield self.master.db.builders.getBuilder(
52                builderid=kwargs['builderid'])
53            if not builder or kwargs['masterid'] not in builder['masterids']:
54                return None
55        m = yield self.master.db.masters.getMaster(kwargs['masterid'])
56        return _db2data(m) if m else None
57
58
59class MastersEndpoint(base.Endpoint):
60
61    isCollection = True
62    pathPatterns = """
63        /masters
64        /builders/n:builderid/masters
65    """
66    rootLinkName = 'masters'
67
68    @defer.inlineCallbacks
69    def get(self, resultSpec, kwargs):
70        masterlist = yield self.master.db.masters.getMasters()
71        if 'builderid' in kwargs:
72            builder = yield self.master.db.builders.getBuilder(
73                builderid=kwargs['builderid'])
74            if builder:
75                masterids = set(builder['masterids'])
76                masterlist = [m for m in masterlist if m['id'] in masterids]
77            else:
78                masterlist = []
79        return [_db2data(m) for m in masterlist]
80
81
82class Master(base.ResourceType):
83
84    name = "master"
85    plural = "masters"
86    endpoints = [MasterEndpoint, MastersEndpoint]
87    eventPathPatterns = """
88        /masters/:masterid
89    """
90    keyField = "masterid"
91    subresources = ["Builder"]
92
93    class EntityType(types.Entity):
94        masterid = types.Integer()
95        name = types.String()
96        active = types.Boolean()
97        last_active = types.DateTime()
98    entityType = EntityType(name, 'Master')
99
100    @base.updateMethod
101    @defer.inlineCallbacks
102    def masterActive(self, name, masterid):
103        activated = yield self.master.db.masters.setMasterState(
104            masterid=masterid, active=True)
105        if activated:
106            self.produceEvent(
107                dict(masterid=masterid, name=name, active=True),
108                'started')
109
110    @base.updateMethod
111    @defer.inlineCallbacks
112    def expireMasters(self, forceHouseKeeping=False):
113        too_old = epoch2datetime(self.master.reactor.seconds() - 60 * EXPIRE_MINUTES)
114        masters = yield self.master.db.masters.getMasters()
115        for m in masters:
116            if m['last_active'] is not None and m['last_active'] >= too_old:
117                continue
118
119            # mark the master inactive, and send a message on its behalf
120            deactivated = yield self.master.db.masters.setMasterState(
121                masterid=m['id'], active=False)
122            if deactivated:
123                yield self._masterDeactivated(m['id'], m['name'])
124            elif forceHouseKeeping:
125                yield self._masterDeactivatedHousekeeping(m['id'], m['name'])
126
127    @base.updateMethod
128    @defer.inlineCallbacks
129    def masterStopped(self, name, masterid):
130        deactivated = yield self.master.db.masters.setMasterState(
131            masterid=masterid, active=False)
132        if deactivated:
133            yield self._masterDeactivated(masterid, name)
134
135    @defer.inlineCallbacks
136    def _masterDeactivatedHousekeeping(self, masterid, name):
137        log.msg("doing housekeeping for master {} {}".format(masterid, name))
138
139        # common code for deactivating a master
140        yield self.master.data.rtypes.worker._masterDeactivated(
141            masterid=masterid)
142        yield self.master.data.rtypes.builder._masterDeactivated(
143            masterid=masterid)
144        yield self.master.data.rtypes.scheduler._masterDeactivated(
145            masterid=masterid)
146        yield self.master.data.rtypes.changesource._masterDeactivated(
147            masterid=masterid)
148
149        # for each build running on that instance..
150        builds = yield self.master.data.get(('builds',),
151                                            filters=[resultspec.Filter('masterid', 'eq',
152                                                                       [masterid]),
153                                                     resultspec.Filter('complete', 'eq', [False])])
154        for build in builds:
155            # stop any running steps..
156            steps = yield self.master.data.get(
157                ('builds', build['buildid'], 'steps'),
158                filters=[resultspec.Filter('results', 'eq', [None])])
159            for step in steps:
160                # finish remaining logs for those steps..
161                logs = yield self.master.data.get(
162                    ('steps', step['stepid'], 'logs'),
163                    filters=[resultspec.Filter('complete', 'eq',
164                                               [False])])
165                for _log in logs:
166                    yield self.master.data.updates.finishLog(
167                        logid=_log['logid'])
168                yield self.master.data.updates.finishStep(
169                    stepid=step['stepid'], results=RETRY, hidden=False)
170            # then stop the build itself
171            yield self.master.data.updates.finishBuild(
172                buildid=build['buildid'], results=RETRY)
173
174        # unclaim all of the build requests owned by the deactivated instance
175        buildrequests = yield self.master.db.buildrequests.getBuildRequests(
176            complete=False, claimed=masterid)
177        yield self.master.db.buildrequests.unclaimBuildRequests(
178            brids=[br['buildrequestid'] for br in buildrequests])
179
180    @defer.inlineCallbacks
181    def _masterDeactivated(self, masterid, name):
182        yield self._masterDeactivatedHousekeeping(masterid, name)
183
184        self.produceEvent(
185            dict(masterid=masterid, name=name, active=False),
186            'stopped')
187