Merge lp:~dooferlad/juju-ci-tools/addressable-containers-assess into lp:juju-ci-tools
- addressable-containers-assess
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1173 |
Proposed branch: | lp:~dooferlad/juju-ci-tools/addressable-containers-assess |
Merge into: | lp:juju-ci-tools |
Prerequisite: | lp:~dooferlad/juju-ci-tools/addressable-containers-tools |
Diff against target: |
575 lines (+497/-7) 3 files modified
assess_container_networking.py (+342/-5) substrate.py (+2/-2) test_assess_container_networking.py (+153/-0) |
To merge this branch: | bzr merge lp:~dooferlad/juju-ci-tools/addressable-containers-assess |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (community) | Approve | ||
James Tunnicliffe (community) | Needs Resubmitting | ||
Review via email: mp+271837@code.launchpad.net |
Commit message
Description of the change
https:/
This adds tests that check container networking as set up by the Juju addressable containers feature.
James Tunnicliffe (dooferlad) wrote : | # |
Thanks Aaron,
Yes, your patch seems entirely reasonable. What I had evolved into
something much more complicated than was needed! Thanks for calling me out
on it.
On Tue, Sep 22, 2015 at 8:02 PM, Aaron Bentley <email address hidden>
wrote:
> Review: Needs Information
>
> Thanks.
>
> The file for testing assess_
> test_assess_
>
> You are treating args.juju_bin as if it's the path to the directory, but
> it's the path to the binary.
>
> I'm still having trouble understanding why we need something as
> complicated as MachineGetter. Here's a patch that I think could remove the
> need for it:
> https:/
>
> Does that seem sane? Just make a bunch of machines and containers and
> then use them?
>
> See also inline comments.
>
> Diff comments:
>
> > === modified file 'assess_
> > --- assess_
> > +++ assess_
> > @@ -148,3 +218,210 @@
> > self.requested_
> > if self._new_
> > return
> > +
> > +
> > +def find_network(
> > + """Find a connected subnet containing the given address.
> > +
> > + When using this to find the subnet of a container, don't use the
> container
> > + as the machine to run the ip route show command on ("machine"), use
> a real
> > + box because lxc will just send everything to its host machine, so
> it is on
> > + a subnet containing itself. Not much use.
> > + :param client: A Juju client
> > + :param machine: ID of the machine on which to run a command
> > + :param addr: find the connected subnet containing this address
> > + :return: CIDR containing the address if found, else, None
> > + """
> > + ip_cmd = ' '.join(['ip', 'route', 'show', 'to', 'match', addr])
> > + routes = ssh(client, machine, ip_cmd)
> > +
> > + for route in re.findall(
> re.MULTILINE):
> > + if route != 'default':
> > + return route
> > +
> > + raise ValueError("Unable to find route to %r" % addr)
> > +
> > +
> > +def assess_
> > + """Test that all containers in target can talk to target[0]
> > + :param client: Juju client
> > + :param targets: machine IDs of machines to test
> > + :return: None;
> > + """
> > + status = client.
> > + source = targets[0]
> > + dests = targets[1:]
> > +
> > + with tempfile.
> > + f.write('tmux new-session -d -s test "nc -l 6778 >
> nc_listen.out"')
> > + client.juju('scp', (f.name, source + ':/home/
> > + os.remove(f.name)
> > +
> > + # Containers are named 'x/type/y' where x is the host of the
> container. We
> > + host = source.
> > + address = status[
> > +
> > + for dest in dests:
> > + ssh(client, source, 'rm nc_listen.out; bash ./listen.sh')
> > + ssh(client...
- 1094. By James Tunnicliffe
-
retry ssh commands if they fail with code 255, which seems to happen on heavily loaded machines.
- 1095. By James Tunnicliffe
-
Fixed missing import
- 1096. By James Tunnicliffe
-
SSH wrapper now trying even harder to retry on connection errors.
clean_environment is more able to cope with Juju getting confused and retrying.
Use random data in assess_network_ traffic to avoid stale data triggering false positive in tests.
Not using asserts.
Clean out machines from the environment again (accidental check in of debug code).
Fixed dumping logs at the end of the test.
Better fix for EnvJujuClient.get_status. - 1097. By James Tunnicliffe
-
Merge from addressable-
containers- tools - 1098. By James Tunnicliffe
-
A bit more reliability hacking around ssh and reboots.
Find a private address to ping for testing rather than 8.8.8.8.
Generate the correct exit code on failure.
join URLs using urljoin, not string concatination (copes with if a / is needed or not). This may need to be expanded to more areas of the code. - 1099. By James Tunnicliffe
-
merge upstream changes
- 1100. By James Tunnicliffe
-
Merged upstream
Aaron Bentley (abentley) wrote : | # |
This looks pretty good.
There are bunch of files that you've removed the execute bit from. Please don't do that.
See other comments inline.
- 1101. By James Tunnicliffe
-
Merge latest addressable-
containers- tools - 1102. By James Tunnicliffe
-
Fixed up tests.
- 1103. By James Tunnicliffe
-
Removed unused imports
- 1104. By James Tunnicliffe
-
Removed unused variable
- 1105. By James Tunnicliffe
-
Fixed up tests.
Use wait_for_port instead of a simple delay where possible.
Aaron Bentley (abentley) wrote : | # |
You haven't addressed these comments from 2015-11-13:
+ for host in hosts:
280 + ssh(client, host, 'sudo reboot')
281 +
282 + # If wait_for_started is called too early the machines still appear to be
283 + # up, so we wait a few seconds for the reboot.
284 + time.sleep(10)
Aaron Bentley (abentley) wrote on 2015-11-13:
I think it would be more reliable to use wait_for_port(22, closed=True) here.
365 --- substrate.py 2015-09-17 20:39:25 +0000
366 +++ substrate.py 2015-11-11 19:51:39 +0000
367 @@ -1,3 +1,4 @@
368 +__metaclass__ = type
Aaron Bentley (abentley) wrote on 2015-11-13:
Assigning metaclass must go below any imports or it will break lint on wily.
372 @@ -6,7 +7,7 @@
373 import os
374 import subprocess
375 from time import sleep
376 -
377 +import urlparse
378 from utility import temp_dir
Aaron Bentley (abentley) wrote on 2015-11-13:
There should be a blank line between standard library imports and local application imports.
- 1106. By James Tunnicliffe
-
Fixed subnets/
<ver>/? op=reserved_ ip_ranges when no IP addresses had been explicitly added.
James Tunnicliffe (dooferlad) wrote : | # |
Sorry, tried wait_for_
- should have commented on the merge proposal. Of course today when I
try again it has worked perfectly three times in a row and failed with
a DNS error once, which I am not going to blame it for. Whatever I am
doing differently today seems to be fine.
On Mon, Nov 23, 2015 at 5:15 PM, Aaron Bentley
<email address hidden> wrote:
> You haven't addressed these comments from 2015-11-13:
> + for host in hosts:
> 280 + ssh(client, host, 'sudo reboot')
> 281 +
> 282 + # If wait_for_started is called too early the machines still appear to be
> 283 + # up, so we wait a few seconds for the reboot.
> 284 + time.sleep(10)
> Aaron Bentley (abentley) wrote on 2015-11-13:
> I think it would be more reliable to use wait_for_port(22, closed=True) here.
>
> 365 --- substrate.py 2015-09-17 20:39:25 +0000
> 366 +++ substrate.py 2015-11-11 19:51:39 +0000
> 367 @@ -1,3 +1,4 @@
> 368 +__metaclass__ = type
> Aaron Bentley (abentley) wrote on 2015-11-13:
> Assigning metaclass must go below any imports or it will break lint on wily.
>
>
> 372 @@ -6,7 +7,7 @@
> 373 import os
> 374 import subprocess
> 375 from time import sleep
> 376 -
> 377 +import urlparse
> 378 from utility import temp_dir
> Aaron Bentley (abentley) wrote on 2015-11-13:
> There should be a blank line between standard library imports and local application imports.
> --
> https:/
> You are the owner of lp:~dooferlad/juju-ci-tools/addressable-containers-assess.
- 1107. By James Tunnicliffe
-
Doh!
- 1108. By James Tunnicliffe
-
Fixed up reboot ordering and waits.
James Tunnicliffe (dooferlad) wrote : | # |
That should do it. Please review.
Aaron Bentley (abentley) wrote : | # |
Thanks for your changes. You haven't addressed this comment yet:
372 @@ -6,7 +7,7 @@
373 import os
374 import subprocess
375 from time import sleep
376 -
377 +import urlparse
378 from utility import temp_dir
Aaron Bentley (abentley) wrote on 2015-11-13:
There should be a blank line between standard library imports and local application imports.
- 1109. By James Tunnicliffe
-
Fixed import spacing
James Tunnicliffe (dooferlad) wrote : | # |
Bother. Sorry. Fix on its way.
On Fri, Nov 27, 2015 at 3:06 PM, Aaron Bentley
<email address hidden> wrote:
> Thanks for your changes. You haven't addressed this comment yet:
>
> 372 @@ -6,7 +7,7 @@
> 373 import os
> 374 import subprocess
> 375 from time import sleep
> 376 -
> 377 +import urlparse
> 378 from utility import temp_dir
> Aaron Bentley (abentley) wrote on 2015-11-13:
> There should be a blank line between standard library imports and local application imports.
> --
> https:/
> You are the owner of lp:~dooferlad/juju-ci-tools/addressable-containers-assess.
James Tunnicliffe (dooferlad) wrote : | # |
Fix pushed.
Aaron Bentley (abentley) wrote : | # |
You have added a blank line between "standard library imports" and "local application imports", but you have also removed the blank line between "related third party imports" and "local application imports". I will fix this, but in the future, please try to follow PEP8 style more closely for imports.
Preview Diff
1 | === modified file 'assess_container_networking.py' |
2 | --- assess_container_networking.py 2015-09-25 15:52:47 +0000 |
3 | +++ assess_container_networking.py 2015-11-27 15:17:35 +0000 |
4 | @@ -1,18 +1,99 @@ |
5 | #!/usr/bin/env python |
6 | from __future__ import print_function |
7 | -import subprocess |
8 | from copy import ( |
9 | copy, |
10 | deepcopy, |
11 | ) |
12 | +import logging |
13 | import time |
14 | - |
15 | -__metaclass__ = type |
16 | - |
17 | +import re |
18 | +import tempfile |
19 | +import os |
20 | +import subprocess |
21 | +from textwrap import dedent |
22 | +from argparse import ArgumentParser |
23 | + |
24 | +from jujuconfig import ( |
25 | + get_juju_home, |
26 | +) |
27 | +from jujupy import ( |
28 | + parse_new_state_server_from_error, |
29 | + temp_bootstrap_env, |
30 | + SimpleEnvironment, |
31 | + EnvJujuClient, |
32 | +) |
33 | +from utility import ( |
34 | + wait_for_port, |
35 | + print_now, |
36 | + add_basic_testing_arguments, |
37 | +) |
38 | +from deploy_stack import ( |
39 | + update_env, |
40 | + dump_env_logs, |
41 | + get_random_string, |
42 | +) |
43 | |
44 | KVM_MACHINE = 'kvm' |
45 | LXC_MACHINE = 'lxc' |
46 | |
47 | +__metaclass__ = type |
48 | + |
49 | + |
50 | +def parse_args(argv=None): |
51 | + """Parse all arguments.""" |
52 | + |
53 | + description = dedent("""\ |
54 | + Test container address allocation. |
55 | + For LXC and KVM, create machines of each type and test the network |
56 | + between LXC <--> LXC, KVM <--> KVM and LXC <--> KVM. Also test machine |
57 | + to outside world, DNS and that these tests still pass after a reboot. In |
58 | + case of failure pull logs and configuration files from the machine that |
59 | + we detected a problem on for later analysis. |
60 | + """) |
61 | + parser = add_basic_testing_arguments(ArgumentParser( |
62 | + description=description |
63 | + )) |
64 | + parser.add_argument( |
65 | + '--machine-type', |
66 | + help='Which virtual machine/container type to test. Defaults to all.', |
67 | + choices=[KVM_MACHINE, LXC_MACHINE]) |
68 | + parser.add_argument( |
69 | + '--clean-environment', action='store_true', help=dedent("""\ |
70 | + Attempts to re-use an existing environment rather than destroying it |
71 | + and creating a new one. |
72 | + |
73 | + On launch, if an environment exists, clean out services and machines |
74 | + from it rather than destroying it. If an environment doesn't exist, |
75 | + create one and use it. |
76 | + |
77 | + At termination, clean out services and machines from the environment |
78 | + rather than destroying it.""")) |
79 | + return parser.parse_args(argv) |
80 | + |
81 | + |
82 | +def ssh(client, machine, cmd): |
83 | + """Convenience function: run a juju ssh command and get back the output |
84 | + :param client: A Juju client |
85 | + :param machine: ID of the machine on which to run a command |
86 | + :param cmd: the command to run |
87 | + :return: text output of the command |
88 | + """ |
89 | + back_off = 2 |
90 | + attempts = 4 |
91 | + for attempt in range(attempts): |
92 | + try: |
93 | + return client.get_juju_output('ssh', machine, cmd) |
94 | + except subprocess.CalledProcessError as e: |
95 | + # If the connection to the host failed, try again in a couple of |
96 | + # seconds. This is usually due to heavy load. |
97 | + if(attempt < attempts-1 and |
98 | + re.search('ssh_exchange_identification: ' |
99 | + 'Connection closed by remote host', e.stderr)): |
100 | + time.sleep(back_off) |
101 | + back_off *= 2 |
102 | + else: |
103 | + raise |
104 | + |
105 | |
106 | def clean_environment(client, services_only=False): |
107 | """Remove all the services and, optionally, machines from an environment. |
108 | @@ -49,7 +130,7 @@ |
109 | # containers on it. Normally a small pause and trying |
110 | # again is all that is needed to resolve this issue. |
111 | time.sleep(2) |
112 | - s = client.wait_for_started() |
113 | + client.wait_for_started() |
114 | client.juju('remove-machine', m) |
115 | |
116 | client.wait_for('machines-not-0', 'none') |
117 | @@ -102,3 +183,259 @@ |
118 | containers[type][host].append(c[0]) |
119 | |
120 | return hosts, containers |
121 | + |
122 | + |
123 | +def find_network(client, machine, addr): |
124 | + """Find a connected subnet containing the given address. |
125 | + |
126 | + When using this to find the subnet of a container, don't use the container |
127 | + as the machine to run the ip route show command on ("machine"), use a real |
128 | + box because lxc will just send everything to its host machine, so it is on |
129 | + a subnet containing itself. Not much use. |
130 | + :param client: A Juju client |
131 | + :param machine: ID of the machine on which to run a command |
132 | + :param addr: find the connected subnet containing this address |
133 | + :return: CIDR containing the address if found, else, None |
134 | + """ |
135 | + ip_cmd = ' '.join(['ip', 'route', 'show', 'to', 'match', addr]) |
136 | + routes = ssh(client, machine, ip_cmd) |
137 | + |
138 | + for route in re.findall(r'^(\S+).*[\d\.]+/\d+', routes, re.MULTILINE): |
139 | + if route != 'default': |
140 | + return route |
141 | + |
142 | + raise ValueError("Unable to find route to %r" % addr) |
143 | + |
144 | + |
145 | +def assess_network_traffic(client, targets): |
146 | + """Test that all containers in target can talk to target[0] |
147 | + :param client: Juju client |
148 | + :param targets: machine IDs of machines to test |
149 | + :return: None; |
150 | + """ |
151 | + status = client.wait_for_started().status |
152 | + source = targets[0] |
153 | + dests = targets[1:] |
154 | + |
155 | + with tempfile.NamedTemporaryFile(delete=False) as f: |
156 | + f.write('tmux new-session -d -s test "nc -l 6778 > nc_listen.out"') |
157 | + client.juju('scp', (f.name, source + ':/home/ubuntu/listen.sh')) |
158 | + os.remove(f.name) |
159 | + |
160 | + # Containers are named 'x/type/y' where x is the host of the container. We |
161 | + host = source.split('/')[0] |
162 | + address = status['machines'][host]['containers'][source]['dns-name'] |
163 | + |
164 | + for dest in dests: |
165 | + msg = get_random_string() |
166 | + ssh(client, source, 'rm nc_listen.out; bash ./listen.sh') |
167 | + ssh(client, dest, |
168 | + 'echo "{msg}" | nc {addr} 6778'.format(msg=msg, addr=address)) |
169 | + result = ssh(client, source, 'more nc_listen.out') |
170 | + if result.rstrip() != msg: |
171 | + raise ValueError("Wrong or missing message: %r" % result.rstrip()) |
172 | + |
173 | + |
174 | +def private_address(client, host): |
175 | + default_route = ssh(client, host, 'ip -4 -o route list 0/0') |
176 | + device = re.search(r'(\w+)\s*$', default_route).group(1) |
177 | + device_ip = ssh(client, host, 'ip -4 -o addr show {}'.format(device)) |
178 | + return re.search(r'inet\s+(\S+)/\d+\s', device_ip).group(1) |
179 | + |
180 | + |
181 | +def assess_address_range(client, targets): |
182 | + """Test that two containers are in the same subnet as their host |
183 | + :param client: Juju client |
184 | + :param targets: machine IDs of machines to test |
185 | + :return: None; raises ValueError on failure |
186 | + """ |
187 | + status = client.wait_for_started().status |
188 | + |
189 | + host_subnet_cache = {} |
190 | + |
191 | + for target in targets: |
192 | + host = target.split('/')[0] |
193 | + |
194 | + if host in host_subnet_cache: |
195 | + host_subnet = host_subnet_cache[host] |
196 | + else: |
197 | + host_address = private_address(client, host) |
198 | + host_subnet = find_network(client, host, host_address) |
199 | + host_subnet_cache[host] = host_subnet |
200 | + |
201 | + addr = status['machines'][host]['containers'][target]['dns-name'] |
202 | + subnet = find_network(client, host, addr) |
203 | + if host_subnet != subnet: |
204 | + raise ValueError( |
205 | + '{} ({}) not on the same subnet as {} ({})'.format( |
206 | + target, subnet, host, host_subnet)) |
207 | + |
208 | + |
209 | +def assess_internet_connection(client, targets): |
210 | + """Test that targets can ping their default route |
211 | + :param client: Juju client |
212 | + :param targets: machine IDs of machines to test |
213 | + :return: None; raises ValueError on failure |
214 | + """ |
215 | + |
216 | + for target in targets: |
217 | + routes = ssh(client, target, 'ip route show') |
218 | + |
219 | + d = re.search(r'^default\s+via\s+([\d\.]+)\s+', routes, re.MULTILINE) |
220 | + if d: |
221 | + rc = client.juju('ssh', (target, 'ping -c1 -q ' + d.group(1)), |
222 | + check=False) |
223 | + if rc != 0: |
224 | + raise ValueError('%s unable to ping default route' % target) |
225 | + else: |
226 | + raise ValueError("Default route not found") |
227 | + |
228 | + |
229 | +def _assessment_iteration(client, containers): |
230 | + """Run the network tests on this collection of machines and containers |
231 | + :param client: Juju client |
232 | + :param hosts: list of hosts of containers |
233 | + :param containers: list of containers to run tests between |
234 | + :return: None |
235 | + """ |
236 | + assess_internet_connection(client, containers) |
237 | + assess_address_range(client, containers) |
238 | + assess_network_traffic(client, containers) |
239 | + |
240 | + |
241 | +def _assess_container_networking(client, types, hosts, containers): |
242 | + """Run _assessment_iteration on all useful combinations of containers |
243 | + :param client: Juju client |
244 | + :param args: Parsed command line arguments |
245 | + :return: None |
246 | + """ |
247 | + for container_type in types: |
248 | + # Test with two containers on the same host |
249 | + _assessment_iteration(client, containers[container_type][hosts[0]]) |
250 | + |
251 | + # Now test with two containers on two different hosts |
252 | + test_containers = [ |
253 | + containers[container_type][hosts[0]][0], |
254 | + containers[container_type][hosts[1]][0], |
255 | + ] |
256 | + _assessment_iteration(client, test_containers) |
257 | + |
258 | + if KVM_MACHINE in types and LXC_MACHINE in types: |
259 | + test_containers = [ |
260 | + containers[LXC_MACHINE][hosts[0]][0], |
261 | + containers[KVM_MACHINE][hosts[0]][0], |
262 | + ] |
263 | + _assessment_iteration(client, test_containers) |
264 | + |
265 | + # Test with an LXC and a KVM on different machines |
266 | + test_containers = [ |
267 | + containers[LXC_MACHINE][hosts[0]][0], |
268 | + containers[KVM_MACHINE][hosts[1]][0], |
269 | + ] |
270 | + _assessment_iteration(client, test_containers) |
271 | + |
272 | + |
273 | +def assess_container_networking(client, args): |
274 | + """Runs _assess_address_allocation, reboots hosts, repeat. |
275 | + :param client: Juju client |
276 | + :param args: Parsed command line arguments |
277 | + :return: None |
278 | + """ |
279 | + # Only test the containers we were asked to test |
280 | + if args.machine_type: |
281 | + types = [args.machine_type] |
282 | + else: |
283 | + types = [KVM_MACHINE, LXC_MACHINE] |
284 | + |
285 | + hosts, containers = make_machines(client, types) |
286 | + status = client.wait_for_started().status |
287 | + |
288 | + _assess_container_networking(client, types, hosts, containers) |
289 | + |
290 | + # Reboot all hosts apart from machine 0 because we use machine 0 to jump |
291 | + # through for some hosts. |
292 | + for host in hosts[1:]: |
293 | + ssh(client, host, 'sudo reboot') |
294 | + |
295 | + # Finally reboot machine 0 |
296 | + ssh(client, hosts[0], 'sudo reboot') |
297 | + |
298 | + # Wait for the state server to shut down. This prevents us from calling |
299 | + # wait_for_started before machine 0 has shut down, which can cause us |
300 | + # to think that we have finished rebooting before we actually have. |
301 | + hostname = status['machines'][hosts[0]]['dns-name'] |
302 | + wait_for_port(hostname, 22, closed=True) |
303 | + |
304 | + client.wait_for_started() |
305 | + |
306 | + # Once Juju is up it can take a little while before ssh responds. |
307 | + for host in hosts: |
308 | + hostname = status['machines'][host]['dns-name'] |
309 | + wait_for_port(hostname, 22, timeout=240) |
310 | + |
311 | + _assess_container_networking(client, types, hosts, containers) |
312 | + |
313 | + |
314 | +def get_client(args): |
315 | + client = EnvJujuClient.by_version( |
316 | + SimpleEnvironment.from_config(args.env), |
317 | + args.juju_bin, args.debug |
318 | + ) |
319 | + client.enable_container_address_allocation() |
320 | + update_env(client.env, args.temp_env_name) |
321 | + return client |
322 | + |
323 | + |
324 | +def main(): |
325 | + args = parse_args() |
326 | + client = get_client(args) |
327 | + juju_home = get_juju_home() |
328 | + bootstrap_host = None |
329 | + success = True |
330 | + try: |
331 | + if args.clean_environment: |
332 | + try: |
333 | + if not clean_environment(client): |
334 | + with temp_bootstrap_env(juju_home, client): |
335 | + client.bootstrap(args.upload_tools) |
336 | + except Exception as e: |
337 | + logging.exception(e) |
338 | + client.destroy_environment() |
339 | + client = get_client(args) |
340 | + with temp_bootstrap_env(juju_home, client): |
341 | + client.bootstrap(args.upload_tools) |
342 | + else: |
343 | + client.destroy_environment() |
344 | + client = get_client(args) |
345 | + with temp_bootstrap_env(juju_home, client): |
346 | + client.bootstrap(args.upload_tools) |
347 | + |
348 | + logging.info('Waiting for the bootstrap machine agent to start.') |
349 | + status = client.wait_for_started() |
350 | + mid, data = list(status.iter_machines())[0] |
351 | + bootstrap_host = data['dns-name'] |
352 | + |
353 | + assess_container_networking(client, args) |
354 | + |
355 | + except Exception as e: |
356 | + success = False |
357 | + logging.exception(e) |
358 | + try: |
359 | + if bootstrap_host is None: |
360 | + bootstrap_host = parse_new_state_server_from_error(e) |
361 | + except Exception as e: |
362 | + print_now('exception while dumping logs:\n') |
363 | + logging.exception(e) |
364 | + finally: |
365 | + if bootstrap_host is not None: |
366 | + dump_env_logs(client, bootstrap_host, args.logs) |
367 | + |
368 | + if args.clean_environment: |
369 | + clean_environment(client) |
370 | + else: |
371 | + client.destroy_environment() |
372 | + if not success: |
373 | + exit(1) |
374 | + |
375 | +if __name__ == '__main__': |
376 | + main() |
377 | |
378 | === modified file 'substrate.py' |
379 | --- substrate.py 2015-11-13 16:27:48 +0000 |
380 | +++ substrate.py 2015-11-27 15:17:35 +0000 |
381 | @@ -6,9 +6,9 @@ |
382 | import os |
383 | import subprocess |
384 | from time import sleep |
385 | +import urlparse |
386 | |
387 | from utility import temp_dir |
388 | - |
389 | from boto import ec2 |
390 | from boto.exception import EC2ResponseError |
391 | |
392 | @@ -395,7 +395,7 @@ |
393 | |
394 | def __init__(self, profile, url, oauth): |
395 | self.profile = profile |
396 | - self.url = url + 'api/1.0/' |
397 | + self.url = urlparse.urljoin(url, 'api/1.0/') |
398 | self.oauth = oauth |
399 | |
400 | @classmethod |
401 | |
402 | === modified file 'test_assess_container_networking.py' |
403 | --- test_assess_container_networking.py 2015-11-13 16:35:35 +0000 |
404 | +++ test_assess_container_networking.py 2015-11-27 15:17:35 +0000 |
405 | @@ -12,6 +12,8 @@ |
406 | import assess_container_networking as jcnet |
407 | from copy import deepcopy |
408 | from contextlib import contextmanager |
409 | +from tests import parse_error |
410 | + |
411 | |
412 | __metaclass__ = type |
413 | |
414 | @@ -162,6 +164,68 @@ |
415 | patcher.start() |
416 | self.addCleanup(patcher.stop) |
417 | |
418 | + def assert_ssh(self, args, machine, cmd): |
419 | + self.assertEqual(args, [('ssh', machine, cmd), ]) |
420 | + |
421 | + def test_parse_args(self): |
422 | + # Test a simple command line that should work |
423 | + cmdline = ['env', 'juju_bin', 'logs', 'ten'] |
424 | + args = jcnet.parse_args(cmdline) |
425 | + self.assertEqual(args.machine_type, None) |
426 | + self.assertEqual(args.juju_bin, 'juju_bin') |
427 | + self.assertEqual(args.env, 'env') |
428 | + self.assertEqual(args.logs, 'logs') |
429 | + self.assertEqual(args.temp_env_name, 'ten') |
430 | + self.assertEqual(args.debug, False) |
431 | + self.assertEqual(args.upload_tools, False) |
432 | + self.assertEqual(args.clean_environment, False) |
433 | + |
434 | + # check the optional arguments |
435 | + opts = ['--machine-type', jcnet.KVM_MACHINE, '--debug', |
436 | + '--upload-tools', '--clean-environment'] |
437 | + args = jcnet.parse_args(cmdline + opts) |
438 | + self.assertEqual(args.machine_type, jcnet.KVM_MACHINE) |
439 | + self.assertEqual(args.debug, True) |
440 | + self.assertEqual(args.upload_tools, True) |
441 | + self.assertEqual(args.clean_environment, True) |
442 | + |
443 | + # Now check that we can only set machine_type to kvm or lxc |
444 | + opts = ['--machine-type', jcnet.LXC_MACHINE] |
445 | + args = jcnet.parse_args(cmdline + opts) |
446 | + self.assertEqual(args.machine_type, jcnet.LXC_MACHINE) |
447 | + |
448 | + # Set up an error (bob is invalid) |
449 | + opts = ['--machine-type', 'bob'] |
450 | + with parse_error(self) as stderr: |
451 | + jcnet.parse_args(cmdline + opts) |
452 | + self.assertRegexpMatches( |
453 | + stderr.getvalue(), |
454 | + ".*error: argument --machine-type: invalid choice: 'bob'.*") |
455 | + |
456 | + def test_ssh(self): |
457 | + machine, addr = '0', 'foobar' |
458 | + with patch.object(self.client, 'get_juju_output', |
459 | + autospec=True) as ssh_mock: |
460 | + jcnet.ssh(self.client, machine, addr) |
461 | + self.assertEqual(1, ssh_mock.call_count) |
462 | + self.assert_ssh(ssh_mock.call_args, machine, addr) |
463 | + |
464 | + def test_find_network(self): |
465 | + machine, addr = '0', '1.1.1.1' |
466 | + self.assertRaisesRegexp( |
467 | + ValueError, "Unable to find route to '1.1.1.1'", |
468 | + jcnet.find_network, self.client, machine, addr) |
469 | + |
470 | + self.juju_mock.set_ssh_output([ |
471 | + 'default via 192.168.0.1 dev eth3\n' |
472 | + '1.1.1.0/24 dev eth3 proto kernel scope link src 1.1.1.22', |
473 | + ]) |
474 | + self.juju_mock.commands = [] |
475 | + jcnet.find_network(self.client, machine, addr) |
476 | + self.assertItemsEqual(self.juju_mock.commands, |
477 | + [('ssh', ( |
478 | + machine, 'ip route show to match ' + addr))]) |
479 | + |
480 | def test_clean_environment(self): |
481 | self.juju_mock.add_machine('1') |
482 | self.juju_mock.add_machine('2') |
483 | @@ -229,3 +293,92 @@ |
484 | '1': ['1/kvm/0']}, |
485 | } |
486 | self.assertDictEqual(containers, expected) |
487 | + |
488 | + def test_test_network_traffic(self): |
489 | + targets = ['0/lxc/0', '0/lxc/1'] |
490 | + self.juju_mock.set_status({'machines': {'0': { |
491 | + 'containers': {targets[0]: {'dns-name': '0-dns-name'}}}}}) |
492 | + |
493 | + with patch('assess_container_networking.get_random_string', |
494 | + lambda *args, **kw: 'hello'): |
495 | + |
496 | + self.juju_mock.set_ssh_output(['', '', 'hello']) |
497 | + jcnet.assess_network_traffic(self.client, targets) |
498 | + |
499 | + self.juju_mock.reset_calls() |
500 | + self.juju_mock.set_ssh_output(['', '', 'fail']) |
501 | + self.assertRaisesRegexp( |
502 | + ValueError, "Wrong or missing message: 'fail'", |
503 | + jcnet.assess_network_traffic, self.client, targets) |
504 | + |
505 | + def test_test_address_range(self): |
506 | + targets = ['0/lxc/0', '0/lxc/1'] |
507 | + self.juju_mock.set_status({'machines': {'0': { |
508 | + 'containers': { |
509 | + targets[0]: {'dns-name': 'lxc0-dns-name'}, |
510 | + targets[1]: {'dns-name': 'lxc1-dns-name'}, |
511 | + }, |
512 | + 'dns-name': '0-dns-name', |
513 | + }}}) |
514 | + self.juju_mock.set_ssh_output([ |
515 | + 'default via 192.168.0.1 dev eth3', |
516 | + '2: eth3 inet 192.168.0.22/24 brd 192.168.0.255 scope ' |
517 | + 'global eth3\ valid_lft forever preferred_lft forever', |
518 | + '192.168.0.0/24', |
519 | + ]) |
520 | + |
521 | + jcnet.assess_address_range(self.client, targets) |
522 | + |
523 | + def test_test_address_range_fail(self): |
524 | + targets = ['0/lxc/0', '0/lxc/1'] |
525 | + self.juju_mock.set_status({'machines': {'0': { |
526 | + 'containers': { |
527 | + targets[0]: {'dns-name': 'lxc0-dns-name'}, |
528 | + targets[1]: {'dns-name': 'lxc1-dns-name'}, |
529 | + }, |
530 | + 'dns-name': '0-dns-name', |
531 | + }}}) |
532 | + self.juju_mock.set_ssh_output([ |
533 | + 'default via 192.168.0.1 dev eth3', |
534 | + '2: eth3 inet 192.168.0.22/24 brd 192.168.0.255 scope ' |
535 | + 'global eth3\ valid_lft forever preferred_lft forever', |
536 | + '192.168.0.0/24', |
537 | + '192.168.1.0/24', |
538 | + '192.168.2.0/24', |
539 | + '192.168.3.0/24', |
540 | + ]) |
541 | + |
542 | + self.assertRaisesRegexp( |
543 | + ValueError, '0/lxc/0 \S+ not on the same subnet as 0 \S+', |
544 | + jcnet.assess_address_range, self.client, targets) |
545 | + |
546 | + def test_test_internet_connection(self): |
547 | + targets = ['0/lxc/0', '0/lxc/1'] |
548 | + self.juju_mock.set_status({'machines': {'0': { |
549 | + 'containers': { |
550 | + targets[0]: {'dns-name': 'lxc0-dns-name'}, |
551 | + targets[1]: {'dns-name': 'lxc1-dns-name'}, |
552 | + }, |
553 | + 'dns-name': '0-dns-name', |
554 | + }}}) |
555 | + |
556 | + # Can ping default route |
557 | + self.juju_mock.set_ssh_output([ |
558 | + 'default via 192.168.0.1 dev eth3', 0, |
559 | + 'default via 192.168.0.1 dev eth3', 0]) |
560 | + jcnet.assess_internet_connection(self.client, targets) |
561 | + |
562 | + # Can't ping default route |
563 | + self.juju_mock.set_ssh_output([ |
564 | + 'default via 192.168.0.1 dev eth3', 1]) |
565 | + self.juju_mock.reset_calls() |
566 | + self.assertRaisesRegexp( |
567 | + ValueError, "0/lxc/0 unable to ping default route", |
568 | + jcnet.assess_internet_connection, self.client, targets) |
569 | + |
570 | + # Can't find default route |
571 | + self.juju_mock.set_ssh_output(['', 1]) |
572 | + self.juju_mock.reset_calls() |
573 | + self.assertRaisesRegexp( |
574 | + ValueError, "Default route not found", |
575 | + jcnet.assess_internet_connection, self.client, targets) |
Thanks.
The file for testing assess_ container_ networking should be named test_assess_ container_ networking.
You are treating args.juju_bin as if it's the path to the directory, but it's the path to the binary.
I'm still having trouble understanding why we need something as complicated as MachineGetter. Here's a patch that I think could remove the need for it: /pastebin. canonical. com/140360/
https:/
Does that seem sane? Just make a bunch of machines and containers and then use them?
See also inline comments.