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