Merge lp:~dooferlad/juju-ci-tools/addressable-containers-assess into lp:juju-ci-tools

Proposed by James Tunnicliffe
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
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
James Tunnicliffe (community) Needs Resubmitting
Review via email: mp+271837@code.launchpad.net

Description of the change

https://code.launchpad.net/~dooferlad/juju-ci-tools/juju-ci-tools-addressable-containers/+merge/267494 has been split into two changes. This is the second.

This adds tests that check container networking as set up by the Juju addressable containers feature.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

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:
https://pastebin.canonical.com/140360/

Does that seem sane? Just make a bunch of machines and containers and then use them?

See also inline comments.

review: Needs Information
Revision history for this message
James Tunnicliffe (dooferlad) wrote :
Download full text (14.1 KiB)

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_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:
> https://pastebin.canonical.com/140360/
>
> 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_container_networking.py'
> > --- assess_container_networking.py 2015-09-21 16:08:26 +0000
> > +++ assess_container_networking.py 2015-09-21 16:08:26 +0000
> > @@ -148,3 +218,210 @@
> > self.requested_machines.append(machine)
> > if self._new_machine_count() == 0:
> > return
> > +
> > +
> > +def find_network(client, machine, addr):
> > + """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(r'^(\S+).*[\d\.]+/\d+', routes,
> re.MULTILINE):
> > + if route != 'default':
> > + return route
> > +
> > + raise ValueError("Unable to find route to %r" % addr)
> > +
> > +
> > +def assess_network_traffic(client, targets):
> > + """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.wait_for_started().status
> > + source = targets[0]
> > + dests = targets[1:]
> > +
> > + with tempfile.NamedTemporaryFile(delete=False) as f:
> > + f.write('tmux new-session -d -s test "nc -l 6778 >
> nc_listen.out"')
> > + client.juju('scp', (f.name, source + ':/home/ubuntu/listen.sh'))
> > + os.remove(f.name)
> > +
> > + # Containers are named 'x/type/y' where x is the host of the
> container. We
> > + host = source.split('/')[0]
> > + address = status['machines'][host]['containers'][source]['dns-name']
> > +
> > + 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

Revision history for this message
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.

review: Needs Fixing
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.

Revision history for this message
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.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Sorry, tried wait_for_port(hostname, 22, closed=True) but it was flaky
- 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://code.launchpad.net/~dooferlad/juju-ci-tools/addressable-containers-assess/+merge/271837
> 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.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

That should do it. Please review.

review: Needs Resubmitting
Revision history for this message
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

Revision history for this message
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://code.launchpad.net/~dooferlad/juju-ci-tools/addressable-containers-assess/+merge/271837
> You are the owner of lp:~dooferlad/juju-ci-tools/addressable-containers-assess.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Fix pushed.

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches