1
from contextlib import contextmanager
1
from argparse import Namespace
10
4
from assess_recovery import (
12
delete_controller_members,
9
from jujuconfig import get_jenv_path
16
10
from jujupy import (
13
_temp_env as temp_env,
19
15
from tests import (
23
from tests.test_jujupy import fake_juju_client
26
21
class TestParseArgs(TestCase):
28
23
def test_parse_args(self):
29
args = parse_args(['an-env', '/juju', 'log', 'tmp-env'])
30
self.assertEqual(args.env, 'an-env')
31
self.assertEqual(args.juju_bin, '/juju')
32
self.assertEqual(args.logs, 'log')
33
self.assertEqual(args.temp_env_name, 'tmp-env')
34
self.assertEqual(args.charm_series, '')
24
args = parse_args(['foo', 'bar', 'baz'])
25
self.assertEqual(args.juju_path, 'foo')
26
self.assertEqual(args.env_name, 'bar')
27
self.assertEqual(args.logs, 'baz')
28
self.assertEqual(args.charm_prefix, '')
35
29
self.assertEqual(args.strategy, 'backup')
36
self.assertEqual(args.verbose, logging.INFO)
37
30
self.assertEqual(args.debug, False)
38
31
self.assertIs(args.agent_stream, None)
39
32
self.assertIs(args.series, None)
41
34
def test_parse_args_ha(self):
42
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha'])
35
args = parse_args(['foo', 'bar', 'baz', '--ha'])
43
36
self.assertEqual(args.strategy, 'ha')
45
38
def test_parse_args_ha_backup(self):
46
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha-backup'])
39
args = parse_args(['foo', 'bar', 'baz', '--ha-backup'])
47
40
self.assertEqual(args.strategy, 'ha-backup')
49
42
def test_parse_args_backup(self):
50
args = parse_args(['an-env', '/juju', 'log', 'tmp-env', '--ha',
43
args = parse_args(['foo', 'bar', 'baz', '--ha', '--backup'])
52
44
self.assertEqual(args.strategy, 'backup')
54
def test_parse_args_charm_series(self):
55
args = parse_args(['an-env', '/juju', 'log', 'tmp-env',
56
'--charm-series', 'qux'])
57
self.assertEqual(args.charm_series, 'qux')
60
class TestAssessRecovery(TestCase):
63
def assess_recovery_cxt(self, client):
66
def terminate(env, instance_ids):
67
model = client._backend.controller_state.controller_model
68
for instance_id in instance_ids:
69
model.remove_state_server(instance_id)
71
with patch('assess_recovery.wait_for_state_server_to_shutdown',
73
with patch('assess_recovery.terminate_instances',
74
side_effect=terminate):
75
with patch('deploy_stack.wait_for_port', autospec=True):
78
def test_backup(self):
79
client = fake_juju_client()
80
bs_manager = Mock(client=client, known_hosts={})
81
with self.assess_recovery_cxt(client):
82
assess_recovery(bs_manager, 'backup', 'trusty')
85
client = fake_juju_client()
86
bs_manager = Mock(client=client, known_hosts={})
87
with self.assess_recovery_cxt(client):
88
assess_recovery(bs_manager, 'ha', 'trusty')
90
def test_ha_backup(self):
91
client = fake_juju_client()
92
bs_manager = Mock(client=client, known_hosts={})
93
with self.assess_recovery_cxt(client):
94
assess_recovery(bs_manager, 'ha-backup', 'trusty')
96
def test_controller_model_backup(self):
97
client = fake_juju_client()
98
bs_manager = Mock(client=client, known_hosts={})
99
with self.assess_recovery_cxt(client):
100
assess_recovery(bs_manager, 'backup', 'trusty')
102
def test_controller_model_ha(self):
103
client = fake_juju_client()
104
bs_manager = Mock(client=client, known_hosts={})
105
with self.assess_recovery_cxt(client):
106
assess_recovery(bs_manager, 'ha', 'trusty')
108
def test_controller_model_ha_backup(self):
109
client = fake_juju_client()
110
bs_manager = Mock(client=client, known_hosts={})
111
with self.assess_recovery_cxt(client):
112
assess_recovery(bs_manager, 'ha-backup', 'trusty')
115
@patch('assess_recovery.configure_logging', autospec=True)
116
@patch('assess_recovery.BootstrapManager.booted_context', autospec=True)
46
def test_parse_args_charm_prefix(self):
47
args = parse_args(['foo', 'bar', 'baz', '--charm-prefix', 'qux'])
48
self.assertEqual(args.charm_prefix, 'qux')
50
def test_parse_args_debug(self):
51
args = parse_args(['foo', 'bar', 'baz', '--debug'])
52
self.assertEqual(args.debug, True)
54
def test_parse_args_temp_env_name(self):
55
args = parse_args(['foo', 'bar', 'baz'])
56
self.assertIs(args.temp_env_name, None)
57
args = parse_args(['foo', 'bar', 'baz', 'qux'])
58
self.assertEqual(args.temp_env_name, 'qux')
60
def test_parse_args_agent_stream(self):
61
args = parse_args(['foo', 'bar', 'baz', '--agent-stream', 'qux'])
62
self.assertEqual(args.agent_stream, 'qux')
64
def test_parse_args_series(self):
65
args = parse_args(['foo', 'bar', 'baz', '--series', 'qux'])
66
self.assertEqual(args.series, 'qux')
69
class TestMakeClientFromArgs(TestCase):
71
def test_make_client_from_args(self):
72
with temp_env({'environments': {'foo': {}}}):
73
with patch.object(EnvJujuClient, 'get_version', return_value=''):
74
client = make_client_from_args(
75
Namespace(env_name='foo', juju_path='bar',
76
temp_env_name='temp-foo', debug=False,
77
agent_stream=None, series=None))
78
self.assertEqual(client.env.config, {'name': 'temp-foo'})
79
self.assertEqual(client.env.environment, 'temp-foo')
82
def make_mocked_client(name, status_error=None):
83
client = EnvJujuClient(SimpleEnvironment(
84
name, {'type': 'paas'}), '1.23', 'path')
85
patch.object(client, 'wait_for_ha', autospec=True).start()
87
client, 'get_status', autospec=True, side_effect=status_error).start()
88
patch.object(client, 'destroy_environment', autospec=True).start()
89
patch.object(client, 'is_jes_enabled', autospec=True,
90
return_value=False).start()
94
@patch('deploy_stack.dump_env_logs_known_hosts', autospec=True)
95
@patch('assess_recovery.parse_new_state_server_from_error', autospec=True,
96
return_value='new_host')
97
@patch('assess_recovery.wait_for_state_server_to_shutdown', autospec=True)
98
@patch('assess_recovery.delete_instance', autospec=True)
99
@patch('assess_recovery.deploy_stack', autospec=True, return_value='i_id')
100
@patch('deploy_stack.get_machine_dns_name', autospec=True,
102
@patch('subprocess.check_output', autospec=True)
103
@patch('subprocess.check_call', autospec=True)
104
@patch('sys.stderr', autospec=True)
117
105
class TestMain(FakeHomeTestCase):
119
def test_main(self, mock_bc, mock_cl):
120
client = Mock(spec=['is_jes_enabled', 'version'])
121
client.version = '1.25.5'
122
with patch('deploy_stack.client_from_config',
123
return_value=client) as mock_c:
124
with patch('assess_recovery.assess_recovery',
125
autospec=True) as mock_assess:
126
main(['an-env', '/juju', 'log_dir', 'tmp-env', '--backup',
127
'--charm-series', 'a-series'])
128
mock_cl.assert_called_once_with(logging.INFO)
129
mock_c.assert_called_once_with('an-env', '/juju', debug=False)
130
self.assertEqual(mock_bc.call_count, 1)
131
self.assertEqual(mock_assess.call_count, 1)
132
bs_manager, strategy, series = mock_assess.call_args[0]
133
self.assertEqual((bs_manager.client, strategy, series),
134
(client, 'backup', 'a-series'))
136
def test_error(self, mock_bc, mock_cl):
137
class FakeError(Exception):
138
"""Custom exception to validate error handling."""
139
error = FakeError('An error during test')
140
client = Mock(spec=['is_jes_enabled', 'version'])
141
client.version = '2.0.0'
142
with patch('deploy_stack.client_from_config',
143
return_value=client) as mock_c:
144
with patch('assess_recovery.parse_new_state_server_from_error',
145
autospec=True, return_value='a-host') as mock_pe:
146
with patch('assess_recovery.assess_recovery', autospec=True,
147
side_effect=error) as mock_assess:
148
with self.assertRaises(FakeError) as ctx:
149
main(['an-env', '/juju', 'log_dir', 'tmp-env', '--ha',
150
'--verbose', '--charm-series', 'a-series'])
151
self.assertIs(ctx.exception, error)
152
mock_cl.assert_called_once_with(logging.DEBUG)
153
mock_c.assert_called_once_with('an-env', '/juju', debug=False)
154
mock_pe.assert_called_once_with(error)
155
self.assertEqual(mock_bc.call_count, 1)
156
self.assertEqual(mock_assess.call_count, 1)
157
bs_manager, strategy, series = mock_assess.call_args[0]
158
self.assertEqual((bs_manager.client, strategy, series),
159
(client, 'ha', 'a-series'))
160
self.assertEqual(bs_manager.known_hosts['0'], 'a-host')
163
@patch('assess_recovery.wait_for_state_server_to_shutdown', autospec=True)
164
@patch('assess_recovery.terminate_instances', autospec=True)
165
class TestDeleteControllerMembers(FakeHomeTestCase):
167
def test_delete_controller_members(self, ti_mock, wsss_mock):
168
client = Mock(spec=['env', 'get_controller_members'])
169
client.env = sentinel.env
170
client.env.config = {'type': 'lxd'}
171
client.get_controller_members.return_value = [
173
'dns-name': '10.0.0.3',
174
'instance-id': 'juju-dddd-machine-3',
175
'controller-member-status': 'has-vote'}),
177
'dns-name': '10.0.0.0',
178
'instance-id': 'juju-aaaa-machine-0',
179
'controller-member-status': 'has-vote'}),
181
'dns-name': '10.0.0.2',
182
'instance-id': 'juju-cccc-machine-2',
183
'controller-member-status': 'has-vote'}),
185
deleted = delete_controller_members(client)
186
self.assertEqual(['2', '0', '3'], deleted)
187
client.get_controller_members.assert_called_once_with()
188
# terminate_instance was call in the reverse order of members.
190
[call(client.env, ['juju-cccc-machine-2']),
191
call(client.env, ['juju-aaaa-machine-0']),
192
call(client.env, ['juju-dddd-machine-3'])],
195
[call('10.0.0.2', client, 'juju-cccc-machine-2', timeout=120),
196
call('10.0.0.0', client, 'juju-aaaa-machine-0', timeout=120),
197
call('10.0.0.3', client, 'juju-dddd-machine-3', timeout=120)],
198
wsss_mock.mock_calls)
200
self.log_stream.getvalue(),
201
'INFO Instrumenting node failure for member 2:'
202
' juju-cccc-machine-2 at 10.0.0.2\n'
203
'INFO Instrumenting node failure for member 0:'
204
' juju-aaaa-machine-0 at 10.0.0.0\n'
205
'INFO Instrumenting node failure for member 3:'
206
' juju-dddd-machine-3 at 10.0.0.3\n')
208
def test_delete_controller_members_leader_only(self, ti_mock, wsss_mock):
209
client = Mock(spec=['env', 'get_controller_leader'])
210
client.env = sentinel.env
211
client.env.config = {'type': 'lxd'}
212
client.get_controller_leader.return_value = Machine('3', {
213
'dns-name': '10.0.0.3',
214
'instance-id': 'juju-dddd-machine-3',
215
'controller-member-status': 'has-vote'})
216
deleted = delete_controller_members(client, leader_only=True)
217
self.assertEqual(['3'], deleted)
218
client.get_controller_leader.assert_called_once_with()
219
ti_mock.assert_called_once_with(client.env, ['juju-dddd-machine-3'])
220
wsss_mock.assert_called_once_with(
221
'10.0.0.3', client, 'juju-dddd-machine-3', timeout=120)
223
self.log_stream.getvalue(),
224
'INFO Instrumenting node failure for member 3:'
225
' juju-dddd-machine-3 at 10.0.0.3\n')
227
def test_delete_controller_members_azure(self, ti_mock, wsss_mock):
228
client = Mock(spec=['env', 'get_controller_leader'])
229
client.env = sentinel.env
230
client.env.config = {'type': 'azure'}
231
client.get_controller_leader.return_value = Machine('3', {
232
'dns-name': '10.0.0.3',
233
'instance-id': 'juju-dddd-machine-3',
234
'controller-member-status': 'has-vote'})
235
with patch('assess_recovery.convert_to_azure_ids', autospec=True,
236
return_value=['juju-azure-id']):
237
deleted = delete_controller_members(client, leader_only=True)
238
self.assertEqual(['3'], deleted)
239
client.get_controller_leader.assert_called_once_with()
240
ti_mock.assert_called_once_with(client.env, ['juju-azure-id'])
241
wsss_mock.assert_called_once_with(
242
'10.0.0.3', client, 'juju-azure-id', timeout=120)
244
self.log_stream.getvalue(),
245
'INFO Instrumenting node failure for member 3:'
246
' juju-azure-id at 10.0.0.3\n')
107
def test_ha(self, so_mock, cc_mock, co_mock,
108
dns_mock, ds_mock, di_mock, ws_mock, ns_mock, dl_mock):
109
client = make_mocked_client('foo')
110
with patch('assess_recovery.make_client_from_args', autospec=True,
111
return_value=client) as mc_mock:
112
main(['./', 'foo', 'log_dir',
113
'--ha', '--charm-prefix', 'prefix'])
114
mc_mock.assert_called_once_with(Namespace(
115
agent_stream=None, charm_prefix='prefix', debug=False,
116
env_name='foo', juju_path='./', logs='log_dir', strategy='ha',
117
temp_env_name=None, series=None))
118
client.wait_for_ha.assert_called_once_with()
119
client.get_status.assert_called_once_with(600)
120
self.assertEqual(2, client.destroy_environment.call_count)
121
dns_mock.assert_called_once_with(client, '0')
122
ds_mock.assert_called_once_with(client, 'prefix')
123
di_mock.assert_called_once_with(client, 'i_id')
124
ws_mock.assert_called_once_with('host', client, 'i_id')
125
jenv_path = get_jenv_path(client.env.juju_home, client.env.environment)
126
dl_mock.assert_called_once_with(client, 'log_dir', jenv_path,
128
self.assertEqual(0, ns_mock.call_count)
130
def test_ha_error(self, so_mock, cc_mock, co_mock,
131
dns_mock, ds_mock, di_mock, ws_mock, ns_mock, dl_mock):
133
client = make_mocked_client('foo', status_error=error)
134
with patch('assess_recovery.make_client_from_args', autospec=True,
135
return_value=client) as mc_mock:
136
with self.assertRaises(SystemExit):
137
main(['./', 'foo', 'log_dir',
138
'--ha', '--charm-prefix', 'prefix'])
139
mc_mock.assert_called_once_with(Namespace(
140
agent_stream=None, charm_prefix='prefix', debug=False,
141
env_name='foo', juju_path='./', logs='log_dir', strategy='ha',
142
temp_env_name=None, series=None))
143
client.wait_for_ha.assert_called_once_with()
144
client.get_status.assert_called_once_with(600)
145
self.assertEqual(2, client.destroy_environment.call_count)
146
dns_mock.assert_called_once_with(client, '0')
147
ds_mock.assert_called_once_with(client, 'prefix')
148
di_mock.assert_called_once_with(client, 'i_id')
149
ws_mock.assert_called_once_with('host', client, 'i_id')
150
ns_mock.assert_called_once_with(error)
151
jenv_path = get_jenv_path(client.env.juju_home, client.env.environment)
152
dl_mock.assert_called_once_with(client, 'log_dir', jenv_path,
155
def test_destroy_on_boot_error(self, so_mock, cc_mock, co_mock,
156
dns_mock, ds_mock, di_mock, ws_mock,
158
client = make_mocked_client('foo')
159
with patch('assess_recovery.make_client', autospec=True,
160
return_value=client):
161
with patch.object(client, 'bootstrap', side_effect=Exception):
162
with self.assertRaises(SystemExit):
163
main(['./', 'foo', 'log_dir',
164
'--ha', '--charm-prefix', 'prefix'])
165
self.assertEqual(2, client.destroy_environment.call_count)