1
from contextlib import contextmanager
2
from unittest import TestCase
10
4
from assess_recovery import (
13
delete_controller_members,
16
restore_missing_state_server,
18
8
from jujupy import (
25
from tests.test_jujupy import fake_juju_client
26
from utility import JujuAssertionError
29
17
class TestParseArgs(TestCase):
31
19
def test_parse_args(self):
32
args = parse_args(['an-env', '/juju', 'log', 'tmp-env'])
33
self.assertEqual(args.env, 'an-env')
34
self.assertEqual(args.juju_bin, '/juju')
35
self.assertEqual(args.logs, 'log')
36
self.assertEqual(args.temp_env_name, 'tmp-env')
37
self.assertEqual(args.charm_series, '')
20
args = parse_args(['foo', 'bar', 'baz'])
21
self.assertEqual(args.juju_path, 'foo')
22
self.assertEqual(args.env_name, 'bar')
23
self.assertEqual(args.logs, 'baz')
24
self.assertEqual(args.charm_prefix, '')
38
25
self.assertEqual(args.strategy, 'backup')
39
self.assertEqual(args.verbose, logging.INFO)
40
26
self.assertEqual(args.debug, False)
41
self.assertIs(args.agent_stream, None)
42
self.assertIs(args.series, None)
44
28
def test_parse_args_ha(self):
45
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha'])
29
args = parse_args(['foo', 'bar', 'baz', '--ha'])
46
30
self.assertEqual(args.strategy, 'ha')
48
32
def test_parse_args_ha_backup(self):
49
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha-backup'])
33
args = parse_args(['foo', 'bar', 'baz', '--ha-backup'])
50
34
self.assertEqual(args.strategy, 'ha-backup')
52
36
def test_parse_args_backup(self):
53
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha',
37
args = parse_args(['foo', 'bar', 'baz', '--ha', '--backup'])
55
38
self.assertEqual(args.strategy, 'backup')
57
def test_parse_args_charm_series(self):
58
args = parse_args(['an-env', '/juju', 'log', 'tmp-env',
59
'--charm-series', 'qux'])
60
self.assertEqual(args.charm_series, 'qux')
63
class TestAssessRecovery(TestCase):
66
def assess_recovery_cxt(self, client):
69
def terminate(env, instance_ids):
70
model = client._backend.controller_state.controller_model
71
for instance_id in instance_ids:
72
model.remove_state_server(instance_id)
74
with patch('assess_recovery.wait_for_state_server_to_shutdown',
76
with patch('assess_recovery.terminate_instances',
77
side_effect=terminate):
78
with patch('deploy_stack.wait_for_port', autospec=True):
79
with patch('assess_recovery.restore_present_state_server',
81
with patch('assess_recovery.check_token',
83
side_effect=['Token: One', 'Token: Two']):
84
with patch('assess_recovery.show_controller',
86
return_value='controller'):
89
def test_backup(self):
90
client = fake_juju_client()
91
bs_manager = Mock(client=client, known_hosts={})
92
with self.assess_recovery_cxt(client):
93
assess_recovery(bs_manager, 'backup', 'trusty')
96
client = fake_juju_client()
97
bs_manager = Mock(client=client, known_hosts={})
98
with self.assess_recovery_cxt(client):
99
assess_recovery(bs_manager, 'ha', 'trusty')
101
def test_ha_backup(self):
102
client = fake_juju_client()
103
bs_manager = Mock(client=client, known_hosts={})
104
with self.assess_recovery_cxt(client):
105
assess_recovery(bs_manager, 'ha-backup', 'trusty')
107
def test_controller_model_backup(self):
108
client = fake_juju_client()
109
bs_manager = Mock(client=client, known_hosts={})
110
with self.assess_recovery_cxt(client):
111
assess_recovery(bs_manager, 'backup', 'trusty')
113
def test_controller_model_ha(self):
114
client = fake_juju_client()
115
bs_manager = Mock(client=client, known_hosts={})
116
with self.assess_recovery_cxt(client):
117
assess_recovery(bs_manager, 'ha', 'trusty')
119
def test_controller_model_ha_backup(self):
120
client = fake_juju_client()
121
bs_manager = Mock(client=client, known_hosts={})
122
with self.assess_recovery_cxt(client):
123
assess_recovery(bs_manager, 'ha-backup', 'trusty')
126
@patch('assess_recovery.configure_logging', autospec=True)
127
@patch('assess_recovery.BootstrapManager.booted_context', autospec=True)
128
class TestMain(FakeHomeTestCase):
130
def test_main(self, mock_bc, mock_cl):
131
client = Mock(spec=['is_jes_enabled', 'version'])
132
client.version = '1.25.5'
133
with patch('deploy_stack.client_from_config',
134
return_value=client) as mock_c:
135
with patch('assess_recovery.assess_recovery',
136
autospec=True) as mock_assess:
137
main(['an-env', '/juju', 'log_dir', 'tmp-env', '--backup',
138
'--charm-series', 'a-series'])
139
mock_cl.assert_called_once_with(logging.INFO)
140
mock_c.assert_called_once_with('an-env', '/juju', debug=False,
142
self.assertEqual(mock_bc.call_count, 1)
143
self.assertEqual(mock_assess.call_count, 1)
144
bs_manager, strategy, series = mock_assess.call_args[0]
145
self.assertEqual((bs_manager.client, strategy, series),
146
(client, 'backup', 'a-series'))
148
def test_error(self, mock_bc, mock_cl):
149
class FakeError(Exception):
150
"""Custom exception to validate error handling."""
151
error = FakeError('An error during test')
152
client = Mock(spec=['is_jes_enabled', 'version'])
153
client.version = '2.0.0'
154
with patch('deploy_stack.client_from_config',
155
return_value=client) as mock_c:
156
with patch('assess_recovery.parse_new_state_server_from_error',
157
autospec=True, return_value='a-host') as mock_pe:
158
with patch('assess_recovery.assess_recovery', autospec=True,
159
side_effect=error) as mock_assess:
160
with self.assertRaises(FakeError) as ctx:
161
main(['an-env', '/juju', 'log_dir', 'tmp-env', '--ha',
162
'--verbose', '--charm-series', 'a-series'])
163
self.assertIs(ctx.exception, error)
164
mock_cl.assert_called_once_with(logging.DEBUG)
165
mock_c.assert_called_once_with('an-env', '/juju', debug=False,
167
mock_pe.assert_called_once_with(error)
168
self.assertEqual(mock_bc.call_count, 1)
169
self.assertEqual(mock_assess.call_count, 1)
170
bs_manager, strategy, series = mock_assess.call_args[0]
171
self.assertEqual((bs_manager.client, strategy, series),
172
(client, 'ha', 'a-series'))
173
self.assertEqual(bs_manager.known_hosts['0'], 'a-host')
40
def test_parse_args_charm_prefix(self):
41
args = parse_args(['foo', 'bar', 'baz', '--charm-prefix', 'qux'])
42
self.assertEqual(args.charm_prefix, 'qux')
44
def test_parse_args_debug(self):
45
args = parse_args(['foo', 'bar', 'baz', '--debug'])
46
self.assertEqual(args.debug, True)
48
def test_parse_args_temp_env_name(self):
49
args = parse_args(['foo', 'bar', 'baz'])
50
self.assertIs(args.temp_env_name, None)
51
args = parse_args(['foo', 'bar', 'baz', 'qux'])
52
self.assertEqual(args.temp_env_name, 'qux')
55
def make_mocked_client(name, status_error=None):
56
client = EnvJujuClient(SimpleEnvironment(
57
name, {'type': 'paas'}), '1.23', 'path')
58
patch.object(client, 'wait_for_ha', autospec=True).start()
60
client, 'get_status', autospec=True, side_effect=status_error).start()
61
patch.object(client, 'destroy_environment', autospec=True).start()
65
@patch('assess_recovery.dump_env_logs', autospec=True)
66
@patch('assess_recovery.parse_new_state_server_from_error', autospec=True,
67
return_value='new_host')
176
68
@patch('assess_recovery.wait_for_state_server_to_shutdown', autospec=True)
177
@patch('assess_recovery.terminate_instances', autospec=True)
178
class TestDeleteControllerMembers(FakeHomeTestCase):
180
def test_delete_controller_members(self, ti_mock, wsss_mock):
181
client = Mock(spec=['env', 'get_controller_members'])
182
client.env = sentinel.env
183
client.env.config = {'type': 'lxd'}
184
client.get_controller_members.return_value = [
186
'dns-name': '10.0.0.3',
187
'instance-id': 'juju-dddd-machine-3',
188
'controller-member-status': 'has-vote'}),
190
'dns-name': '10.0.0.0',
191
'instance-id': 'juju-aaaa-machine-0',
192
'controller-member-status': 'has-vote'}),
194
'dns-name': '10.0.0.2',
195
'instance-id': 'juju-cccc-machine-2',
196
'controller-member-status': 'has-vote'}),
198
deleted = delete_controller_members(client)
199
self.assertEqual(['2', '0', '3'], deleted)
200
client.get_controller_members.assert_called_once_with()
201
# terminate_instance was call in the reverse order of members.
203
[call(client.env, ['juju-cccc-machine-2']),
204
call(client.env, ['juju-aaaa-machine-0']),
205
call(client.env, ['juju-dddd-machine-3'])],
208
[call('10.0.0.2', client, 'juju-cccc-machine-2', timeout=120),
209
call('10.0.0.0', client, 'juju-aaaa-machine-0', timeout=120),
210
call('10.0.0.3', client, 'juju-dddd-machine-3', timeout=120)],
211
wsss_mock.mock_calls)
213
self.log_stream.getvalue(),
214
'INFO Instrumenting node failure for member 2:'
215
' juju-cccc-machine-2 at 10.0.0.2\n'
216
'INFO Instrumenting node failure for member 0:'
217
' juju-aaaa-machine-0 at 10.0.0.0\n'
218
'INFO Instrumenting node failure for member 3:'
219
' juju-dddd-machine-3 at 10.0.0.3\n')
221
def test_delete_controller_members_leader_only(self, ti_mock, wsss_mock):
222
client = Mock(spec=['env', 'get_controller_leader'])
223
client.env = sentinel.env
224
client.env.config = {'type': 'lxd'}
225
client.get_controller_leader.return_value = Machine('3', {
226
'dns-name': '10.0.0.3',
227
'instance-id': 'juju-dddd-machine-3',
228
'controller-member-status': 'has-vote'})
229
deleted = delete_controller_members(client, leader_only=True)
230
self.assertEqual(['3'], deleted)
231
client.get_controller_leader.assert_called_once_with()
232
ti_mock.assert_called_once_with(client.env, ['juju-dddd-machine-3'])
233
wsss_mock.assert_called_once_with(
234
'10.0.0.3', client, 'juju-dddd-machine-3', timeout=120)
236
self.log_stream.getvalue(),
237
'INFO Instrumenting node failure for member 3:'
238
' juju-dddd-machine-3 at 10.0.0.3\n')
240
def test_delete_controller_members_azure(self, ti_mock, wsss_mock):
241
client = Mock(spec=['env', 'get_controller_leader'])
242
client.env = sentinel.env
243
client.env.config = {'type': 'azure'}
244
client.get_controller_leader.return_value = Machine('3', {
245
'dns-name': '10.0.0.3',
246
'instance-id': 'juju-dddd-machine-3',
247
'controller-member-status': 'has-vote'})
248
with patch('assess_recovery.convert_to_azure_ids', autospec=True,
249
return_value=['juju-azure-id']):
250
deleted = delete_controller_members(client, leader_only=True)
251
self.assertEqual(['3'], deleted)
252
client.get_controller_leader.assert_called_once_with()
253
ti_mock.assert_called_once_with(client.env, ['juju-azure-id'])
254
wsss_mock.assert_called_once_with(
255
'10.0.0.3', client, 'juju-azure-id', timeout=120)
257
self.log_stream.getvalue(),
258
'INFO Instrumenting node failure for member 3:'
259
' juju-azure-id at 10.0.0.3\n')
262
class TestRestoreMissingStateServer(FakeHomeTestCase):
264
def test_restore_missing_state_server_with_check_controller(self):
265
client = Mock(spec=['env', 'set_config', 'wait_for_started',
266
'wait_for_workloads'])
267
controller_client = Mock(spec=['restore_backup', 'wait_for_started'])
268
with patch('assess_recovery.check_token',
269
autospec=True, return_value='Token: Two'):
270
with patch('assess_recovery.show_controller', autospec=True):
271
restore_missing_state_server(
272
client, controller_client, 'backup_file',
273
check_controller=True)
274
controller_client.restore_backup.assert_called_once_with('backup_file')
275
controller_client.wait_for_started.assert_called_once_with(600)
276
client.set_config.assert_called_once_with(
277
'dummy-source', {'token': 'Two'})
278
client.wait_for_started.assert_called_once_with()
279
client.wait_for_workloads.assert_called_once_with()
281
def test_restore_missing_state_server_without_check_controller(self):
282
client = Mock(spec=['env', 'set_config', 'wait_for_started',
283
'wait_for_workloads'])
284
controller_client = Mock(spec=['restore_backup', 'wait_for_started'])
285
with patch('assess_recovery.check_token',
286
autospec=True, return_value='Token: Two'):
287
with patch('assess_recovery.show_controller', autospec=True):
288
restore_missing_state_server(
289
client, controller_client, 'backup_file',
290
check_controller=False)
291
self.assertEqual(0, controller_client.wait_for_started.call_count)
294
class TestCheckToken(TestCase):
296
def test_check_token_found(self):
298
with patch('assess_recovery.get_token_from_status', autospec=True,
299
side_effect=['Token: foo']):
300
found = check_token(client, 'foo')
301
self.assertEqual('Token: foo', found)
303
def test_check_token_none_before_found(self):
305
with patch('assess_recovery.get_token_from_status', autospec=True,
306
side_effect=[None, 'foo']):
307
found = check_token(client, 'foo')
308
self.assertEqual('foo', found)
310
def test_check_token_other_before_found(self):
312
with patch('assess_recovery.get_token_from_status', autospec=True,
313
side_effect=['Starting', 'foo']):
314
found = check_token(client, 'foo')
315
self.assertEqual('foo', found)
317
def test_check_token_not_found(self):
319
with patch('assess_recovery.get_token_from_status', autospec=True,
320
return_value='other'):
321
with patch('assess_recovery.until_timeout', autospec=True,
322
side_effect=['1', '0']):
323
with self.assertRaises(JujuAssertionError):
324
check_token(client, 'foo')
69
@patch('assess_recovery.delete_instance', autospec=True)
70
@patch('assess_recovery.deploy_stack', autospec=True, return_value='i_id')
71
@patch('assess_recovery.get_machine_dns_name', autospec=True,
73
@patch('subprocess.check_output', autospec=True)
74
@patch('subprocess.check_call', autospec=True)
75
@patch('sys.stdout', autospec=True)
76
class TestMain(TestCase):
79
setup_test_logging(self)
81
def test_ha(self, so_mock, cc_mock, co_mock,
82
dns_mock, ds_mock, di_mock, ws_mock, ns_mock, dl_mock):
83
client = make_mocked_client('foo')
84
with patch('assess_recovery.make_client', autospec=True,
85
return_value=client) as mc_mock:
86
main(['./', 'foo', 'log_dir',
87
'--ha', '--charm-prefix', 'prefix'])
88
mc_mock.assert_called_once_with('./', False, 'foo', None)
89
client.wait_for_ha.assert_called_once_with()
90
client.get_status.assert_called_once_with(600)
91
client.destroy_environment.assert_called_once_with()
92
dns_mock.assert_called_once_with(client, 0)
93
ds_mock.assert_called_once_with(client, 'prefix')
94
di_mock.assert_called_once_with(client, 'i_id')
95
ws_mock.assert_called_once_with('host', client, 'i_id')
96
dl_mock.assert_called_once_with(client, None, 'log_dir')
97
self.assertEqual(0, ns_mock.call_count)
99
def test_ha_error(self, so_mock, cc_mock, co_mock,
100
dns_mock, ds_mock, di_mock, ws_mock, ns_mock, dl_mock):
102
client = make_mocked_client('foo', status_error=error)
103
with patch('assess_recovery.make_client', autospec=True,
104
return_value=client) as mc_mock:
105
with self.assertRaises(SystemExit):
106
main(['./', 'foo', 'log_dir',
107
'--ha', '--charm-prefix', 'prefix'])
108
mc_mock.assert_called_once_with('./', False, 'foo', None)
109
client.wait_for_ha.assert_called_once_with()
110
client.get_status.assert_called_once_with(600)
111
client.destroy_environment.assert_called_once_with()
112
dns_mock.assert_called_once_with(client, 0)
113
ds_mock.assert_called_once_with(client, 'prefix')
114
di_mock.assert_called_once_with(client, 'i_id')
115
ws_mock.assert_called_once_with('host', client, 'i_id')
116
ns_mock.assert_called_once_with(error)
117
dl_mock.assert_called_once_with(client, 'new_host', 'log_dir')