1
from argparse import ArgumentParser
2
from contextlib import contextmanager
3
from copy import deepcopy
4
from unittest import TestCase
12
import assess_spaces_subnets as jss
23
"""A mock of the parts of the Juju command that the tests hit."""
24
# XXX This is the version from assess-spaces-subnets r 1103, which has
25
# enough differences from the assess_container_networking version that the
27
# These differences should be reconciled in a future branch.
31
self._status = {'services': {},
32
'machines': {'0': {}}}
38
self._subnet_count = 0
39
self._ssh_machine_output = {}
40
self._next_service_machine = 1
43
def add_machine(self, args):
44
if isinstance(args, tuple) and args[0] == '-n':
45
for n in range(int(args[1])):
48
self._add_machine(args)
50
def _add_machine(self, name=None):
51
if name is None or name == '':
52
name = str(self.next_machine)
53
self.next_machine += 1
55
bits = name.split(':')
59
container_type = bits[0]
60
if machine not in self._status['machines']:
61
self._status['machines'][machine] = {}
62
if 'containers' not in self._status['machines'][machine]:
63
self._status['machines'][machine]['containers'] = {}
66
c_name = machine + '/' + container_type + '/' + str(n)
67
while c_name in self._status['machines'][machine]['containers']:
69
c_name = machine + '/' + container_type + '/' + str(n)
70
self._status['machines'][machine]['containers'][c_name] = {}
73
self._status['machines'][name] = {}
77
def add_service(self, name):
78
# We just add a hunk of data captured from a real Juju run and don't
79
# worry about how it doesn't match reality. It is enough to exercise
80
# the code under test.
85
'since': '06 Aug 2015 11:39:29+01:00'
87
'charm': 'cs:trusty/{}-26'.format(name),
88
'relations': {'cluster': [name]},
90
self._status['services'][name] = deepcopy(new_service)
91
self._services[name] = 0
94
def add_unit(self, name):
95
machine = self._add_machine()
96
unit_name = name + '/' + str(self._services[name])
97
self._services[name] += 1
98
self._status['services'][name]['units'][unit_name] = {
99
'machine': str(machine),
100
'public-address': 'noxious-disgust.maas',
102
'current': 'unknown',
103
'since': '06 Aug 2015 11:39:29+01:00'},
106
'since': '06 Aug 2015 11:39:33+01:00',
107
'version': '1.25-alpha1.1'},
108
'agent-state': 'started',
109
'agent-version': '1.25-alpha1.1',
112
def juju(self, cmd, *args, **kwargs):
115
self.commands.append((cmd, args))
116
if cmd == 'remove-service' and args in self._status['services']:
117
del self._status['services'][args]
119
elif cmd == 'remove-machine':
120
if args in self._status['machines']:
121
del self._status['machines'][args]
123
machine = args.split('/')[0]
124
del self._status['machines'][machine]['containers'][args]
126
if len(self._status['machines'][machine]['containers']) == 0:
127
del self._status['machines'][machine]['containers']
129
elif cmd == 'add-machine':
130
self.add_machine(args)
133
if args[0] in self._ssh_machine_output:
134
ssh_output = self._ssh_machine_output[args[0]]
136
ssh_output = self._ssh_output
138
if len(ssh_output) == 0:
142
return ssh_output[self._call_number()]
144
# If we ran out of values, just return the last one
145
return ssh_output[-1]
147
elif cmd == 'add-space':
148
self._spaces[args] = []
149
elif cmd == 'list-space':
150
return yaml.dump({'spaces': self._spaces})
151
elif cmd == 'add-subnet':
152
subnet = '10.{}.0.0/16'.format(self._subnet_count)
153
self._subnet_count += 1
155
self._spaces[args[1]].append(subnet)
156
self._subnets[subnet] = args[0]
157
elif cmd == 'deploy':
158
parser = ArgumentParser()
159
# Due to a long standing bug in argparse, we can't use positional
160
# arguments with a '-' in their value. If you got here wondering
161
# if you could deploy a charm with a name foo-bar, yes, you can
162
# with Juju, but you can't with this test framework. Sorry.
163
parser.add_argument('charm')
164
parser.add_argument('name', nargs='?', default=None)
165
parser.add_argument('--constraints', nargs=1, default="")
166
args = parser.parse_args(args)
169
self.add_service(args.name)
171
self.add_service(args.charm)
173
self._next_service_machine += 1
174
elif cmd == 'add-unit':
181
raise ValueError("Unpatched command: {} {}".format(cmd, args))
184
def juju_async(self, cmd, args):
189
def _call_number(self):
190
call_number = self._call_n
194
def get_status(self, machine_id=None):
195
return Status(deepcopy(self._status), "")
197
def set_status(self, status):
198
self._status = deepcopy(status)
200
def set_ssh_output(self, ssh_output, machine_id=None):
201
if machine_id is not None:
202
self._ssh_machine_output[machine_id] = deepcopy(ssh_output)
204
self._ssh_output = deepcopy(ssh_output)
206
def reset_calls(self):
210
class JujuMockTestCase(TestCase):
212
self.client = EnvJujuClient(
213
JujuData('foo', {'type': 'local'}), '1.234-76', None)
218
self.juju_mock = JujuMock()
219
self.ssh_mock = Mock()
222
patch.object(self.client, 'juju', self.juju_mock.juju),
223
patch.object(self.client, 'get_status', self.juju_mock.get_status),
224
patch.object(self.client, 'juju_async', self.juju_mock.juju_async),
225
patch.object(self.client, 'wait_for', nil_func),
226
patch.object(self.client, 'wait_for_started',
227
self.juju_mock.get_status),
228
patch.object(self.client, 'get_juju_output', self.juju_mock.juju),
231
for patcher in patches:
233
self.addCleanup(patcher.stop)
236
class TestSubnetsSpaces(JujuMockTestCase):
237
def test_ipv4_to_int(self):
239
jss.ipv4_to_int('1.2.3.4'),
243
jss.ipv4_to_int('255.255.255.255'),
246
def test_ipv4_in_cidr(self):
247
self.assertTrue(jss.ipv4_in_cidr('1.1.1.1', '1.1.1.1/32'))
248
self.assertTrue(jss.ipv4_in_cidr('1.1.1.1', '1.1.1.0/24'))
249
self.assertTrue(jss.ipv4_in_cidr('1.1.1.1', '1.1.0.0/16'))
250
self.assertTrue(jss.ipv4_in_cidr('1.1.1.1', '1.0.0.0/8'))
251
self.assertTrue(jss.ipv4_in_cidr('1.1.1.1', '0.0.0.0/0'))
253
self.assertFalse(jss.ipv4_in_cidr('2.1.1.1', '1.1.1.1/32'))
254
self.assertFalse(jss.ipv4_in_cidr('2.1.1.1', '1.1.1.0/24'))
255
self.assertFalse(jss.ipv4_in_cidr('2.1.1.1', '1.1.0.0/16'))
256
self.assertFalse(jss.ipv4_in_cidr('2.1.1.1', '1.0.0.0/8'))
259
'apps': ['10.0.0.0/16', '10.1.0.0/16'],
260
'backend': ['10.2.0.0/16', '10.3.0.0/16'],
261
'default': ['10.4.0.0/16', '10.5.0.0/16'],
262
'dmz': ['10.6.0.0/16', '10.7.0.0/16'],
265
'haproxy': {'space': 'dmz'},
266
'mediawiki': {'space': 'apps'},
267
'memcached': {'space': 'apps'},
268
'mysql': {'space': 'backend'},
275
def test_assess_spaces_subnets(self):
276
# The following table is derived from the above settings
278
# Charm ---------------- space --- address in subnet
279
# haproxy - dmz - 10.6.0.2
280
# mediawiki, memcached - apps - 10.0.0.2
281
# mysql, mysql-slace - backend - 10.2.0.2
283
# We translate the above table into these responses to "ip -o addr",
284
# which are assigned to machines that we have found by running this
285
# test. The order is fixed because we iterate over keys in dictionaries
287
self.juju_mock.set_ssh_output(['2: eth0 inet 10.6.0.2'], '1')
288
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '2')
289
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '3')
290
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '4')
291
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '5')
292
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '6')
293
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '7')
295
jss._assess_spaces_subnets(
296
self.client, self.network_config, self.charms_to_space)
298
def test_assess_spaces_subnets_fail(self):
299
# The output in this test is set to be the same as in
300
# test_assess_spaces_subnets with machines 1 and 2 swapped.
301
# This results in mediawiki/0 appearing in the dmz instead of apps
302
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '1')
303
self.juju_mock.set_ssh_output(['2: eth0 inet 10.6.0.2'], '2')
304
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '3')
305
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '4')
306
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '5')
307
self.juju_mock.set_ssh_output(['2: eth0 inet 10.2.0.2'], '6')
308
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '7')
310
self.assertRaisesRegexp(
311
ValueError, 'Found mediawiki/0 in dmz, expected apps',
312
jss._assess_spaces_subnets,
313
self.client, self.network_config, self.charms_to_space)
315
def test_assess_spaces_subnets_fail_to_find_all_spaces(self):
316
# Should get an error if we can't find the space for each unit
317
self.juju_mock.set_ssh_output(['2: eth0 inet 10.6.0.2'], '1')
318
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '2')
319
self.juju_mock.set_ssh_output(['2: eth0 inet 10.0.0.2'], '3')
320
self.assertRaisesRegexp(
321
ValueError, 'Could not find spaces for all units',
322
jss._assess_spaces_subnets,
323
self.client, self.network_config, self.charms_to_space)