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
16import os
17import stat
18
19from twisted.internet import defer
20from twisted.python import log
21from zope.interface import implementer
22
23from buildbot import config
24from buildbot.interfaces import IMachineAction
25from buildbot.machine.latent import AbstractLatentMachine
26from buildbot.util import misc
27from buildbot.util import private_tempdir
28from buildbot.util import runprocess
29from buildbot.util.git import getSshArgsForKeys
30from buildbot.util.git import getSshKnownHostsContents
31
32
33class GenericLatentMachine(AbstractLatentMachine):
34
35    def checkConfig(self, name, start_action, stop_action, **kwargs):
36        super().checkConfig(name, **kwargs)
37
38        for action, arg_name in [(start_action, 'start_action'),
39                                 (stop_action, 'stop_action')]:
40            if not IMachineAction.providedBy(action):
41                msg = "{} of {} does not implement required " \
42                      "interface".format(arg_name, self.name)
43                raise Exception(msg)
44
45    @defer.inlineCallbacks
46    def reconfigService(self, name, start_action, stop_action, **kwargs):
47        yield super().reconfigService(name, **kwargs)
48        self.start_action = start_action
49        self.stop_action = stop_action
50
51    def start_machine(self):
52        return self.start_action.perform(self)
53
54    def stop_machine(self):
55        return self.stop_action.perform(self)
56
57
58@defer.inlineCallbacks
59def runProcessLogFailures(reactor, args, expectedCode=0):
60    code, stdout, stderr = yield runprocess.run_process(reactor, args)
61    if code != expectedCode:
62        log.err(('Got unexpected return code when running {}: '
63                 'code: {}, stdout: {}, stderr: {}').format(args, code, stdout, stderr))
64        return False
65    return True
66
67
68class _LocalMachineActionMixin:
69    def setupLocal(self, command):
70        if not isinstance(command, list):
71            config.error('command parameter must be a list')
72        self._command = command
73
74    @defer.inlineCallbacks
75    def perform(self, manager):
76        args = yield manager.renderSecrets(self._command)
77        return (yield runProcessLogFailures(manager.master.reactor, args))
78
79
80class _SshActionMixin:
81    def setupSsh(self, sshBin, host, remoteCommand, sshKey=None,
82                 sshHostKey=None):
83        if not isinstance(sshBin, str):
84            config.error('sshBin parameter must be a string')
85        if not isinstance(host, str):
86            config.error('host parameter must be a string')
87        if not isinstance(remoteCommand, list):
88            config.error('remoteCommand parameter must be a list')
89
90        self._sshBin = sshBin
91        self._host = host
92        self._remoteCommand = remoteCommand
93        self._sshKey = sshKey
94        self._sshHostKey = sshHostKey
95
96    @defer.inlineCallbacks
97    def _performImpl(self, manager, key_path, known_hosts_path):
98        args = getSshArgsForKeys(key_path, known_hosts_path)
99        args.append((yield manager.renderSecrets(self._host)))
100        args.extend((yield manager.renderSecrets(self._remoteCommand)))
101        return (yield runProcessLogFailures(manager.master.reactor, [self._sshBin] + args))
102
103    @defer.inlineCallbacks
104    def _prepareSshKeys(self, manager, temp_dir_path):
105        key_path = None
106        if self._sshKey is not None:
107            ssh_key_data = yield manager.renderSecrets(self._sshKey)
108
109            key_path = os.path.join(temp_dir_path, 'ssh-key')
110            misc.writeLocalFile(key_path, ssh_key_data,
111                                mode=stat.S_IRUSR)
112
113        known_hosts_path = None
114        if self._sshHostKey is not None:
115            ssh_host_key_data = yield manager.renderSecrets(self._sshHostKey)
116            ssh_host_key_data = getSshKnownHostsContents(ssh_host_key_data)
117
118            known_hosts_path = os.path.join(temp_dir_path, 'ssh-known-hosts')
119            misc.writeLocalFile(known_hosts_path, ssh_host_key_data)
120
121        return (key_path, known_hosts_path)
122
123    @defer.inlineCallbacks
124    def perform(self, manager):
125        if self._sshKey is not None or self._sshHostKey is not None:
126            with private_tempdir.PrivateTemporaryDirectory(
127                    prefix='ssh-', dir=manager.master.basedir) as temp_dir:
128
129                key_path, hosts_path = yield self._prepareSshKeys(manager,
130                                                                  temp_dir)
131
132                ret = yield self._performImpl(manager, key_path, hosts_path)
133        else:
134            ret = yield self._performImpl(manager, None, None)
135        return ret
136
137
138@implementer(IMachineAction)
139class LocalWakeAction(_LocalMachineActionMixin):
140    def __init__(self, command):
141        self.setupLocal(command)
142
143
144class LocalWOLAction(LocalWakeAction):
145    def __init__(self, wakeMac, wolBin='wakeonlan'):
146        LocalWakeAction.__init__(self, [wolBin, wakeMac])
147
148
149@implementer(IMachineAction)
150class RemoteSshWakeAction(_SshActionMixin):
151    def __init__(self, host, remoteCommand, sshBin='ssh',
152                 sshKey=None, sshHostKey=None):
153        self.setupSsh(sshBin, host, remoteCommand,
154                      sshKey=sshKey, sshHostKey=sshHostKey)
155
156
157class RemoteSshWOLAction(RemoteSshWakeAction):
158    def __init__(self, host, wakeMac, wolBin='wakeonlan', sshBin='ssh',
159                 sshKey=None, sshHostKey=None):
160        RemoteSshWakeAction.__init__(self, host, [wolBin, wakeMac],
161                                     sshBin=sshBin,
162                                     sshKey=sshKey, sshHostKey=sshHostKey)
163
164
165@implementer(IMachineAction)
166class RemoteSshSuspendAction(_SshActionMixin):
167    def __init__(self, host, remoteCommand=None, sshBin='ssh',
168                 sshKey=None, sshHostKey=None):
169        if remoteCommand is None:
170            remoteCommand = ['systemctl', 'suspend']
171        self.setupSsh(sshBin, host, remoteCommand,
172                      sshKey=sshKey, sshHostKey=sshHostKey)
173