~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to tests/test_assess_spaces_subnets.py

  • Committer: Aaron Bentley
  • Date: 2015-09-02 17:46:47 UTC
  • mto: This revision was merged to the branch mainline in revision 1082.
  • Revision ID: aaron.bentley@canonical.com-20150902174647-06vmnsooo6yzd46t
Stop supplying env to subprocess calls.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from argparse import ArgumentParser
2
 
from contextlib import contextmanager
3
 
from copy import deepcopy
4
 
from unittest import TestCase
5
 
 
6
 
from mock import (
7
 
    Mock,
8
 
    patch,
9
 
    )
10
 
import yaml
11
 
 
12
 
import assess_spaces_subnets as jss
13
 
from jujupy import (
14
 
    EnvJujuClient,
15
 
    JujuData,
16
 
    Status,
17
 
    )
18
 
 
19
 
__metaclass__ = type
20
 
 
21
 
 
22
 
class JujuMock:
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
26
 
    # tests break.
27
 
    # These differences should be reconciled in a future branch.
28
 
 
29
 
    def __init__(self):
30
 
        self._call_n = 0
31
 
        self._status = {'services': {},
32
 
                        'machines': {'0': {}}}
33
 
        self.commands = []
34
 
        self.next_machine = 1
35
 
        self._ssh_output = []
36
 
        self._spaces = {}
37
 
        self._subnets = {}
38
 
        self._subnet_count = 0
39
 
        self._ssh_machine_output = {}
40
 
        self._next_service_machine = 1
41
 
        self._services = {}
42
 
 
43
 
    def add_machine(self, args):
44
 
        if isinstance(args, tuple) and args[0] == '-n':
45
 
            for n in range(int(args[1])):
46
 
                self._add_machine()
47
 
        else:
48
 
            self._add_machine(args)
49
 
 
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
54
 
 
55
 
        bits = name.split(':')
56
 
        if len(bits) > 1:
57
 
            # is a container
58
 
            machine = bits[1]
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'] = {}
64
 
 
65
 
            n = 0
66
 
            c_name = machine + '/' + container_type + '/' + str(n)
67
 
            while c_name in self._status['machines'][machine]['containers']:
68
 
                n += 1
69
 
                c_name = machine + '/' + container_type + '/' + str(n)
70
 
            self._status['machines'][machine]['containers'][c_name] = {}
71
 
        else:
72
 
            # normal machine
73
 
            self._status['machines'][name] = {}
74
 
 
75
 
        return name
76
 
 
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.
81
 
        new_service = {
82
 
            'units': {},
83
 
            'service-status': {
84
 
                'current': 'unknown',
85
 
                'since': '06 Aug 2015 11:39:29+01:00'
86
 
            },
87
 
            'charm': 'cs:trusty/{}-26'.format(name),
88
 
            'relations': {'cluster': [name]},
89
 
            'exposed': False}
90
 
        self._status['services'][name] = deepcopy(new_service)
91
 
        self._services[name] = 0
92
 
        self.add_unit(name)
93
 
 
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',
101
 
            'workload-status': {
102
 
                'current': 'unknown',
103
 
                'since': '06 Aug 2015 11:39:29+01:00'},
104
 
            'agent-status': {
105
 
                'current': 'idle',
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',
110
 
        }
111
 
 
112
 
    def juju(self, cmd, *args, **kwargs):
113
 
        if len(args) == 1:
114
 
            args = args[0]
115
 
        self.commands.append((cmd, args))
116
 
        if cmd == 'remove-service' and args in self._status['services']:
117
 
            del self._status['services'][args]
118
 
 
119
 
        elif cmd == 'remove-machine':
120
 
            if args in self._status['machines']:
121
 
                del self._status['machines'][args]
122
 
            else:
123
 
                machine = args.split('/')[0]
124
 
                del self._status['machines'][machine]['containers'][args]
125
 
 
126
 
                if len(self._status['machines'][machine]['containers']) == 0:
