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
17import os
18from unittest import mock
19
20from twisted.internet import defer
21from twisted.trial import unittest
22
23from buildbot.machine.generic import LocalWakeAction
24from buildbot.machine.generic import LocalWOLAction
25from buildbot.machine.generic import RemoteSshSuspendAction
26from buildbot.machine.generic import RemoteSshWakeAction
27from buildbot.machine.generic import RemoteSshWOLAction
28from buildbot.test.fake.private_tempdir import MockPrivateTemporaryDirectory
29from buildbot.test.util import config
30from buildbot.test.util.misc import TestReactorMixin
31from buildbot.test.util.runprocess import ExpectMaster
32from buildbot.test.util.runprocess import MasterRunProcessMixin
33
34
35class FakeManager:
36
37    def __init__(self, reactor, basedir=None):
38        self.master = mock.Mock()
39        self.master.basedir = basedir
40        self.master.reactor = reactor
41
42    def renderSecrets(self, args):
43        return defer.succeed(args)
44
45
46class TestActions(MasterRunProcessMixin, config.ConfigErrorsMixin, TestReactorMixin,
47                  unittest.TestCase):
48    def setUp(self):
49        self.setUpTestReactor()
50        self.setup_master_run_process()
51
52    def tearDown(self):
53        pass
54
55    @defer.inlineCallbacks
56    def test_local_wake_action(self):
57        self.expect_commands(
58            ExpectMaster(['cmd', 'arg1', 'arg2'])
59            .exit(1),
60
61            ExpectMaster(['cmd', 'arg1', 'arg2'])
62            .exit(0),
63        )
64
65        manager = FakeManager(self.reactor)
66        action = LocalWakeAction(['cmd', 'arg1', 'arg2'])
67        self.assertFalse((yield action.perform(manager)))
68        self.assertTrue((yield action.perform(manager)))
69        self.assert_all_commands_ran()
70
71    def test_local_wake_action_command_not_list(self):
72        with self.assertRaisesConfigError('command parameter must be a list'):
73            LocalWakeAction('not-list')
74
75    @defer.inlineCallbacks
76    def test_local_wol_action(self):
77        self.expect_commands(
78            ExpectMaster(['wol', '00:11:22:33:44:55'])
79            .exit(1),
80
81            ExpectMaster(['wakeonlan', '00:11:22:33:44:55'])
82            .exit(0),
83        )
84
85        manager = FakeManager(self.reactor)
86        action = LocalWOLAction('00:11:22:33:44:55', wolBin='wol')
87        self.assertFalse((yield action.perform(manager)))
88
89        action = LocalWOLAction('00:11:22:33:44:55')
90        self.assertTrue((yield action.perform(manager)))
91        self.assert_all_commands_ran()
92
93    @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
94                new_callable=MockPrivateTemporaryDirectory)
95    @mock.patch('buildbot.util.misc.writeLocalFile')
96    @defer.inlineCallbacks
97    def test_remote_ssh_wake_action_no_keys(self, write_local_file_mock,
98                                            temp_dir_mock):
99        self.expect_commands(
100            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'remotebin', 'arg1'])
101            .exit(1),
102
103            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'remotebin', 'arg1'])
104            .exit(0),
105        )
106
107        manager = FakeManager(self.reactor)
108        action = RemoteSshWakeAction('remote_host', ['remotebin', 'arg1'])
109        self.assertFalse((yield action.perform(manager)))
110        self.assertTrue((yield action.perform(manager)))
111        self.assert_all_commands_ran()
112
113        self.assertEqual(temp_dir_mock.dirs, [])
114        write_local_file_mock.assert_not_called()
115
116    @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
117                new_callable=MockPrivateTemporaryDirectory)
118    @mock.patch('buildbot.util.misc.writeLocalFile')
119    @defer.inlineCallbacks
120    def test_remote_ssh_wake_action_with_keys(self, write_local_file_mock,
121                                              temp_dir_mock):
122        temp_dir_path = os.path.join('path-to-master', 'ssh-@@@')
123        ssh_key_path = os.path.join(temp_dir_path, 'ssh-key')
124        ssh_known_hosts_path = os.path.join(temp_dir_path, 'ssh-known-hosts')
125
126        self.expect_commands(
127            ExpectMaster(['ssh', '-o', 'BatchMode=yes', '-i', ssh_key_path,
128                          '-o', 'UserKnownHostsFile={0}'.format(ssh_known_hosts_path),
129                          'remote_host', 'remotebin', 'arg1'])
130            .exit(0),
131        )
132
133        manager = FakeManager(self.reactor, 'path-to-master')
134        action = RemoteSshWakeAction('remote_host', ['remotebin', 'arg1'],
135                                     sshKey='ssh_key',
136                                     sshHostKey='ssh_host_key')
137        self.assertTrue((yield action.perform(manager)))
138
139        self.assert_all_commands_ran()
140
141        self.assertEqual(temp_dir_mock.dirs,
142                         [(temp_dir_path, 0o700)])
143
144        self.assertSequenceEqual(write_local_file_mock.call_args_list, [
145            mock.call(ssh_key_path, 'ssh_key', mode=0o400),
146            mock.call(ssh_known_hosts_path, '* ssh_host_key'),
147        ])
148
149    def test_remote_ssh_wake_action_sshBin_not_str(self):
150        with self.assertRaisesConfigError('sshBin parameter must be a string'):
151            RemoteSshWakeAction('host', ['cmd'], sshBin=123)
152
153    def test_remote_ssh_wake_action_host_not_str(self):
154        with self.assertRaisesConfigError('host parameter must be a string'):
155            RemoteSshWakeAction(123, ['cmd'])
156
157    def test_remote_ssh_wake_action_command_not_list(self):
158        with self.assertRaisesConfigError(
159                'remoteCommand parameter must be a list'):
160            RemoteSshWakeAction('host', 'cmd')
161
162    @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
163                new_callable=MockPrivateTemporaryDirectory)
164    @mock.patch('buildbot.util.misc.writeLocalFile')
165    @defer.inlineCallbacks
166    def test_remote_ssh_wol_action_no_keys(self, write_local_file_mock,
167                                           temp_dir_mock):
168        self.expect_commands(
169            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'wakeonlan',
170                          '00:11:22:33:44:55'])
171            .exit(0),
172
173            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'wolbin',
174                          '00:11:22:33:44:55'])
175            .exit(0),
176        )
177
178        manager = FakeManager(self.reactor)
179        action = RemoteSshWOLAction('remote_host', '00:11:22:33:44:55')
180        self.assertTrue((yield action.perform(manager)))
181
182        action = RemoteSshWOLAction('remote_host', '00:11:22:33:44:55',
183                                    wolBin='wolbin')
184        self.assertTrue((yield action.perform(manager)))
185        self.assert_all_commands_ran()
186
187        self.assertEqual(temp_dir_mock.dirs, [])
188        write_local_file_mock.assert_not_called()
189
190    @mock.patch('buildbot.util.private_tempdir.PrivateTemporaryDirectory',
191                new_callable=MockPrivateTemporaryDirectory)
192    @mock.patch('buildbot.util.misc.writeLocalFile')
193    @defer.inlineCallbacks
194    def test_remote_ssh_suspend_action_no_keys(self, write_local_file_mock,
195                                               temp_dir_mock):
196        self.expect_commands(
197            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'systemctl', 'suspend'])
198            .exit(0),
199
200            ExpectMaster(['ssh', '-o', 'BatchMode=yes', 'remote_host', 'dosuspend', 'arg1'])
201            .exit(0),
202        )
203
204        manager = FakeManager(self.reactor)
205        action = RemoteSshSuspendAction('remote_host')
206        self.assertTrue((yield action.perform(manager)))
207
208        action = RemoteSshSuspendAction('remote_host',
209                                        remoteCommand=['dosuspend', 'arg1'])
210        self.assertTrue((yield action.perform(manager)))
211        self.assert_all_commands_ran()
212
213        self.assertEqual(temp_dir_mock.dirs, [])
214        write_local_file_mock.assert_not_called()
215