127
 
                    del self._status['machines'][machine]['containers']
128
 
 
129
 
        elif cmd == 'add-machine':
130
 
            self.add_machine(args)
131
 
 
132
 
        elif cmd == 'ssh':
133
 
            if args[0] in self._ssh_machine_output:
134
 
                ssh_output = self._ssh_machine_output[args[0]]
135
 
            else:
136
 
                ssh_output = self._ssh_output
137
 
 
138
 
            if len(ssh_output) == 0:
139
 
                return ""
140
 
 
141
 
            try:
142
 
                return ssh_output[self._call_number()]
143
 
            except IndexError:
144
 
                # If we ran out of values, just return the last one
145
 
                return ssh_output[-1]
146
 
 
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
154
 
 
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)
167
 
 
168
 
            if args.name:
169
 
                self.add_service(args.name)
170
 
            else:
171
 
                self.add_service(args.charm)
172
 
 
173
 
            self._next_service_machine += 1
174
 
        elif cmd == 'add-unit':
175
 
            self.add_unit(args)
176
 
 
177
 
        elif cmd == 'scp':
178
 
            pass
179
 
 
180
 
        else:
181
 
            raise ValueError("Unpatched command: {} {}".format(cmd, args))
182
 
 
183
 
    @contextmanager
184
 
    def juju_async(self, cmd, args):
185
 
        self.juju(cmd, args)
186
 
        yield
187
 
        pass
188
 
 
189
 
    def _call_number(self):
190
 
        call_number = self._call_n
191
 
        self._call_n += 1
192
 
        return call_number
193
 
 
194
 
    def get_status(self, machine_id=None):
195
 
        return Status(deepcopy(self._status), "")
196
 
 
197
 
    def set_status(self, status):
198
 
        self._status = deepcopy(status)
199
 
 
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)
203
 
        else:
204
 
            self._ssh_output = deepcopy(ssh_output)
205
 
 
206
 
    def reset_calls(self):
207
 
        self._call_n = 0
208
 
 
209
 
 
210
 
class JujuMockTestCase(TestCase):
211
 
    def setUp(self):
212
 
        self.client = EnvJujuClient(
213
 
            JujuData('foo', {'type': 'local'}), '1.234-76', None)
214
 
 
215
 
        def nil_func():
216
 
            return None
217
 
 
218
 
        self.juju_mock = JujuMock()
219
 
        self.ssh_mock = Mock()
220
 
 
221
 
        patches = [
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),
229
 
        ]
230
 
 
231
 
        for patcher in patches:
232
 
            patcher.start()
233
 
            self.addCleanup(patcher.stop)
234
 
 
235
 
 
236
 
class TestSubnetsSpaces(JujuMockTestCase):
237
 
    def test_ipv4_to_int(self):
238
 
        self.assertEqual(
239
 
            jss.ipv4_to_int('1.2.3.4'),
240
 
            0x01020304)
241
 
 
242
 
        self.assertEqual(
243
 
            jss.ipv4_to_int('255.255.255.255'),
244
 
            0xFFFFFFFF)
245
 
 
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'))
252
 
 
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'))
257
 
 
258
 
    network_config = {
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'],
263
 
    }
264
 
    charms_to_space = {
265
 
        'haproxy': {'space': 'dmz'},
266
 
        'mediawiki': {'space': 'apps'},
267
 
        'memcached': {'space': 'apps'},
268
 
        'mysql': {'space': 'backend'},
269
 
        'mysql-slave': {
270
 
            'space': 'backend',
271
 
            'charm': 'mysql',
272
 
        },
273
 
    }
274
 
 
275
 
    def test_assess_spaces_subnets(self):
276
 
        # The following table is derived from the above settings
277
 
 
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
282
 
 
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
286
 
        # in a sorted order.
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')
294
 
 
295
 
        jss._assess_spaces_subnets(
296
 
            self.client, self.network_config, self.charms_to_space)
297
 
 
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')
309
 
 
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)
314
 
 
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)