Merge lp:~vila/uci-engine/integration into lp:uci-engine
- integration
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Vincent Ladeuil |
Approved revision: | no longer in the source branch. |
Merged at revision: | 430 |
Proposed branch: | lp:~vila/uci-engine/integration |
Merge into: | lp:uci-engine |
Diff against target: |
2279 lines (+984/-641) 19 files modified
cli/tests/test_image.py (+18/-11) juju-deployer/deploy.py (+382/-51) juju-deployer/test_deploy.py (+243/-69) juju-deployer/test_run.py (+68/-84) juju-deployer/test_update.py (+14/-17) run-tests (+67/-9) tarmac.sh (+1/-1) test_runner/tstrun/__init__.py (+4/-1) test_runner/tstrun/tests/test_data_store.py (+4/-0) tests/deployers.py (+12/-12) tests/run.py (+49/-347) tests/test_data_store.py (+1/-2) tests/test_image_builder.py (+16/-3) tests/test_integration.py (+26/-9) tests/test_juju_gui.py (+20/-7) tests/test_ppa_assigner.py (+23/-8) tests/test_test_runner.py (+16/-3) tests/test_ticket_system.py (+19/-6) ticket_system/people/tests.py (+1/-1) |
To merge this branch: | bzr merge lp:~vila/uci-engine/integration |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Evan (community) | Approve | ||
PS Jenkins bot (community) | continuous-integration | Approve | |
Review via email: mp+215097@code.launchpad.net |
Commit message
Complete run-tests to support all tests defined in the project (still requires a venv).
Description of the change
This refactor deploy.py and run.py to share more code so run-tests can enjoy it too.
Some renaming had to be done for the integration test files so python can import them properly (and use a cleaner namespace).
At that point, run-tests still requires a venv but that MP is big enough, I'll address that in a followup.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:291
http://
Executed test runs:
Click here to trigger a rebuild:
http://
% time ./juju-
Traceback (most recent call last):
File "./juju-
main()
File "./juju-
check_
File "./juju-
current_value = os.environ[
File "/usr/lib/
raise KeyError(key)
KeyError: 'CI_PRIVATE_
This is because check_and_
% time ./juju-
Private PPAs only is DISABLED (CI_PRIVATE_
Checking juju status
Traceback (most recent call last):
File "./juju-
main()
File "./juju-
check_
File "./juju-
if not distutils.
AttributeError: 'module' object has no attribute 'spawn'
92 +import distutils
...
110 -from distutils import spawn
This should be import distutils.spawn
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:292
http://
Executed test runs:
Click here to trigger a rebuild:
http://
% time ./juju-
Private PPAs only is DISABLED (CI_PRIVATE_
Checking juju status
Preparing local branch upload...
No handlers could be found for logger "keystoneclient
Uploading local branch, fingerprint 20b63b0bb3e58ec
No revisions or tags to pull.
No revisions or tags to pull.
No revisions or tags to pull.
Traceback (most recent call last):
File "./juju-
main()
File "./juju-
setup()
File "./juju-
os.
UnboundLocalError: local variable 'working_dir' referenced before assignment
Looks like you're missing an indent on:
os.environ[
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:298
http://
Executed test runs:
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:299
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Ubuntu CI Bot (uci-bot) wrote : | # |
The attempt to merge lp:~vila/uci-engine/integration into lp:uci-engine failed. Below is the output from the failed tests.
New python executable in /tmp/tmp.
Installing distribute.
Installing pip....
== Testing ci-utils ....
Unpacking /tmp/tarmac/
Running setup.py egg_info for package from file://
warning: no previously-included files matching '*' found under directory 'docs/_build'
warning: no previously-included files matching '*.pyc' found under directory 'tests'
warning: no previously-included files matching '*.pyo' found under directory 'tests'
Installing collected packages: Babel
Running setup.py install for Babel
warning: no previously-included files matching '*' found under directory 'docs/_build'
warning: no previously-included files matching '*.pyc' found under directory 'tests'
warning: no previously-included files matching '*.pyo' found under directory 'tests'
Installing pybabel script to /tmp/tmp.
Successfully installed Babel
Cleaning up...
Using saved parent location: bzr+ssh:
No revisions or tags to pull.
Unpacking /tmp/tarmac/
Running setup.py egg_info for package from file://
[pbr] Processing SOURCES.txt
warning: LocalManifestMaker: standard file '-c' not found
warning: no previously-included files found matching '.gitignore'
warning: no previously-included files found matching '.gitreview'
warning: no previously-included files matching '*.pyc' found anywhere in distribution
warning: no previously-included files found matching '.gitignore'
warning: no previously-included files found matching '.gitreview'
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Installing collected packages: pbr
Running setup.py install for pbr
[pbr] Reusing existing SOURCES.txt
Successfully installed pbr
Cleaning up...
Using saved parent location: bzr+ssh:
No revisions or tags to pull.
Unpacking /tmp/tarmac/
Running setup.py egg_info for package from file://
Installing collected packages: iso8601
Running setup.py install for iso8601
Successfully installed iso8601
Cleaning up...
Using saved parent location: bzr+ssh:
No revisions or tags to pull.
Unpacking /tmp/tarmac/
Running setup.py egg_info for package from file://
Installing collected packages: prettytable
Running setup.py install for prettytable
Successfully installed pret...
- 430. By Vincent Ladeuil
-
[r=Evan Dandrea, PS Jenkins bot] Complete run-tests to support all tests defined in the project (still requires a venv). from Vincent Ladeuil
Preview Diff
1 | === modified file 'cli/tests/test_image.py' |
2 | --- cli/tests/test_image.py 2014-03-17 17:17:25 +0000 |
3 | +++ cli/tests/test_image.py 2014-04-15 12:18:41 +0000 |
4 | @@ -28,7 +28,6 @@ |
5 | from tests import capture_stdout |
6 | |
7 | import urllib2 |
8 | -urllib2.urlopen = mock.Mock() |
9 | |
10 | |
11 | class FakeUrlOpenReturn: |
12 | @@ -94,9 +93,10 @@ |
13 | if os.path.exists(self.image_path): |
14 | os.remove(self.image_path) |
15 | |
16 | + @mock.patch('urllib2.urlopen') |
17 | @mock.patch('ci_utils.data_store.create_for_ticket') |
18 | - def test_get_ticket_image(self, mock_create_datastore): |
19 | - urllib2.urlopen.side_effect = [ |
20 | + def test_get_ticket_image(self, mock_create_datastore, mock_urlopen): |
21 | + mock_urlopen.side_effect = [ |
22 | FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
23 | mock_datastore = mock.Mock() |
24 | mock_datastore.get_file.return_value = buffer(" ") |
25 | @@ -111,9 +111,11 @@ |
26 | "Image file can be found at: {}\n".format(self.image_path)) |
27 | mock_datastore.assert_called_once() |
28 | |
29 | + @mock.patch('urllib2.urlopen') |
30 | @mock.patch('ci_utils.data_store.create_for_ticket') |
31 | - def test_get_ticket_image_not_found(self, mock_create_datastore): |
32 | - urllib2.urlopen.side_effect = [ |
33 | + def test_get_ticket_image_not_found(self, mock_create_datastore, |
34 | + mock_urlopen): |
35 | + mock_urlopen.side_effect = [ |
36 | FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
37 | mock_datastore = mock.Mock() |
38 | mock_datastore.get_file.side_effect = data_store.DataStoreException( |
39 | @@ -128,9 +130,11 @@ |
40 | with self.assertRaises(ImageObjectNotFound): |
41 | args.func(args) |
42 | |
43 | + @mock.patch('urllib2.urlopen') |
44 | @mock.patch('ci_utils.data_store.DataStore') |
45 | - def test_get_ticket_image_already_exists(self, mock_data_store): |
46 | - urllib2.urlopen.side_effect = [ |
47 | + def test_get_ticket_image_already_exists(self, mock_data_store, |
48 | + mock_urlopen): |
49 | + mock_urlopen.side_effect = [ |
50 | FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
51 | open(self.image_path, 'a').close() |
52 | args = self.cli.parse_arguments(['get_image', '-t', '4', '-n', |
53 | @@ -140,9 +144,11 @@ |
54 | "{} already exists.\n".format(self.image_path), |
55 | cm) |
56 | |
57 | + @mock.patch('urllib2.urlopen') |
58 | @mock.patch('ci_utils.data_store.create_for_ticket') |
59 | - def test_get_last_completed_ticket_image(self, mock_create_datastore): |
60 | - urllib2.urlopen.side_effect = [ |
61 | + def test_get_last_completed_ticket_image(self, mock_create_datastore, |
62 | + mock_urlopen): |
63 | + mock_urlopen.side_effect = [ |
64 | FakeUrlOpenReturn(200, json.dumps(ticket_data)), |
65 | FakeUrlOpenReturn(200, json.dumps(artifact_data)) |
66 | ] |
67 | @@ -158,9 +164,10 @@ |
68 | "Image file can be found at: {}\n".format(self.image_path)) |
69 | mock_datastore.assert_called_once() |
70 | |
71 | - def test_get_last_completed_ticket_none_complete(self): |
72 | + @mock.patch('urllib2.urlopen') |
73 | + def test_get_last_completed_ticket_none_complete(self, mock_urlopen): |
74 | data = {'objects': []} |
75 | - urllib2.urlopen.side_effect = [ |
76 | + mock_urlopen.side_effect = [ |
77 | FakeUrlOpenReturn(200, json.dumps(data))] |
78 | args = self.cli.parse_arguments(['get_image', '-n', self.image_path]) |
79 | with self.assertRaises(SystemExit) as cm: |
80 | |
81 | === modified file 'juju-deployer/deploy.py' |
82 | --- juju-deployer/deploy.py 2014-04-09 21:23:35 +0000 |
83 | +++ juju-deployer/deploy.py 2014-04-15 12:18:41 +0000 |
84 | @@ -1,19 +1,37 @@ |
85 | #!/usr/bin/python |
86 | +# Ubuntu Continuous Integration Engine |
87 | +# Copyright 2013, 2014 Canonical Ltd. |
88 | + |
89 | +# This program is free software: you can redistribute it and/or modify it |
90 | +# under the terms of the GNU Affero General Public License version 3, as |
91 | +# published by the Free Software Foundation. |
92 | + |
93 | +# This program is distributed in the hope that it will be useful, but |
94 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
95 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
96 | +# PURPOSE. See the GNU Affero General Public License for more details. |
97 | + |
98 | +# You should have received a copy of the GNU Affero General Public License |
99 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
100 | |
101 | import argparse |
102 | +import atexit |
103 | import base64 |
104 | +from distutils import spawn |
105 | import hashlib |
106 | +import itertools |
107 | +import logging |
108 | import os |
109 | +import shutil |
110 | +import signal |
111 | +import stat |
112 | +import subprocess |
113 | import sys |
114 | -import subprocess |
115 | -import stat |
116 | -import atexit |
117 | -import shutil |
118 | import tempfile |
119 | import textwrap |
120 | +import time |
121 | import yaml |
122 | -from distutils import spawn |
123 | -from itertools import chain |
124 | + |
125 | |
126 | import bzrlib |
127 | from bzrlib import ( |
128 | @@ -21,9 +39,15 @@ |
129 | plugin, |
130 | workingtree, |
131 | ) |
132 | - |
133 | - |
134 | +from novaclient.v1_1 import client as nova_client |
135 | +import novaclient.exceptions |
136 | +import swiftclient |
137 | + |
138 | + |
139 | +log = logging.getLogger(__name__) |
140 | HERE = os.path.abspath(os.path.dirname(__file__)) |
141 | +SSH_OPTS = ['-o', 'UserKnownHostsFile=/dev/null', |
142 | + '-o', 'StrictHostKeyChecking=no'] |
143 | |
144 | # the juju-deployer/ directory. |
145 | deployer_dir = os.path.abspath(os.path.dirname(sys.argv[0])) |
146 | @@ -37,29 +61,11 @@ |
147 | plugin.load_plugins() |
148 | |
149 | |
150 | -def check_and_set_ppa_privacy(private_ppas_only): |
151 | - current_value = os.environ['CI_PRIVATE_PPAS_ONLY'] |
152 | - if private_ppas_only: |
153 | - value = 'True' |
154 | - status = 'ENABLED' |
155 | - elif current_value not in ['0', '1']: |
156 | - print('Invalid CI_PRIVATE_PPAS_ONLY value. Options are 0 (disabled) ' |
157 | - 'and 1 (enabled). Provided value is: "{}"'.format(current_value)) |
158 | - sys.exit(1) |
159 | - else: |
160 | - if current_value == '0': |
161 | - value = 'False' |
162 | - status = 'DISABLED' |
163 | - else: |
164 | - value = 'True' |
165 | - status = 'ENABLED' |
166 | - os.environ['CI_PRIVATE_PPAS_ONLY'] = value |
167 | - print('Private PPAs only is {} (CI_PRIVATE_PPAS_ONLY: {})'.format(status, |
168 | - value)) |
169 | - |
170 | - |
171 | def check_environment(): |
172 | - '''Cheetah requires environment variables for populating our templates''' |
173 | + '''Cheetah requires environment variables for populating our templates. |
174 | + |
175 | + Some default values are also set here as a side-effect. |
176 | + ''' |
177 | cheetah_vars = { |
178 | 'OS_USERNAME': None, |
179 | 'CI_CODE_SOURCE': 'branch', |
180 | @@ -91,6 +97,258 @@ |
181 | exit(template.format('\n '.join(missing_required))) |
182 | |
183 | |
184 | +def bootstrap(): |
185 | + """Bootstrap juju. Specify needed as bootstrap to just bootstrap, otherwise |
186 | + tunnel to bootstrap and tunnel.""" |
187 | + |
188 | + args = ['juju', 'bootstrap', '--constraints=mem=1024M'] |
189 | + try: |
190 | + subprocess.check_output(args) |
191 | + except subprocess.CalledProcessError as e: |
192 | + # FIXME: if check_output raise an exception, we don't get a proper |
193 | + # output to check for the text below -- vila 2014-02-07 |
194 | + if 'environment is already bootstrapped' not in e.output: |
195 | + raise |
196 | + |
197 | + |
198 | +def _get_control_bucket(): |
199 | + """Get the name of the bucket in Swift that juju uses for state |
200 | + information.""" |
201 | + |
202 | + default_home = os.path.join(os.path.expanduser('~'), '.juju') |
203 | + juju_home = os.environ.get('JUJU_HOME') or default_home |
204 | + |
205 | + with open(os.path.join(juju_home, 'environments.yaml')) as fp: |
206 | + envs = yaml.safe_load(fp.read()) |
207 | + |
208 | + juju_env = os.environ.get('JUJU_ENV') |
209 | + if juju_env is None: |
210 | + juju_env = envs.get('default') |
211 | + if not juju_env: |
212 | + log.error('No juju environment specified.') |
213 | + sys.exit(1) |
214 | + |
215 | + return envs['environments'][juju_env]['control-bucket'] |
216 | + |
217 | + |
218 | +def _delete_control_bucket(): |
219 | + """Remove the juju control bucket and all of its files.""" |
220 | + |
221 | + bucket = _get_control_bucket() |
222 | + conn = _connect_to_swift() |
223 | + try: |
224 | + files = [x['name'] for x in conn.get_container(bucket)[1]] |
225 | + for f in files: |
226 | + conn.delete_object(bucket, f) |
227 | + conn.delete_container(bucket) |
228 | + except swiftclient.exceptions.ClientException: |
229 | + # Assume the bucket is not present |
230 | + pass |
231 | + |
232 | + |
233 | +def _connect_to_swift(): |
234 | + opts = {'tenant_name': os.environ['OS_TENANT_NAME'], |
235 | + 'region_name': os.environ['OS_REGION_NAME'], |
236 | + } |
237 | + conn = swiftclient.client.Connection( |
238 | + os.environ['OS_AUTH_URL'], |
239 | + os.environ['OS_USERNAME'], |
240 | + os.environ['OS_PASSWORD'], |
241 | + os_options=opts, |
242 | + auth_version='2.0') |
243 | + return conn |
244 | + |
245 | + |
246 | +def _get_from_swift(bucket, path): |
247 | + """Download the file path from swift at bucket.""" |
248 | + |
249 | + conn = _connect_to_swift() |
250 | + # get_object return a tuple of (metadata, data) |
251 | + return conn.get_object(bucket, path)[1] |
252 | + |
253 | + |
254 | +def get_bootstrap_ip(instance, max_tries=120): |
255 | + """Wait for the juju bootstrap node to boot and obtain an IP address. |
256 | + |
257 | + This is a pre-requisite to setup a tunnel. |
258 | + |
259 | + :raises: NotFound if the instance has not been spawned. |
260 | + |
261 | + :raises: Exception if no IP can be found after max_tries*3 seconds. |
262 | + """ |
263 | + |
264 | + log.info("Getting the bootstrap IP address.") |
265 | + |
266 | + args = [os.environ['OS_USERNAME'], os.environ['OS_PASSWORD'], |
267 | + os.environ['OS_TENANT_NAME'], os.environ['OS_AUTH_URL']] |
268 | + kwargs = {'region_name': os.environ['OS_REGION_NAME'], |
269 | + 'service_type': 'compute'} |
270 | + nt = nova_client.Client(*args, **kwargs) |
271 | + |
272 | + tries = 0 |
273 | + while tries < max_tries: |
274 | + # Raises NotFound if the instance does not exist. |
275 | + server = nt.servers.get(instance) |
276 | + networks = server.networks.values() |
277 | + if networks: |
278 | + # We use a single network and don't care how it's named |
279 | + net = networks[0] |
280 | + if net: |
281 | + # Either we have a single address or the last one has been |
282 | + # added to make the instance reachable (floating or public). |
283 | + net = net[-1] |
284 | + # Don't sleep anymore. We have our IP, let's move on |
285 | + return net |
286 | + |
287 | + tries += 1 |
288 | + if tries != max_tries: |
289 | + time.sleep(3) |
290 | + raise Exception('Timed out waiting for bootstrap IP.') |
291 | + |
292 | + |
293 | +def get_bootstrap_nova_instance(): |
294 | + """Get the Nova instance identifier for the juju bootstrap node. |
295 | + |
296 | + Raises swiftclient.exceptions.ClientException if the instance cannot be |
297 | + found.""" |
298 | + |
299 | + bucket = _get_control_bucket() |
300 | + |
301 | + state = yaml.safe_load(_get_from_swift(bucket, 'provider-state')) |
302 | + if not state: |
303 | + # Cause unknown (yet) but this has been seen in real life. I can't |
304 | + # reproduce anymore but I suspect 'juju bootstrap' failed mid-way so |
305 | + # returning None and letting the caller delete the bucket is the Right |
306 | + # Thing to do -- vila 2014-02-07 |
307 | + log.warning("Empty 'provider-state' in the juju bucket") |
308 | + return None |
309 | + return state['state-instances'][0] |
310 | + |
311 | + |
312 | +def wait_for_node(ip): |
313 | + """Wait for the bootstrap node to become available on ssh. We need to wait |
314 | + for this state before attempting to set up a sshuttle.""" |
315 | + |
316 | + log.info("Waiting for the bootstrap node to respond to ssh.") |
317 | + |
318 | + cmd = ['ssh'] + SSH_OPTS + ['ubuntu@%s' % ip, 'exit'] |
319 | + tries = 0 |
320 | + while tries < 120: |
321 | + try: |
322 | + subprocess.check_call(cmd) |
323 | + return |
324 | + except subprocess.CalledProcessError: |
325 | + tries += 1 |
326 | + time.sleep(5) |
327 | + |
328 | + raise Exception('Timed out waiting for bootstrap node.') |
329 | + |
330 | + |
331 | +def destroy_tunnel(): |
332 | + if os.path.exists('sshuttle.pid'): |
333 | + with open('sshuttle.pid') as fp: |
334 | + pid = int(fp.read()) |
335 | + os.kill(pid, signal.SIGTERM) |
336 | + |
337 | + |
338 | +def tunnel(ip): |
339 | + """Tunnel to the bootstrap node using sshuttle.""" |
340 | + |
341 | + log.info("Tunneling to the bootstrap node.") |
342 | + |
343 | + host = 'ubuntu@%s' % ip |
344 | + mask = '10.55.0.0/16' |
345 | + cmd = ['sshuttle', '-D', '-r', host, mask] |
346 | + opt = ['-e', 'ssh %s' % ' '.join(SSH_OPTS)] |
347 | + subprocess.check_call(cmd + opt) |
348 | + atexit.register(destroy_tunnel) |
349 | + |
350 | + |
351 | +def wait_then_tunnel(): |
352 | + instance = get_bootstrap_nova_instance() |
353 | + ip = get_bootstrap_ip(instance) |
354 | + wait_for_node(ip) |
355 | + tunnel(ip) |
356 | + |
357 | + |
358 | +def needs_bootstrap(): |
359 | + """Check to see if there's a Swift bucket for juju. If not, we need to |
360 | + boostrap. Check to see if the IP of the bootstrap node is owned by a |
361 | + running Nova instance. If not, we need to bootstrap.""" |
362 | + |
363 | + log.info("Checking to see if a bootstrap is needed.") |
364 | + |
365 | + def inconsistent_state(msg): |
366 | + log.warning(msg) |
367 | + _delete_control_bucket() |
368 | + |
369 | + try: |
370 | + instance = get_bootstrap_nova_instance() |
371 | + except swiftclient.exceptions.ClientException: |
372 | + instance = None |
373 | + if instance is None: |
374 | + # We don't have an instance. We need to bootstrap. |
375 | + inconsistent_state('No bootstrap instance in the swift bucket') |
376 | + return True |
377 | + |
378 | + try: |
379 | + get_bootstrap_ip(instance, 1) |
380 | + except novaclient.exceptions.NotFound: |
381 | + # Juju claims we have an instance that does not exist. We're in an |
382 | + # inconsistent state. Delete the bucket and bootstrap. |
383 | + inconsistent_state('No IP for the bootstrap instance') |
384 | + return True |
385 | + |
386 | + return False |
387 | + |
388 | + |
389 | +def _get_pid_of_sshuttle(ip_addr): |
390 | + try: |
391 | + cmd = ['pgrep', '-f', 'sshuttle.*%s' % ip_addr] |
392 | + pid = subprocess.check_output(cmd) |
393 | + # pgrep returns a list of pids matching the search argument |
394 | + pid.split('\n')[0] |
395 | + return int(pid) |
396 | + except subprocess.CalledProcessError: |
397 | + return -1 |
398 | + |
399 | + |
400 | +def private_IP(addr): |
401 | + """Check whether an IP address can be routed. |
402 | + :param addr: The IP address as a string. |
403 | + |
404 | + :returns: True if the address is private (can't be routed). False |
405 | + otherwise. |
406 | + |
407 | + """ |
408 | + if addr.startswith('10.'): # 10.0.0.0 - 10.255.255.255 |
409 | + return True |
410 | + elif addr.startswith('192.168'): # 192.168.0.0 - 192.168.255.255 |
411 | + return True |
412 | + elif addr.startswith('172.'): # 172.16.0.0 - 172.31.255.255 |
413 | + a, b, c, d = addr.split('.') |
414 | + if 16 <= int(b) <= 31: |
415 | + return True |
416 | + return False |
417 | + |
418 | + |
419 | +def needs_tunnel(): |
420 | + """Check to see if a tunnel is required and up. |
421 | + |
422 | + When required, it depends on juju's Swift bucket existing, and the IP it |
423 | + refers to is pointed at by a sshuttle instance. |
424 | + """ |
425 | + log.info("Checking to see if a tunnel needs to be setup.") |
426 | + |
427 | + instance = get_bootstrap_nova_instance() |
428 | + ip_addr = get_bootstrap_ip(instance, 1) |
429 | + if private_IP(ip_addr): |
430 | + sshuttle_pid = _get_pid_of_sshuttle(ip_addr) |
431 | + return sshuttle_pid < 0 |
432 | + else: |
433 | + return False |
434 | + |
435 | + |
436 | def check_bootstrapped(): |
437 | try: |
438 | # XXX: Added a print to give user feedback in case sshuttle isn't |
439 | @@ -116,6 +374,20 @@ |
440 | sys.exit(1) |
441 | |
442 | |
443 | +def populate_all_templates(): |
444 | + deployer_dir = os.path.join(HERE, '..', 'juju-deployer') |
445 | + temp = tempfile.mkdtemp() |
446 | + atexit.register(lambda: shutil.rmtree(temp)) |
447 | + # A place to put our generated .yaml files. |
448 | + working_dir = os.path.join(temp, 'deployer') |
449 | + # Copy all existing .yaml files, they don't need to be populated but |
450 | + # juju-deployer expect them in 'working_dir'. |
451 | + shutil.copytree(deployer_dir, working_dir) |
452 | + # Create .yaml files from .yaml.tmpl files. |
453 | + populate_templates(deployer_dir, working_dir) |
454 | + return working_dir |
455 | + |
456 | + |
457 | def populate_templates(deployer_dir, working_dir): |
458 | # The command to populate the template. |
459 | cmd_base = ['cheetah', 'fill', '--env', '-p'] |
460 | @@ -123,8 +395,12 @@ |
461 | for tmpl in os.listdir(deployer_dir): |
462 | path = os.path.join(deployer_dir, tmpl) |
463 | if os.path.isdir(path): |
464 | + # When called with a directory, recurse and process all templates |
465 | + # found there. |
466 | src = os.path.join(deployer_dir, tmpl) |
467 | dst = os.path.join(working_dir, tmpl) |
468 | + if not os.path.isdir(dst): |
469 | + os.mkdir(dst) |
470 | populate_templates(src, dst) |
471 | continue |
472 | if not tmpl.endswith('.yaml.tmpl'): |
473 | @@ -178,6 +454,31 @@ |
474 | cmd.run_argv_aliases(args) |
475 | |
476 | |
477 | +def install_charmworld(where=None): |
478 | + """Recent versions of amulet need charmworldlib. Install it.""" |
479 | + if where is None: |
480 | + where = os.path.join(HERE, '..', 'branches') |
481 | + dest = os.path.abspath(os.path.join(where, 'charmworldlib')) |
482 | + get_or_pull_branch('lp:charmworldlib', dest) |
483 | + prepend_path('PYTHONPATH', dest) |
484 | + # FIXME: This is a symptom that our setup story needs to receive some more |
485 | + # love -- vila 2014-04-10 |
486 | + sys.path.insert(0, dest) |
487 | + return dest |
488 | + |
489 | + |
490 | +def install_amulet(where=None): |
491 | + if where is None: |
492 | + where = os.path.join(HERE, '..', 'branches') |
493 | + dest = os.path.abspath(os.path.join(where, 'amulet')) |
494 | + get_or_pull_branch('lp:amulet', dest) |
495 | + prepend_path('PYTHONPATH', dest) |
496 | + # FIXME: This is a symptom that our setup story needs to receive some more |
497 | + # love -- vila 2014-04-10 |
498 | + sys.path.insert(0, dest) |
499 | + return dest |
500 | + |
501 | + |
502 | def install_websocket_client(where=None): |
503 | """Install python-websocket-client from bzr, as jujuclient needs it.""" |
504 | if where is None: |
505 | @@ -205,9 +506,10 @@ |
506 | if where is None: |
507 | where = os.path.join(HERE, '..', 'branches') |
508 | dest = os.path.abspath(os.path.join(where, 'juju-deployer')) |
509 | - # For an obscure reason tag:0.3.1 is not in lp:juju-deployer |
510 | get_or_pull_branch('lp:juju-deployer', dest, revision='revno:103') |
511 | prepend_path('PYTHONPATH', dest) |
512 | + # FIXME: This is a symptom that our setup story needs to receive some more |
513 | + # love -- vila 2014-04-10 |
514 | sys.path.insert(0, dest) |
515 | # FIXME: This duplicates setup.py and shouldn't be needed. This specific |
516 | # piece (juju-deployer) is a weird beast, it installs from source (not pip |
517 | @@ -415,45 +717,74 @@ |
518 | return parser.parse_args(args) |
519 | |
520 | |
521 | +def setup(for_tests=False, list_only=False): |
522 | + """Setup the environment for a deployment. |
523 | + |
524 | + This includes: |
525 | + - installing the source dependencies, |
526 | + - uploading our source payload to swift, |
527 | + - populating the templates, |
528 | + - starting the juju boootstrap node if needed, with a tunnel if needed. |
529 | + """ |
530 | + if for_tests: |
531 | + install_amulet() |
532 | + install_charmworld() |
533 | + install_websocket_client() |
534 | + install_jujuclient() |
535 | + install_deployer() |
536 | + |
537 | + check_environment() |
538 | + if for_tests: |
539 | + if not list_only: |
540 | + upload_payload_to_swift() |
541 | + working_dir = populate_all_templates() |
542 | + if not list_only: |
543 | + build_charms() |
544 | + # FIXME: We use JUJU_DEPLOYER_DIR to tell deployers.DeployerTest where |
545 | + # the populated templates are, there should be a better way than using |
546 | + # an env var as a global variable -- vila 2014-04-11 |
547 | + os.environ['JUJU_DEPLOYER_DIR'] = working_dir |
548 | + if for_tests: |
549 | + if needs_bootstrap(): |
550 | + bootstrap() |
551 | + if needs_tunnel(): |
552 | + wait_then_tunnel() |
553 | + |
554 | + |
555 | def main(): |
556 | args = _get_args() |
557 | |
558 | - check_environment() |
559 | - |
560 | # You can configure the system to use private PPAs only or by providing |
561 | # the option --private-ppas-only or by setting the environment variable |
562 | # CI_PRIVATE_PPAS_ONLY. |
563 | # If provided, --private-ppas-only option overrides the environment |
564 | # variable value. |
565 | - check_and_set_ppa_privacy(args.private_ppas_only) |
566 | + if args.private_ppas_only: |
567 | + os.environ['CI_PRIVATE_PPAS_ONLY'] = '1' |
568 | |
569 | check_bootstrapped() |
570 | check_dependencies() |
571 | |
572 | - install_websocket_client() |
573 | - install_jujuclient() |
574 | - install_deployer() |
575 | - |
576 | - # A place to put our generated .yaml files. |
577 | - temp_dir = tempfile.mkdtemp() |
578 | - atexit.register(shutil.rmtree, temp_dir) |
579 | - working_dir = os.path.join(temp_dir, 'deployer') |
580 | - # Copy all existing .yaml files, they don't need to be populated but |
581 | - # juju-deployer expect them in 'working_dir'. |
582 | - shutil.copytree(deployer_dir, working_dir) |
583 | - |
584 | if not args.branch_payload and not args.reuse_payload: |
585 | + # We set some env variables below so this should happen before |
586 | + # populating the templates |
587 | upload_payload_to_swift() |
588 | |
589 | - # Create .yaml files from .yaml.tmpl files. |
590 | - populate_templates(deployer_dir, working_dir) |
591 | + setup() |
592 | + priv = os.environ['CI_PRIVATE_PPAS_ONLY'] |
593 | + if priv == '0': |
594 | + status = 'DISABLED' |
595 | + else: |
596 | + status = 'ENABLED' |
597 | + print('Private PPAs only is {} (CI_PRIVATE_PPAS_ONLY: {})'.format(status, |
598 | + priv)) |
599 | + working_dir = populate_all_templates() |
600 | + build_charms() |
601 | |
602 | # Generate an argument list of -c FILE for each .yaml file in our working |
603 | # directory. |
604 | i = generate_argument_list(working_dir, args.specific) |
605 | - generated = list(chain.from_iterable(i)) |
606 | - |
607 | - build_charms() |
608 | + generated = list(itertools.chain.from_iterable(i)) |
609 | |
610 | if args.upgrade: |
611 | for x in generated: |
612 | |
613 | === modified file 'juju-deployer/test_deploy.py' |
614 | --- juju-deployer/test_deploy.py 2014-03-27 13:40:30 +0000 |
615 | +++ juju-deployer/test_deploy.py 2014-04-15 12:18:41 +0000 |
616 | @@ -1,14 +1,36 @@ |
617 | #!/usr/bin/env python |
618 | - |
619 | +# Ubuntu CI Engine |
620 | +# Copyright 2014 Canonical Ltd. |
621 | + |
622 | +# This program is free software: you can redistribute it and/or modify it |
623 | +# under the terms of the GNU Affero General Public License version 3, as |
624 | +# published by the Free Software Foundation. |
625 | + |
626 | +# This program is distributed in the hope that it will be useful, but |
627 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
628 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
629 | +# PURPOSE. See the GNU Affero General Public License for more details. |
630 | + |
631 | +# You should have received a copy of the GNU Affero General Public License |
632 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
633 | + |
634 | +from cStringIO import StringIO |
635 | import mock |
636 | -import unittest |
637 | import os |
638 | import sys |
639 | import tempfile |
640 | import shutil |
641 | import subprocess |
642 | - |
643 | - |
644 | +import unittest |
645 | + |
646 | +from novaclient import exceptions |
647 | +import swiftclient |
648 | + |
649 | +from bzrlib import ( |
650 | + branchbuilder, |
651 | + errors, |
652 | +) |
653 | +from bzrlib.transport import memory |
654 | from ucitests import fixtures |
655 | |
656 | |
657 | @@ -16,13 +38,24 @@ |
658 | import deploy |
659 | |
660 | |
661 | -class TestDeploy(unittest.TestCase): |
662 | +class TestCheckEnvironment(unittest.TestCase): |
663 | + |
664 | + def setUp(self): |
665 | + super(TestCheckEnvironment, self).setUp() |
666 | + fixtures.set_uniq_cwd(self) |
667 | |
668 | def test_check_environment(self): |
669 | - # If we haven't sourced a novarc, this program should bail out. |
670 | - with mock.patch.dict('os.environ', {'OS_USERNAME': ''}): |
671 | - with self.assertRaises(SystemExit): |
672 | - deploy.check_environment() |
673 | + # If we haven't sourced a valid novarc, this program should bail out. |
674 | + fixtures.isolate_from_env(self, dict(OS_USERNAME='')) |
675 | + with self.assertRaises(SystemExit): |
676 | + deploy.check_environment() |
677 | + |
678 | + |
679 | +class TestCheckDependencies(unittest.TestCase): |
680 | + |
681 | + def setUp(self): |
682 | + super(TestCheckDependencies, self).setUp() |
683 | + fixtures.set_uniq_cwd(self) |
684 | |
685 | # check that some of our values can be defaulted correctly: |
686 | env_required = { |
687 | @@ -33,20 +66,82 @@ |
688 | 'CI_LAUNCHPAD_USER': 'lp-user', |
689 | 'CI_LAUNCHPAD_PPA_OWNER': 'lp-ppa-owner', |
690 | } |
691 | - with mock.patch.dict('os.environ', env_required): |
692 | - deploy.check_environment() |
693 | - for key, val in env_required.iteritems(): |
694 | - self.assertEqual(val, os.environ[key]) |
695 | - # defaulted by code |
696 | - self.assertEqual(os.environ['CI_CODE_SOURCE'], 'branch') |
697 | - self.assertEqual(os.environ['CI_PRIVATE_PPAS_ONLY'], '0') |
698 | + fixtures.isolate_from_env(self, env_required) |
699 | + deploy.check_environment() |
700 | + for key, val in env_required.iteritems(): |
701 | + self.assertEqual(val, os.environ[key]) |
702 | + # defaulted by code |
703 | + self.assertEqual(os.environ['CI_CODE_SOURCE'], 'branch') |
704 | + self.assertEqual(os.environ['CI_PRIVATE_PPAS_ONLY'], '0') |
705 | |
706 | def test_check_dependencies(self): |
707 | # If we cannot find any of our dependencies, this program should bail |
708 | # out. |
709 | - with mock.patch.dict('os.environ', {'PATH': ''}): |
710 | - with self.assertRaises(SystemExit): |
711 | - deploy.check_dependencies() |
712 | + fixtures.isolate_from_env(self, dict(PATH='')) |
713 | + with self.assertRaises(SystemExit): |
714 | + deploy.check_dependencies() |
715 | + |
716 | + |
717 | +class TestGetOrPullBranch(unittest.TestCase): |
718 | + |
719 | + def setUp(self): |
720 | + super(TestGetOrPullBranch, self).setUp() |
721 | + fixtures.set_uniq_cwd(self) |
722 | + fixtures.override_env(self, 'BZR_EMAIL', 'foo@example.com') |
723 | + builder = branchbuilder.BranchBuilder( |
724 | + memory.MemoryTransport("memory:///")) |
725 | + builder.start_series() |
726 | + builder.build_snapshot( |
727 | + 'rev-id', None, |
728 | + [('add', ('', 'root-id', 'directory', '')), |
729 | + ('add', ('filename', 'f-id', 'file', 'content\n'))]) |
730 | + builder.build_snapshot('rev2-id', ['rev-id'], |
731 | + [('modify', ('f-id', 'new-content\n'))]) |
732 | + builder.finish_series() |
733 | + # The tested API want local paths, let's create the branch on disk |
734 | + tree = builder.get_branch().create_checkout('the_branch') |
735 | + self.branch = tree.branch |
736 | + # Capture output |
737 | + self.out = StringIO() |
738 | + self.err = StringIO() |
739 | + # silence progress report |
740 | + fixtures.override_env(self, 'BZR_PROGRESS_BAR', 'none') |
741 | + |
742 | + def do_branch(self, *args): |
743 | + deploy.get_or_pull_branch(*args, out=self.out, err=self.err) |
744 | + self.assertEqual('', self.err.getvalue()) |
745 | + |
746 | + def test_get_branch(self): |
747 | + self.do_branch('the_branch', 'here') |
748 | + self.assertTrue(os.path.exists('here')) |
749 | + self.assertEqual('', self.out.getvalue()) |
750 | + |
751 | + def test_pull_branch(self): |
752 | + self.do_branch('the_branch', 'here') |
753 | + self.do_branch('the_branch', 'here', 'revid:rev-id') |
754 | + self.assertEqual('Now on revision 1.\n', self.out.getvalue()) |
755 | + with open(os.path.join('here', 'filename')) as f: |
756 | + self.assertEqual('content\n', f.read()) |
757 | + |
758 | + def assertErrorPath(self, dir_name, exc): |
759 | + expected = os.path.join(self.uniq_dir, dir_name) + '/' |
760 | + self.assertEqual(expected, exc.path) |
761 | + |
762 | + def test_branch_bogus_src(self): |
763 | + with self.assertRaises(errors.NotBranchError) as cm: |
764 | + self.do_branch('I-dont-exist', 'here') |
765 | + self.assertErrorPath('I-dont-exist', cm.exception) |
766 | + self.assertEqual('', self.out.getvalue()) |
767 | + |
768 | + def test_pull_bogus_src(self): |
769 | + deploy.get_or_pull_branch('the_branch', 'here') |
770 | + with self.assertRaises(errors.NotBranchError) as cm: |
771 | + self.do_branch('I-dont-exist', 'here') |
772 | + self.assertErrorPath('I-dont-exist', cm.exception) |
773 | + self.assertEqual('', self.out.getvalue()) |
774 | + |
775 | + |
776 | +class TestBootStrap(unittest.TestCase): |
777 | |
778 | def test_check_not_bootstrapped(self): |
779 | # If juju environment isn't bootstrapped, this program should bail |
780 | @@ -62,75 +157,95 @@ |
781 | # If juju status returns ok, this should just work. |
782 | deploy.check_bootstrapped() |
783 | |
784 | + |
785 | +class TestInstallDeployer(unittest.TestCase): |
786 | + |
787 | + def setUp(self): |
788 | + super(TestInstallDeployer, self).setUp() |
789 | + fixtures.set_uniq_cwd(self) |
790 | + |
791 | def test_install_deployer(self): |
792 | + fixtures.isolate_from_env(self, dict(PYTHONPATH='')) |
793 | + deployer_dir = deploy.install_deployer('.') |
794 | + self.addCleanup(shutil.rmtree, deployer_dir) |
795 | + self.assertTrue(os.path.exists(os.path.join(deployer_dir, '.bzr'))) |
796 | + self.assertEqual(deployer_dir, os.environ['PYTHONPATH']) |
797 | + deployer_script = os.path.join(deployer_dir, 'juju-deployer') |
798 | + self.assertTrue(os.access(deployer_script, os.X_OK)) |
799 | + self.assertTrue(os.path.exists(deployer_script)) |
800 | + |
801 | + |
802 | +class TestGenerateArgumentList(unittest.TestCase): |
803 | + |
804 | + def setUp(self): |
805 | + super(TestGenerateArgumentList, self).setUp() |
806 | fixtures.set_uniq_cwd(self) |
807 | - with mock.patch.dict('os.environ', {'PYTHONPATH': ''}): |
808 | - deployer_dir = deploy.install_deployer('.') |
809 | - self.assertTrue(os.path.exists(os.path.join(deployer_dir, '.bzr'))) |
810 | - self.assertEqual(deployer_dir, os.environ['PYTHONPATH']) |
811 | - deployer_script = os.path.join(deployer_dir, 'juju-deployer') |
812 | - self.assertTrue(os.access(deployer_script, os.X_OK)) |
813 | - self.assertTrue(os.path.exists(deployer_script)) |
814 | |
815 | def test_generate_empty_list(self): |
816 | - working = tempfile.mkdtemp() |
817 | - self.addCleanup(lambda: shutil.rmtree(working)) |
818 | - |
819 | # Calling without anything in the directory should produce an empty |
820 | # list. |
821 | - self.assertEqual(list(deploy.generate_argument_list(working)), []) |
822 | + self.assertEqual([], list(deploy.generate_argument_list('.'))) |
823 | |
824 | def test_generate_explicit_list(self): |
825 | - working = tempfile.mkdtemp() |
826 | - self.addCleanup(lambda: shutil.rmtree(working)) |
827 | - |
828 | - expected = os.path.join(working, 'foo.yaml') |
829 | + expected = os.path.join('.', 'foo.yaml') |
830 | with open(expected, 'w') as fp: |
831 | fp.write('nothing') |
832 | # Calling with a basic configuration should produce the full path to |
833 | # that configuration in the form ('-c', path). |
834 | - gen = deploy.generate_argument_list(working) |
835 | + gen = deploy.generate_argument_list('.') |
836 | self.assertEqual([('-c', expected)], list(gen)) |
837 | |
838 | + |
839 | +class TestPopulateTemplates(unittest.TestCase): |
840 | + |
841 | + def setUp(self): |
842 | + super(TestPopulateTemplates, self).setUp() |
843 | + fixtures.set_uniq_cwd(self) |
844 | + self.target = os.path.join('.', 'target') |
845 | + os.mkdir(self.target) |
846 | + self.source = os.path.join('.', 'source') |
847 | + os.mkdir(self.source) |
848 | + |
849 | def test_populate_templates_empty_list(self): |
850 | - fake_deployer_dir = tempfile.mkdtemp() |
851 | - self.addCleanup(lambda: shutil.rmtree(fake_deployer_dir)) |
852 | - working = tempfile.mkdtemp() |
853 | - self.addCleanup(lambda: shutil.rmtree(working)) |
854 | - |
855 | - deploy.populate_templates(fake_deployer_dir, working) |
856 | + deploy.populate_templates(self.source, self.target) |
857 | # Nothing happened |
858 | - self.assertEqual([], os.listdir(working)) |
859 | - |
860 | - @mock.patch.dict('os.environ', {'USER': 'foo'}) |
861 | - def test_populate_templates_explicit_list(self, *args): |
862 | - fake_deployer_dir = tempfile.mkdtemp() |
863 | - self.addCleanup(lambda: shutil.rmtree(fake_deployer_dir)) |
864 | - working = tempfile.mkdtemp() |
865 | - self.addCleanup(lambda: shutil.rmtree(working)) |
866 | - |
867 | - test_files = ['config.yaml', 'configs/unit_config.yaml'] |
868 | - for f in test_files: |
869 | - src = os.path.join(fake_deployer_dir, f) |
870 | - dst = os.path.join(working, f) |
871 | - if not os.path.exists(os.path.dirname(src)): |
872 | - os.mkdir(os.path.dirname(src)) |
873 | - if not os.path.exists(os.path.dirname(dst)): |
874 | - os.mkdir(os.path.dirname(dst)) |
875 | - |
876 | - with open(src + '.tmpl', 'w') as f: |
877 | - f.write('me: ${USER}') |
878 | - |
879 | - deploy.populate_templates(fake_deployer_dir, working) |
880 | - |
881 | - for f in test_files: |
882 | - f = os.path.join(working, f) |
883 | - content = open(f).read() |
884 | - self.assertEqual('me: {}'.format(os.environ['USER']), content) |
885 | + self.assertEqual([], os.listdir(self.source)) |
886 | + |
887 | + def test_populate_templates_explicit_list(self): |
888 | + fixtures.override_env(self, 'USER', 'foo') |
889 | + stem = 'config.yaml' |
890 | + with open(os.path.join(self.source, stem + '.tmpl'), 'w') as f: |
891 | + f.write('me: ${USER}') |
892 | + deploy.populate_templates(self.source, self.target) |
893 | + with open(os.path.join(self.target, stem)) as f: |
894 | + content = f.read() |
895 | + self.assertEqual('me: {}'.format(os.environ['USER']), content) |
896 | + |
897 | + def test_populate_recurse(self): |
898 | + fixtures.override_env(self, 'USER', 'foo') |
899 | + configs_dir = os.path.join(self.source, 'configs') |
900 | + os.mkdir(configs_dir) |
901 | + stem = 'config.yaml' |
902 | + with open(os.path.join(configs_dir, stem + '.tmpl'), 'w') as f: |
903 | + f.write('me: ${USER}') |
904 | + # The only existing template is one level below self.source so |
905 | + # populate_templates should recurse there. |
906 | + deploy.populate_templates(self.source, self.target) |
907 | + # Accordingly, the results should one level below self.target |
908 | + with open(os.path.join(self.target, 'configs', stem)) as f: |
909 | + content = f.read() |
910 | + self.assertEqual('me: {}'.format(os.environ['USER']), content) |
911 | + |
912 | + |
913 | +class TestBuildCharms(unittest.TestCase): |
914 | + |
915 | + def setUp(self): |
916 | + super(TestBuildCharms, self).setUp() |
917 | + fixtures.set_uniq_cwd(self) |
918 | |
919 | def test_build_charms(self): |
920 | charmsdir = tempfile.mkdtemp() |
921 | - self.addCleanup(lambda: shutil.rmtree(charmsdir)) |
922 | + self.addCleanup(shutil.rmtree, charmsdir) |
923 | p = os.path.join(charmsdir, 'precise') |
924 | os.mkdir(p) |
925 | charm1 = os.path.join(p, 'charm1') |
926 | @@ -151,5 +266,64 @@ |
927 | self.assertItemsEqual(['charm1', 'charm2', 'webui'], os.listdir(p)) |
928 | |
929 | |
930 | +class TestPrivateIP(unittest.TestCase): |
931 | + |
932 | + def test_public(self): |
933 | + self.assertFalse(deploy.private_IP('2.2.2.2')) |
934 | + |
935 | + def test_class_A_private(self): |
936 | + self.assertTrue(deploy.private_IP('10.0.4.5')) |
937 | + |
938 | + def test_class_B_public(self): |
939 | + self.assertFalse(deploy.private_IP('172.15.2.245')) |
940 | + |
941 | + def test_class_B_private(self): |
942 | + self.assertTrue(deploy.private_IP('172.24.12.36')) |
943 | + |
944 | + def test_class_C_private(self): |
945 | + self.assertTrue(deploy.private_IP('192.168.0.16')) |
946 | + |
947 | + |
948 | +class TestInstallAmulet(unittest.TestCase): |
949 | + |
950 | + def setUp(self): |
951 | + super(TestInstallAmulet, self).setUp() |
952 | + fixtures.set_uniq_cwd(self) |
953 | + |
954 | + def test_install_amulet(self): |
955 | + fixtures.override_env(self, 'PYTHONPATH', '') |
956 | + deploy.install_amulet('.') |
957 | + self.assertTrue(os.path.exists(os.path.join('amulet', '.bzr'))) |
958 | + self.assertEqual(os.path.join(self.uniq_dir, 'amulet'), |
959 | + os.environ['PYTHONPATH']) |
960 | + |
961 | + |
962 | +class TestNeedsBotstrap(unittest.TestCase): |
963 | + |
964 | + @mock.patch('deploy._get_control_bucket', |
965 | + return_value='juju-d911b43e427c204cfcfd8c7276c4b2aa') |
966 | + def test_needs_bootstrap(self, mock_get_control_bucket): |
967 | + provider_state = '{ state-instances: ["abcdefg"] }' |
968 | + with mock.patch('deploy._get_from_swift', return_value=provider_state): |
969 | + # Bootstrap needed - the bucket exists but the instance does not. |
970 | + e = exceptions.NotFound('Four oh four') |
971 | + with mock.patch('deploy.get_bootstrap_ip', side_effect=e): |
972 | + with mock.patch('deploy._delete_control_bucket') as dcb: |
973 | + self.assertTrue(deploy.needs_bootstrap()) |
974 | + dcb.assert_called_with() |
975 | + |
976 | + # Bootstrap not needed. |
977 | + ip_addr = '127.0.0.1' |
978 | + with mock.patch('deploy.get_bootstrap_ip', return_value=ip_addr): |
979 | + self.assertFalse(deploy.needs_bootstrap()) |
980 | + |
981 | + # Bootstrap needed - the bucket does not exist. |
982 | + e = swiftclient.exceptions.ClientException('not found') |
983 | + with mock.patch('deploy._get_from_swift', side_effect=e): |
984 | + with mock.patch('deploy._delete_control_bucket') as dcb: |
985 | + self.assertTrue(deploy.needs_bootstrap()) |
986 | + dcb.assert_called_with() |
987 | + |
988 | + |
989 | if __name__ == '__main__': |
990 | unittest.main() |
991 | |
992 | === renamed file 'tests/test_run.py' => 'juju-deployer/test_run.py' |
993 | --- tests/test_run.py 2014-04-09 14:54:06 +0000 |
994 | +++ juju-deployer/test_run.py 2014-04-15 12:18:41 +0000 |
995 | @@ -1,26 +1,42 @@ |
996 | #!/usr/bin/env python |
997 | - |
998 | -import unittest |
999 | +# Ubuntu CI Engine |
1000 | +# Copyright 2014 Canonical Ltd. |
1001 | + |
1002 | +# This program is free software: you can redistribute it and/or modify it |
1003 | +# under the terms of the GNU Affero General Public License version 3, as |
1004 | +# published by the Free Software Foundation. |
1005 | + |
1006 | +# This program is distributed in the hope that it will be useful, but |
1007 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1008 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1009 | +# PURPOSE. See the GNU Affero General Public License for more details. |
1010 | + |
1011 | +# You should have received a copy of the GNU Affero General Public License |
1012 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1013 | + |
1014 | +import logging |
1015 | import mock |
1016 | -import run |
1017 | -import yaml |
1018 | -import tempfile |
1019 | -import shutil |
1020 | import os |
1021 | -import logging |
1022 | import stat |
1023 | -import swiftclient |
1024 | -from novaclient.exceptions import NotFound |
1025 | +import sys |
1026 | +import tempfile |
1027 | +import unittest |
1028 | +import yaml |
1029 | |
1030 | |
1031 | from ucitests import fixtures |
1032 | |
1033 | - |
1034 | -# Disable all (almost) log output from the tests and the code they run |
1035 | +# FIXME: Disable logging while running tests, this will be revisited once we |
1036 | +# have a proper way to capture log in tests. -- vila 2014-02-12 |
1037 | logging.disable(logging.CRITICAL) |
1038 | |
1039 | - |
1040 | -class TestRunTests(unittest.TestCase): |
1041 | +mydir = os.path.dirname(__file__) |
1042 | +sys.path.append(os.path.abspath(os.path.join( mydir, '..', 'tests'))) |
1043 | +import run |
1044 | + |
1045 | + |
1046 | +class TestDestroyServices(unittest.TestCase): |
1047 | + |
1048 | def _destroy_services(self, fake_status, destroy_machine, destroy_service): |
1049 | fake_status = yaml.safe_dump(fake_status) |
1050 | |
1051 | @@ -43,60 +59,47 @@ |
1052 | fake_status = {'services': {}, 'machines': {}} |
1053 | self._destroy_services(fake_status, destroy_machine, destroy_service) |
1054 | |
1055 | - def test_get_tests_no_tests(self): |
1056 | - temp_dir = tempfile.mkdtemp() |
1057 | - self.addCleanup(lambda: shutil.rmtree(temp_dir)) |
1058 | - self.assertEqual(run.get_tests(temp_dir), []) |
1059 | + |
1060 | +class TestGetTests(unittest.TestCase): |
1061 | + """Minimally test get_tests until run-tests can replace ./tests/run.py. |
1062 | + |
1063 | + We test against the real source but it's ok as we're read-only. |
1064 | + """ |
1065 | + |
1066 | + def setUp(self): |
1067 | + super(TestGetTests, self).setUp() |
1068 | + fixtures.set_uniq_cwd(self) |
1069 | + |
1070 | + def create_file(self, name, content=''): |
1071 | + # We only care about files existing |
1072 | + with open(name, 'w') as f: |
1073 | + f.write(content) |
1074 | + |
1075 | + def test_no_tests(self): |
1076 | + self.assertEqual([], run.get_tests('.')) |
1077 | + |
1078 | + def test_matches_from_env(self): |
1079 | + fixtures.isolate_from_env(self, dict(TESTS='foo')) |
1080 | + self.create_file('test_foo.py') |
1081 | + self.assertEqual(['./test_foo.py'], run.get_tests('.')) |
1082 | + |
1083 | + def test_filtered_from_env(self): |
1084 | + fixtures.isolate_from_env(self, dict(TESTS='foo')) |
1085 | + self.create_file('test_foo.py') |
1086 | + self.create_file('test_bar.py') |
1087 | + self.assertEqual(['./test_foo.py'], run.get_tests('.')) |
1088 | |
1089 | def test_get_tests(self): |
1090 | - temp_dir = tempfile.mkdtemp() |
1091 | - self.addCleanup(lambda: shutil.rmtree(temp_dir)) |
1092 | - os.mkdir(os.path.join(temp_dir, 'foo')) |
1093 | - os.mkdir(os.path.join(temp_dir, 'bar')) |
1094 | - expect = [os.path.join(temp_dir, x, 'test.py') for x in ('foo', 'bar')] |
1095 | - |
1096 | - for x in expect: |
1097 | - with open(x, 'w') as fp: |
1098 | - fp.write('#!/usr/bin/python\nprint "hello"\n') |
1099 | - with open(os.path.join(temp_dir, 'README'), 'w') as fp: |
1100 | - fp.write('Empty.') |
1101 | - os.mkdir(os.path.join(temp_dir, 'baz')) |
1102 | - with open(os.path.join(temp_dir, 'baz', 'README'), 'w') as fp: |
1103 | - fp.write('Empty.') |
1104 | - |
1105 | - self.assertEqual(sorted(run.get_tests(temp_dir)), sorted(expect)) |
1106 | - |
1107 | - def test_install_amulet(self): |
1108 | - fixtures.set_uniq_cwd(self) |
1109 | - with mock.patch.dict('os.environ', {'PYTHONPATH': ''}): |
1110 | - amulet_dir = run.install_amulet('.') |
1111 | - self.addCleanup(shutil.rmtree, amulet_dir) |
1112 | - self.assertTrue(os.path.exists(os.path.join(amulet_dir, '.bzr'))) |
1113 | - self.assertEqual(amulet_dir, os.environ['PYTHONPATH']) |
1114 | - |
1115 | - @mock.patch('run._get_control_bucket', |
1116 | - return_value='juju-d911b43e427c204cfcfd8c7276c4b2aa') |
1117 | - def test_needs_bootstrap(self, mock_get_control_bucket): |
1118 | - provider_state = '{ state-instances: ["abcdefg"] }' |
1119 | - with mock.patch('run._get_from_swift', return_value=provider_state): |
1120 | - # Bootstrap needed - the bucket exists but the instance does not. |
1121 | - e = NotFound('Four oh four') |
1122 | - with mock.patch('run.get_bootstrap_ip', side_effect=e): |
1123 | - with mock.patch('run._delete_control_bucket') as dcb: |
1124 | - self.assertTrue(run.needs_bootstrap()) |
1125 | - dcb.assert_called_with() |
1126 | - |
1127 | - # Bootstrap not needed. |
1128 | - ip_addr = '127.0.0.1' |
1129 | - with mock.patch('run.get_bootstrap_ip', return_value=ip_addr): |
1130 | - self.assertFalse(run.needs_bootstrap()) |
1131 | - |
1132 | - # Bootstrap needed - the bucket does not exist. |
1133 | - e = swiftclient.exceptions.ClientException('not found') |
1134 | - with mock.patch('run._get_from_swift', side_effect=e): |
1135 | - with mock.patch('run._delete_control_bucket') as dcb: |
1136 | - self.assertTrue(run.needs_bootstrap()) |
1137 | - dcb.assert_called_with() |
1138 | + expected = [os.path.join('.', 'test_{}.py'.format(x)) |
1139 | + for x in ('foo', 'bar')] |
1140 | + |
1141 | + for x in expected: |
1142 | + self.create_file(x, '#!/usr/bin/python\nprint "hello"\n') |
1143 | + self.create_file('README') |
1144 | + self.assertEqual(sorted(expected), sorted(run.get_tests('.'))) |
1145 | + |
1146 | + |
1147 | +class TestRunTests(unittest.TestCase): |
1148 | |
1149 | def test_check_sudoers(self): |
1150 | with tempfile.NamedTemporaryFile() as fp: |
1151 | @@ -139,24 +142,5 @@ |
1152 | self.assertEqual(result, ['ci-juju-gui/0']) |
1153 | |
1154 | |
1155 | -class TestPrivateIP(unittest.TestCase): |
1156 | - |
1157 | - def test_public(self): |
1158 | - self.assertFalse(run.private_IP('2.2.2.2')) |
1159 | - |
1160 | - def test_class_A_private(self): |
1161 | - self.assertTrue(run.private_IP('10.0.4.5')) |
1162 | - |
1163 | - def test_class_B_public(self): |
1164 | - self.assertFalse(run.private_IP('172.15.2.245')) |
1165 | - |
1166 | - def test_class_B_private(self): |
1167 | - self.assertTrue(run.private_IP('172.24.12.36')) |
1168 | - |
1169 | - def test_class_C_private(self): |
1170 | - self.assertTrue(run.private_IP('192.168.0.16')) |
1171 | - |
1172 | - |
1173 | - |
1174 | if __name__ == '__main__': |
1175 | unittest.main() |
1176 | |
1177 | === modified file 'juju-deployer/test_update.py' |
1178 | --- juju-deployer/test_update.py 2014-02-19 14:48:22 +0000 |
1179 | +++ juju-deployer/test_update.py 2014-04-15 12:18:41 +0000 |
1180 | @@ -60,28 +60,26 @@ |
1181 | class TestGetDeployerConfigs(unittest.TestCase): |
1182 | |
1183 | def setUp(self): |
1184 | - self.temp_dir = tempfile.mkdtemp() |
1185 | - self.addCleanup(lambda: shutil.rmtree(self.temp_dir)) |
1186 | + super(TestGetDeployerConfigs, self).setUp() |
1187 | + fixtures.set_uniq_cwd(self) |
1188 | |
1189 | def test_get_deployer_no_configs(self): |
1190 | - configs = list(update.get_deployer_configs(self.temp_dir)) |
1191 | + configs = list(update.get_deployer_configs('.')) |
1192 | self.assertEqual(configs, []) |
1193 | |
1194 | def test_get_deployer_two_configs(self): |
1195 | for x in ('foo.yaml', 'bar.yaml.tmpl'): |
1196 | - with open(os.path.join(self.temp_dir, x), 'w') as fp: |
1197 | + with open(x, 'w') as fp: |
1198 | fp.write('nothing: here') |
1199 | - foo = os.path.join(self.temp_dir, 'foo.yaml') |
1200 | - bar = os.path.join(self.temp_dir, 'bar.yaml.tmpl') |
1201 | - self.assertEqual(sorted([foo, bar]), |
1202 | - sorted(update.get_deployer_configs(self.temp_dir))) |
1203 | + self.assertEqual(sorted(['./bar.yaml.tmpl', './foo.yaml']), |
1204 | + sorted(update.get_deployer_configs('.'))) |
1205 | |
1206 | |
1207 | class TestUpdate(unittest.TestCase): |
1208 | |
1209 | def setUp(self): |
1210 | - self.temp_dir = tempfile.mkdtemp() |
1211 | - self.addCleanup(lambda: shutil.rmtree(self.temp_dir)) |
1212 | + super(TestUpdate, self).setUp() |
1213 | + fixtures.set_uniq_cwd(self) |
1214 | fixtures.override_env(self, 'BZR_EMAIL', 'foo@example.com') |
1215 | # We need a real branch with at least 2 revisions (so we can check out |
1216 | # of date references) |
1217 | @@ -97,8 +95,7 @@ |
1218 | builder.finish_series() |
1219 | # get_branches_and_revnos won't be able to use a in-memory branch, |
1220 | # let's create one on disk. |
1221 | - dest = os.path.join(self.temp_dir, 'the_branch') |
1222 | - tree = builder.get_branch().create_checkout(dest) |
1223 | + tree = builder.get_branch().create_checkout('the_branch') |
1224 | self.branch = tree.branch |
1225 | |
1226 | def get_branch_url(self): |
1227 | @@ -120,14 +117,14 @@ |
1228 | |
1229 | def test_ensure_all_branches_are_pinned(self): |
1230 | yaml_string = self.get_service_yaml(1) |
1231 | - path = os.path.join(self.temp_dir, 'foo.yaml') |
1232 | + path = 'foo.yaml' |
1233 | with open(path, 'w') as fp: |
1234 | fp.write(yaml.safe_dump(yaml_string)) |
1235 | self.assertTrue(update.ensure_all_branches_are_pinned([path])) |
1236 | |
1237 | def test_ensure_some_branches_are_not_pinned(self): |
1238 | yaml_string = self.get_service_yaml() |
1239 | - path = os.path.join(self.temp_dir, 'foo.yaml') |
1240 | + path = 'foo.yaml' |
1241 | with open(path, 'w') as fp: |
1242 | fp.write(yaml.safe_dump(yaml_string)) |
1243 | self.assertFalse(update.ensure_all_branches_are_pinned([path])) |
1244 | @@ -136,7 +133,7 @@ |
1245 | branch_url = self.get_branch_url() |
1246 | current = self.branch.revno() |
1247 | y = self.get_service_yaml(current - 1) |
1248 | - path = os.path.join(self.temp_dir, 'foo.yaml') |
1249 | + path = 'foo.yaml' |
1250 | with open(path, 'w') as fp: |
1251 | fp.write(yaml.safe_dump(y)) |
1252 | |
1253 | @@ -151,7 +148,7 @@ |
1254 | def test_set_branches_and_revnos_on_naked_branches(self): |
1255 | branch_url = self.get_branch_url() |
1256 | y = self.get_service_yaml() |
1257 | - path = os.path.join(self.temp_dir, 'foo.yaml') |
1258 | + path = 'foo.yaml' |
1259 | with open(path, 'w') as fp: |
1260 | fp.write(yaml.safe_dump(y)) |
1261 | update.set_branches_and_revnos([path], {branch_url: 1}) |
1262 | @@ -164,7 +161,7 @@ |
1263 | def test_set_branches_and_revnos_on_pinned_branches(self): |
1264 | branch_url = self.get_branch_url() |
1265 | y = self.get_service_yaml(1) |
1266 | - path = os.path.join(self.temp_dir, 'foo.yaml') |
1267 | + path = 'foo.yaml' |
1268 | with open(path, 'w') as fp: |
1269 | fp.write(yaml.safe_dump(y)) |
1270 | update.set_branches_and_revnos([path], {branch_url: 2}) |
1271 | |
1272 | === modified file 'run-tests' |
1273 | --- run-tests 2014-03-27 13:40:30 +0000 |
1274 | +++ run-tests 2014-04-15 12:18:41 +0000 |
1275 | @@ -22,6 +22,11 @@ |
1276 | import sys |
1277 | |
1278 | |
1279 | +HERE = os.path.abspath(os.path.dirname(__file__)) |
1280 | + |
1281 | +# deploy.py isn't in our pythonpath when run as a script |
1282 | +sys.path.append(os.path.join(HERE, 'juju-deployer')) |
1283 | +import deploy |
1284 | from ucitests import ( |
1285 | filters, |
1286 | loaders, |
1287 | @@ -243,8 +248,46 @@ |
1288 | return failures |
1289 | |
1290 | |
1291 | -def run_regular_components(suite, out, result, options): |
1292 | - """Run tests for all the regular components. |
1293 | +def load_orphan_tests(include_regexps, exclude_regexps=None): |
1294 | + """Load tests matching inclusive and exclusive regexps. |
1295 | + |
1296 | + :param include_regexps: A list of regexps describing the tests to include. |
1297 | + |
1298 | + :param exclude_regexps: A list of regexps describing the tests to exclude. |
1299 | + |
1300 | + :return: The test suite for all collected tests. |
1301 | + """ |
1302 | + components = ['juju-deployer'] |
1303 | + loader = loaders.Loader() |
1304 | + suite = loader.suiteClass() |
1305 | + for c in components: |
1306 | + suite.addTests(load_component_tests(loader, c)) |
1307 | + suite = filters.include_regexps(include_regexps, suite) |
1308 | + suite = filters.exclude_regexps(exclude_regexps, suite) |
1309 | + return suite |
1310 | + |
1311 | + |
1312 | +def load_integration_tests(include_regexps, exclude_regexps=None): |
1313 | + """Load tests matching inclusive and exclusive regexps. |
1314 | + |
1315 | + :param include_regexps: A list of regexps describing the tests to include. |
1316 | + |
1317 | + :param exclude_regexps: A list of regexps describing the tests to exclude. |
1318 | + |
1319 | + :return: The test suite for all collected tests. |
1320 | + """ |
1321 | + components = ['tests'] |
1322 | + loader = loaders.Loader() |
1323 | + suite = loader.suiteClass() |
1324 | + for c in components: |
1325 | + suite.addTests(load_component_tests(loader, c)) |
1326 | + suite = filters.include_regexps(include_regexps, suite) |
1327 | + suite = filters.exclude_regexps(exclude_regexps, suite) |
1328 | + return suite |
1329 | + |
1330 | + |
1331 | +def run_regular_tests(suite, out, result, options): |
1332 | + """Run the given test suite under some isolation. |
1333 | |
1334 | :param suite: The test suite to run. |
1335 | |
1336 | @@ -289,16 +332,31 @@ |
1337 | suite = load_regular_component_tests(options.include_regexps, |
1338 | options.exclude_regexps) |
1339 | # Regular components |
1340 | - ret = run_regular_components(suite, stdout, result, options) |
1341 | + ret = run_regular_tests(suite, stdout, result, options) |
1342 | + # orphan tests that don't have a proper home yet |
1343 | + suite = load_orphan_tests(options.include_regexps, |
1344 | + options.exclude_regexps) |
1345 | + orphan_rc = run_regular_tests(suite, stdout, result, options) |
1346 | + ret = orphan_rc or ret |
1347 | # Django-based components |
1348 | with IsolatedImportAndLogging(): |
1349 | - ppa = run_django_tests('ppa-assigner', 'ppa_assigner', True, |
1350 | - stdout, result, options) |
1351 | - ret = ppa or ret |
1352 | + ppa_rc = run_django_tests('ppa-assigner', 'ppa_assigner', True, |
1353 | + stdout, result, options) |
1354 | + ret = ppa_rc or ret |
1355 | with IsolatedImportAndLogging(): |
1356 | - ts = run_django_tests('ticket_system', 'ticket_system', True, |
1357 | - stdout, result, options) |
1358 | - ret = ts or ret |
1359 | + ts_rc = run_django_tests('ticket_system', 'ticket_system', True, |
1360 | + stdout, result, options) |
1361 | + ret = ts_rc or ret |
1362 | + # integration tests |
1363 | + with IsolatedImportAndLogging(), SysPath(['ci-utils']): |
1364 | + # FIXME: We need some more setup here that tests themselves should do |
1365 | + # but they aren't ripe for that yet -- vila 2014-04-11 |
1366 | + deploy.setup(for_tests=True, list_only=options.list_only) |
1367 | + suite = load_integration_tests(options.include_regexps, |
1368 | + options.exclude_regexps) |
1369 | + itg_rc = run_regular_tests(suite, stdout, result, options) |
1370 | + ret = itg_rc or ret |
1371 | + |
1372 | if not options.list_only: |
1373 | # We did run tests, stop the run |
1374 | result.stopTestRun() |
1375 | |
1376 | === modified file 'tarmac.sh' |
1377 | --- tarmac.sh 2014-03-26 15:35:02 +0000 |
1378 | +++ tarmac.sh 2014-04-15 12:18:41 +0000 |
1379 | @@ -58,7 +58,7 @@ |
1380 | ./juju-deployer/update.py --assert-pinned |
1381 | ./juju-deployer/test_update.py |
1382 | ./juju-deployer/test_deploy.py |
1383 | -./tests/test_run.py |
1384 | +./juju-deployer/test_run.py |
1385 | ' |
1386 | # Do not want to parse newlines as the internal field separator. It would break |
1387 | # any test with arguments. |
1388 | |
1389 | === modified file 'test_runner/tstrun/__init__.py' |
1390 | --- test_runner/tstrun/__init__.py 2014-04-10 18:03:39 +0000 |
1391 | +++ test_runner/tstrun/__init__.py 2014-04-15 12:18:41 +0000 |
1392 | @@ -20,11 +20,14 @@ |
1393 | HERE = os.path.abspath(os.path.dirname(__file__)) |
1394 | |
1395 | |
1396 | +HERE = os.path.abspath(os.path.dirname(__file__)) |
1397 | + |
1398 | + |
1399 | # Creating the nova instance for the testbed and storing test results in the |
1400 | # data store require nova credentials. |
1401 | def get_auth_config(path=None): |
1402 | if path is None: |
1403 | - path = os.path.abspath(os.path.join(HERE, '../../unit_config')) |
1404 | + path = os.path.abspath(os.path.join(HERE, '..', '..', 'unit_config')) |
1405 | with open(path) as f: |
1406 | config = yaml.safe_load(f) |
1407 | return config |
1408 | |
1409 | === modified file 'test_runner/tstrun/tests/test_data_store.py' |
1410 | --- test_runner/tstrun/tests/test_data_store.py 2014-02-17 10:13:29 +0000 |
1411 | +++ test_runner/tstrun/tests/test_data_store.py 2014-04-15 12:18:41 +0000 |
1412 | @@ -103,6 +103,10 @@ |
1413 | # Get our content back |
1414 | self.assertEqual(content, self.store.get_file(rel_path)) |
1415 | |
1416 | + def test_create_container_twice(self): |
1417 | + # swift is perfectly happy to create the same container twice. |
1418 | + self.store._create_container(self.store.container) |
1419 | + |
1420 | |
1421 | if __name__ == '__main__': |
1422 | unittest.main() |
1423 | |
1424 | === removed directory 'tests/data_store' |
1425 | === modified file 'tests/deployers.py' |
1426 | --- tests/deployers.py 2014-03-10 22:25:00 +0000 |
1427 | +++ tests/deployers.py 2014-04-15 12:18:41 +0000 |
1428 | @@ -15,16 +15,15 @@ |
1429 | |
1430 | import base64 |
1431 | import os |
1432 | -import shutil |
1433 | import subprocess |
1434 | import unittest |
1435 | |
1436 | + |
1437 | +import amulet |
1438 | import yaml |
1439 | |
1440 | -import amulet |
1441 | |
1442 | -defdir = os.path.join(os.path.dirname(__file__), '../juju-deployer') |
1443 | -DEPLOYER_DIR = os.environ.get('JUJU_DEPLOYER_DIR', defdir) |
1444 | +HERE = os.path.dirname(__file__) |
1445 | |
1446 | |
1447 | class AmuletDeployment(object): |
1448 | @@ -57,6 +56,9 @@ |
1449 | val = base64.b64encode(f.read()) |
1450 | service['options'][key] = val |
1451 | |
1452 | + def tearDown(self): |
1453 | + self.deployment.cleanup() |
1454 | + |
1455 | |
1456 | class DeployerTest(unittest.TestCase): |
1457 | '''Base class for building juju deployer based tests.''' |
1458 | @@ -66,19 +68,17 @@ |
1459 | |
1460 | def setUp(self): |
1461 | super(DeployerTest, self).setUp() |
1462 | - self.deployer = AmuletDeployment(self.deployer_cfg) |
1463 | - self.addCleanup(self._clean_amulet) |
1464 | - if self.timeout: |
1465 | - self.deployer.setUp(self.timeout) |
1466 | + self.deployer = AmuletDeployment( |
1467 | + os.path.join(os.environ['JUJU_DEPLOYER_DIR'], self.deployer_cfg)) |
1468 | + self.addCleanup(self.deployer.tearDown) |
1469 | + self.deployer.setUp(self.timeout) |
1470 | self.status = amulet.waiter.status(self.deployer.deployment.juju_env) |
1471 | |
1472 | - def _clean_amulet(self): |
1473 | - if self.deployer.deployment.deployer_dir: |
1474 | - shutil.rmtree(self.deployer.deployment.deployer_dir) |
1475 | - |
1476 | def get_ip_and_port(self, service, unit=0): |
1477 | units = self.status['services'][service]['units'] |
1478 | ip = units['%s/%d' % (service, unit)]['public-address'] |
1479 | + # FIXME: Can fail with KeyError: 'open-ports'. Need to refresh status ? |
1480 | + # -- vila 2014-04-10 |
1481 | port, _ = units['%s/%d' % (service, unit)]['open-ports'][0].split('/') |
1482 | return ip, int(port) |
1483 | |
1484 | |
1485 | === removed directory 'tests/image_builder' |
1486 | === removed directory 'tests/integration' |
1487 | === removed directory 'tests/juju_gui' |
1488 | === removed directory 'tests/ppa_assigner' |
1489 | === modified file 'tests/run.py' |
1490 | --- tests/run.py 2014-04-10 14:55:27 +0000 |
1491 | +++ tests/run.py 2014-04-15 12:18:41 +0000 |
1492 | @@ -1,23 +1,28 @@ |
1493 | #!/usr/bin/python |
1494 | +# Ubuntu CI Engine |
1495 | +# Copyright 2014 Canonical Ltd. |
1496 | + |
1497 | +# This program is free software: you can redistribute it and/or modify it |
1498 | +# under the terms of the GNU Affero General Public License version 3, as |
1499 | +# published by the Free Software Foundation. |
1500 | + |
1501 | +# This program is distributed in the hope that it will be useful, but |
1502 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1503 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1504 | +# PURPOSE. See the GNU Affero General Public License for more details. |
1505 | + |
1506 | +# You should have received a copy of the GNU Affero General Public License |
1507 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1508 | |
1509 | import atexit |
1510 | import logging |
1511 | import os |
1512 | -import shutil |
1513 | -import signal |
1514 | import subprocess |
1515 | import sys |
1516 | -import tempfile |
1517 | import time |
1518 | import yaml |
1519 | |
1520 | -import swiftclient |
1521 | - |
1522 | -from novaclient.v1_1 import client |
1523 | -from novaclient.exceptions import NotFound |
1524 | - |
1525 | -SSH_OPTS = ['-o', 'UserKnownHostsFile=/dev/null', |
1526 | - '-o', 'StrictHostKeyChecking=no'] |
1527 | + |
1528 | SUDOERS_FILE = '/etc/sudoers.d/sshuttle' |
1529 | SUDOERS = 'ALL = (root) NOPASSWD: /usr/bin/python /usr/lib/sshuttle/main.py *' |
1530 | |
1531 | @@ -28,9 +33,8 @@ |
1532 | |
1533 | HERE = os.path.abspath(os.path.dirname(__file__)) |
1534 | |
1535 | - |
1536 | -# deploy.py probably isn't in our pythonpath |
1537 | -sys.path.append(os.path.join(os.path.dirname(__file__), '../juju-deployer')) |
1538 | +# deploy.py isn't in our pythonpath when run as a script |
1539 | +sys.path.append(os.path.join(HERE, '../juju-deployer')) |
1540 | import deploy |
1541 | |
1542 | |
1543 | @@ -86,6 +90,9 @@ |
1544 | """Assert that there are no deployed services or nodes beyond the bootstrap |
1545 | node.""" |
1546 | |
1547 | + # FIXME: This can block on ci-juju-gui in an error state (seen in real |
1548 | + # life). -- vila 2014-02-14 |
1549 | + |
1550 | # First ensure that anything dying has made the transition to the other |
1551 | # side. |
1552 | _wait_for_death() |
1553 | @@ -117,33 +124,30 @@ |
1554 | _destroy_service(service) |
1555 | |
1556 | |
1557 | -def _find_test_files(test_dir, tests): |
1558 | - for test in tests: |
1559 | - actual_test = os.path.join(test_dir, test, 'test.py') |
1560 | - if not os.path.exists(actual_test): |
1561 | - msg = "Skipping %s as %s does not exist." % (test, actual_test) |
1562 | - log.error(msg) |
1563 | - else: |
1564 | - yield actual_test |
1565 | - |
1566 | - |
1567 | -def _find_dirs_in_dir(test_dir): |
1568 | - """Return an iterator of only the directories in test_dir.""" |
1569 | - |
1570 | - for x in os.listdir(test_dir): |
1571 | - if os.path.isdir(os.path.join(test_dir, x)): |
1572 | - yield x |
1573 | - |
1574 | - |
1575 | def get_tests(test_dir): |
1576 | """Returns a list of full relative paths to test files.""" |
1577 | - |
1578 | - tests = os.environ.get('TESTS', '') |
1579 | - if tests: |
1580 | - tests = tests.split(' ') |
1581 | - else: |
1582 | - tests = _find_dirs_in_dir(test_dir) |
1583 | - return list(_find_test_files(test_dir, tests)) |
1584 | + tests = [] |
1585 | + for name in os.listdir(test_dir): |
1586 | + if name.startswith('test_') and name.endswith('.py'): |
1587 | + tests.append(name) |
1588 | + only = os.environ.get('TESTS', '') |
1589 | + if only: |
1590 | + selected = [] |
1591 | + only = only.split() |
1592 | + # TESTS specifies which components should be tested, for compatibility |
1593 | + # with the previous layout where a 'test.py' file was under a directory |
1594 | + # named like the component, we keep matching on that. |
1595 | + for one in only: |
1596 | + file_name = 'test_{}.py'.format(one) |
1597 | + if file_name in tests: |
1598 | + selected.append(file_name) |
1599 | + else: |
1600 | + msg = "Skipping {} as {} does not exist in {}" |
1601 | + log.error(msg.format(one, file_name, test_dir)) |
1602 | + tests = selected |
1603 | + # Prepend the test dir to all selected tests |
1604 | + tests = [os.path.join(test_dir, name) for name in tests] |
1605 | + return tests |
1606 | |
1607 | |
1608 | def find_failed_units(): |
1609 | @@ -166,285 +170,6 @@ |
1610 | subprocess.check_call(cmd) |
1611 | |
1612 | |
1613 | -def install_charmworld(where=None): |
1614 | - """Recent versions of amulet need charmworldlib. Install it.""" |
1615 | - if where is None: |
1616 | - where = os.path.join(HERE, '..', 'branches') |
1617 | - dest = os.path.abspath(os.path.join(where, 'charmworldlib')) |
1618 | - deploy.get_or_pull_branch('lp:charmworldlib', dest) |
1619 | - deploy.prepend_path('PYTHONPATH', dest) |
1620 | - return dest |
1621 | - |
1622 | - |
1623 | -def install_amulet(where=None): |
1624 | - if where is None: |
1625 | - where = os.path.join(HERE, '..', 'branches') |
1626 | - dest = os.path.abspath(os.path.join(where, 'amulet')) |
1627 | - deploy.get_or_pull_branch('lp:amulet', dest) |
1628 | - deploy.prepend_path('PYTHONPATH', dest) |
1629 | - return dest |
1630 | - |
1631 | - |
1632 | -def remove_branch(branch_dir): |
1633 | - """Clean up the bzr branch if we didn't already do so as a result of |
1634 | - bzr failing.""" |
1635 | - |
1636 | - if os.path.exists(branch_dir): |
1637 | - shutil.rmtree(branch_dir) |
1638 | - |
1639 | - |
1640 | -def needs_bootstrap(): |
1641 | - """Check to see if there's a Swift bucket for juju. If not, we need to |
1642 | - boostrap. Check to see if the IP of the bootstrap node is owned by a |
1643 | - running Nova instance. If not, we need to bootstrap.""" |
1644 | - |
1645 | - log.info("Checking to see if a bootstrap is needed.") |
1646 | - |
1647 | - def inconsistent_state(msg): |
1648 | - log.warning(msg) |
1649 | - _delete_control_bucket() |
1650 | - |
1651 | - try: |
1652 | - instance = get_bootstrap_nova_instance() |
1653 | - except swiftclient.exceptions.ClientException: |
1654 | - instance = None |
1655 | - if instance is None: |
1656 | - # We don't have an instance. We need to bootstrap. |
1657 | - inconsistent_state('No bootstrap instance in the swift bucket') |
1658 | - return True |
1659 | - |
1660 | - try: |
1661 | - get_bootstrap_ip(instance, 1) |
1662 | - except NotFound: |
1663 | - # Juju claims we have an instance that does not exist. We're in an |
1664 | - # inconsistent state. Delete the bucket and bootstrap. |
1665 | - inconsistent_state('No IP for the bootstrap instance') |
1666 | - return True |
1667 | - |
1668 | - return False |
1669 | - |
1670 | - |
1671 | -def _get_pid_of_sshuttle(ip_addr): |
1672 | - try: |
1673 | - cmd = ['pgrep', '-f', 'sshuttle.*%s' % ip_addr] |
1674 | - pid = subprocess.check_output(cmd) |
1675 | - # pgrep returns a list of pids matching the search argument |
1676 | - pid.split('\n')[0] |
1677 | - return int(pid) |
1678 | - except subprocess.CalledProcessError: |
1679 | - return -1 |
1680 | - |
1681 | - |
1682 | -def needs_tunnel(): |
1683 | - """Check to see if a tunnel is required and up. |
1684 | - |
1685 | - When required, it depends on juju's Swift bucket existing, and the IP it |
1686 | - refers to is pointed at by a sshuttle instance. |
1687 | - """ |
1688 | - log.info("Checking to see if a tunnel needs to be setup.") |
1689 | - |
1690 | - instance = get_bootstrap_nova_instance() |
1691 | - ip_addr = get_bootstrap_ip(instance, 1) |
1692 | - if private_IP(ip_addr): |
1693 | - sshuttle_pid = _get_pid_of_sshuttle(ip_addr) |
1694 | - return sshuttle_pid < 0 |
1695 | - else: |
1696 | - return False |
1697 | - |
1698 | - |
1699 | -def _connect_to_swift(): |
1700 | - opts = {'tenant_name': os.environ['OS_TENANT_NAME'], |
1701 | - 'region_name': os.environ['OS_REGION_NAME'], |
1702 | - } |
1703 | - conn = swiftclient.client.Connection( |
1704 | - os.environ['OS_AUTH_URL'], |
1705 | - os.environ['OS_USERNAME'], |
1706 | - os.environ['OS_PASSWORD'], |
1707 | - os_options=opts, |
1708 | - auth_version='2.0') |
1709 | - return conn |
1710 | - |
1711 | - |
1712 | -def _get_from_swift(bucket, path): |
1713 | - """Download the file path from swift at bucket.""" |
1714 | - |
1715 | - conn = _connect_to_swift() |
1716 | - # get_object return a tuple of (metadata, data) |
1717 | - return conn.get_object(bucket, path)[1] |
1718 | - |
1719 | - |
1720 | -def _get_control_bucket(): |
1721 | - """Get the name of the bucket in Swift that juju uses for state |
1722 | - information.""" |
1723 | - |
1724 | - default_home = os.path.join(os.path.expanduser('~'), '.juju') |
1725 | - juju_home = os.environ.get('JUJU_HOME') or default_home |
1726 | - |
1727 | - with open(os.path.join(juju_home, 'environments.yaml')) as fp: |
1728 | - envs = yaml.safe_load(fp.read()) |
1729 | - |
1730 | - juju_env = os.environ.get('JUJU_ENV') |
1731 | - if juju_env is None: |
1732 | - juju_env = envs.get('default') |
1733 | - if not juju_env: |
1734 | - log.error('No juju environment specified.') |
1735 | - sys.exit(1) |
1736 | - |
1737 | - return envs['environments'][juju_env]['control-bucket'] |
1738 | - |
1739 | - |
1740 | -def _delete_control_bucket(): |
1741 | - """Remove the juju control bucket and all of its files.""" |
1742 | - |
1743 | - bucket = _get_control_bucket() |
1744 | - conn = _connect_to_swift() |
1745 | - try: |
1746 | - files = [x['name'] for x in conn.get_container(bucket)[1]] |
1747 | - for f in files: |
1748 | - conn.delete_object(bucket, f) |
1749 | - conn.delete_container(bucket) |
1750 | - except swiftclient.exceptions.ClientException: |
1751 | - # Assume the bucket is not present |
1752 | - pass |
1753 | - |
1754 | - |
1755 | -def get_bootstrap_nova_instance(): |
1756 | - """Get the Nova instance identifier for the juju bootstrap node. |
1757 | - |
1758 | - Raises swiftclient.exceptions.ClientException if the instance cannot be |
1759 | - found.""" |
1760 | - |
1761 | - bucket = _get_control_bucket() |
1762 | - |
1763 | - state = yaml.safe_load(_get_from_swift(bucket, 'provider-state')) |
1764 | - if not state: |
1765 | - # Cause unknown (yet) but this has been seen in real life. I can't |
1766 | - # reproduce anymore but I suspect 'juju bootstrap' failed mid-way so |
1767 | - # returning None and letting the caller delete the bucket is the Right |
1768 | - # Thing to do -- vila 2014-02-07 |
1769 | - log.warning("Empty 'provider-state' in the juju bucket") |
1770 | - return None |
1771 | - return state['state-instances'][0] |
1772 | - |
1773 | - |
1774 | -def private_IP(addr): |
1775 | - """Check whether an IP address can be routed. |
1776 | - :param addr: The IP address as a string. |
1777 | - |
1778 | - :returns: True if the address is private (can't be routed). False |
1779 | - otherwise. |
1780 | - |
1781 | - """ |
1782 | - if addr.startswith('10.'): # 10.0.0.0 - 10.255.255.255 |
1783 | - return True |
1784 | - elif addr.startswith('192.168'): # 192.168.0.0 - 192.168.255.255 |
1785 | - return True |
1786 | - elif addr.startswith('172.'): # 172.16.0.0 - 172.31.255.255 |
1787 | - a, b, c, d = addr.split('.') |
1788 | - if 16 <= int(b) <= 31: |
1789 | - return True |
1790 | - return False |
1791 | - |
1792 | - |
1793 | -def get_bootstrap_ip(instance, max_tries=120): |
1794 | - """Wait for the juju bootstrap node to boot and obtain an IP address. |
1795 | - |
1796 | - This is a pre-requisite to setup a tunnel. |
1797 | - |
1798 | - :raises: NotFound if the instance has not been spawned. |
1799 | - |
1800 | - :raises: Exception if no IP can be found after max_tries*3 seconds. |
1801 | - """ |
1802 | - |
1803 | - log.info("Getting the bootstrap IP address.") |
1804 | - |
1805 | - args = [os.environ['OS_USERNAME'], os.environ['OS_PASSWORD'], |
1806 | - os.environ['OS_TENANT_NAME'], os.environ['OS_AUTH_URL']] |
1807 | - kwargs = {'region_name': os.environ['OS_REGION_NAME'], |
1808 | - 'service_type': 'compute'} |
1809 | - nt = client.Client(*args, **kwargs) |
1810 | - |
1811 | - tries = 0 |
1812 | - while tries < max_tries: |
1813 | - # Raises NotFound if the instance does not exist. |
1814 | - server = nt.servers.get(instance) |
1815 | - networks = server.networks.values() |
1816 | - if networks: |
1817 | - # We use a single network and don't care how it's named |
1818 | - net = networks[0] |
1819 | - if net: |
1820 | - # Either we have a single address or the last one has been |
1821 | - # added to make the instance reachable (floating or public). |
1822 | - net = net[-1] |
1823 | - # Don't sleep anymore. We have our IP, let's move on |
1824 | - return net |
1825 | - |
1826 | - tries += 1 |
1827 | - if tries != max_tries: |
1828 | - time.sleep(3) |
1829 | - raise Exception('Timed out waiting for bootstrap IP.') |
1830 | - |
1831 | - |
1832 | -def destroy_tunnel(): |
1833 | - if os.path.exists('sshuttle.pid'): |
1834 | - with open('sshuttle.pid') as fp: |
1835 | - pid = int(fp.read()) |
1836 | - os.kill(pid, signal.SIGTERM) |
1837 | - |
1838 | - |
1839 | -def tunnel(ip): |
1840 | - """Tunnel to the bootstrap node using sshuttle.""" |
1841 | - |
1842 | - log.info("Tunneling to the bootstrap node.") |
1843 | - |
1844 | - host = 'ubuntu@%s' % ip |
1845 | - mask = '10.55.0.0/16' |
1846 | - cmd = ['sshuttle', '-D', '-r', host, mask] |
1847 | - opt = ['-e', 'ssh %s' % ' '.join(SSH_OPTS)] |
1848 | - subprocess.check_call(cmd + opt) |
1849 | - atexit.register(destroy_tunnel) |
1850 | - |
1851 | - |
1852 | -def wait_for_node(ip): |
1853 | - """Wait for the bootstrap node to become available on ssh. We need to wait |
1854 | - for this state before attempting to set up a sshuttle.""" |
1855 | - |
1856 | - log.info("Waiting for the bootstrap node to respond to ssh.") |
1857 | - |
1858 | - cmd = ['ssh'] + SSH_OPTS + ['ubuntu@%s' % ip, 'exit'] |
1859 | - tries = 0 |
1860 | - while tries < 120: |
1861 | - try: |
1862 | - subprocess.check_call(cmd) |
1863 | - return |
1864 | - except subprocess.CalledProcessError: |
1865 | - tries += 1 |
1866 | - time.sleep(5) |
1867 | - |
1868 | - raise Exception('Timed out waiting for bootstrap node.') |
1869 | - |
1870 | - |
1871 | -def bootstrap(): |
1872 | - """Bootstrap juju. Specify needed as bootstrap to just bootstrap, otherwise |
1873 | - tunnel to bootstrap and tunnel.""" |
1874 | - |
1875 | - args = ['juju', 'bootstrap', '--constraints=mem=1024M'] |
1876 | - try: |
1877 | - subprocess.check_output(args) |
1878 | - except subprocess.CalledProcessError as e: |
1879 | - # FIXME: if check_output raise an exception, we don't get a proper |
1880 | - # output to check for the text below -- vila 2014-02-07 |
1881 | - if 'environment is already bootstrapped' not in e.output: |
1882 | - raise |
1883 | - |
1884 | - |
1885 | -def wait_then_tunnel(): |
1886 | - instance = get_bootstrap_nova_instance() |
1887 | - ip = get_bootstrap_ip(instance) |
1888 | - wait_for_node(ip) |
1889 | - tunnel(ip) |
1890 | - |
1891 | - |
1892 | def check_sudoers(sudoers_path=SUDOERS_FILE): |
1893 | """Check to see if /etc/sudoers.d/sshuttle exists and is of the right |
1894 | form.""" |
1895 | @@ -492,23 +217,10 @@ |
1896 | return rc == 0 |
1897 | |
1898 | |
1899 | -def _prepare_deployer(): |
1900 | - deployer_dir = os.path.join(os.path.dirname(__file__), '../juju-deployer') |
1901 | - temp = tempfile.mkdtemp() |
1902 | - atexit.register(lambda: shutil.rmtree(temp)) |
1903 | - working_dir = os.path.join(temp, 'deployer') |
1904 | - shutil.copytree(deployer_dir, working_dir) |
1905 | - deploy.upload_payload_to_swift() |
1906 | - deploy.populate_templates(deployer_dir, working_dir) |
1907 | - deploy.build_charms() |
1908 | - os.environ['JUJU_DEPLOYER_DIR'] = working_dir |
1909 | - |
1910 | - |
1911 | def main(): |
1912 | check_nova_env() |
1913 | |
1914 | test_dir = os.path.dirname(sys.argv[0]) |
1915 | - deploy.check_environment() |
1916 | tests = get_tests(test_dir) |
1917 | if not tests: |
1918 | # Running no tests is an error |
1919 | @@ -524,32 +236,22 @@ |
1920 | log.error(msg) |
1921 | sys.exit(1) |
1922 | |
1923 | - _prepare_deployer() |
1924 | + deploy.setup(for_tests=True) |
1925 | |
1926 | - install_amulet() |
1927 | - install_charmworld() |
1928 | - deploy.install_deployer() |
1929 | - deploy.install_jujuclient() |
1930 | + destroy_services() |
1931 | + assert_no_deployed_services() |
1932 | + atexit.register(destroy_services) |
1933 | |
1934 | # Now, insert the 'tests' dir in front of PYTHONPATH so we get our tests |
1935 | # without colliding with other 'tests' directories defined by other |
1936 | # packages (including our deps). |
1937 | deploy.prepend_path('PYTHONPATH', os.path.dirname(test_dir)) |
1938 | |
1939 | - if needs_bootstrap(): |
1940 | - bootstrap() |
1941 | - if needs_tunnel(): |
1942 | - wait_then_tunnel() |
1943 | - |
1944 | - destroy_services() |
1945 | - assert_no_deployed_services() |
1946 | - atexit.register(destroy_services) |
1947 | - |
1948 | returncode = 0 |
1949 | for test in tests: |
1950 | - log.error('========================================') |
1951 | - log.error('Testing %s' % test) |
1952 | - log.error('========================================') |
1953 | + log.info('========================================') |
1954 | + log.info('Testing %s' % test) |
1955 | + log.info('========================================') |
1956 | try: |
1957 | subprocess.check_call(['python', test]) |
1958 | except subprocess.CalledProcessError as e: |
1959 | |
1960 | === renamed file 'tests/data_store/test.py' => 'tests/test_data_store.py' |
1961 | --- tests/data_store/test.py 2014-02-17 10:13:29 +0000 |
1962 | +++ tests/test_data_store.py 2014-04-15 12:18:41 +0000 |
1963 | @@ -20,8 +20,7 @@ |
1964 | import sys |
1965 | import yaml |
1966 | |
1967 | -sys.path.append(os.path.join( |
1968 | - os.path.dirname(__file__), '..', '..', 'ci-utils')) |
1969 | +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'ci-utils')) |
1970 | |
1971 | from ci_utils import data_store |
1972 | |
1973 | |
1974 | === renamed file 'tests/image_builder/test.py' => 'tests/test_image_builder.py' |
1975 | --- tests/image_builder/test.py 2014-04-02 11:56:54 +0000 |
1976 | +++ tests/test_image_builder.py 2014-04-15 12:18:41 +0000 |
1977 | @@ -1,18 +1,31 @@ |
1978 | #!/usr/bin/env python |
1979 | +# Ubuntu Continuous Integration Engine |
1980 | +# Copyright 2014 Canonical Ltd. |
1981 | + |
1982 | +# This program is free software: you can redistribute it and/or modify it |
1983 | +# under the terms of the GNU Affero General Public License version 3, as |
1984 | +# published by the Free Software Foundation. |
1985 | + |
1986 | +# This program is distributed in the hope that it will be useful, but |
1987 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1988 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1989 | +# PURPOSE. See the GNU Affero General Public License for more details. |
1990 | + |
1991 | +# You should have received a copy of the GNU Affero General Public License |
1992 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1993 | |
1994 | import json |
1995 | -import os |
1996 | import unittest |
1997 | |
1998 | import httplib2 |
1999 | |
2000 | -from tests import deployers |
2001 | +import deployers |
2002 | |
2003 | |
2004 | class TestTestRunner(deployers.DeployerTest): |
2005 | """Integration tests for image builder service run on a juju deployment""" |
2006 | |
2007 | - deployer_cfg = os.path.join(deployers.DEPLOYER_DIR, 'image-builder.yaml') |
2008 | + deployer_cfg = 'image-builder.yaml' |
2009 | |
2010 | def get_status(self): |
2011 | url = 'http://{}:{}/api/v1/status'.format( |
2012 | |
2013 | === renamed file 'tests/integration/test.py' => 'tests/test_integration.py' |
2014 | --- tests/integration/test.py 2014-03-07 13:23:35 +0000 |
2015 | +++ tests/test_integration.py 2014-04-15 12:18:41 +0000 |
2016 | @@ -1,4 +1,18 @@ |
2017 | #!/usr/bin/env python |
2018 | +# Ubuntu Continuous Integration Engine |
2019 | +# Copyright 2014 Canonical Ltd. |
2020 | + |
2021 | +# This program is free software: you can redistribute it and/or modify it |
2022 | +# under the terms of the GNU Affero General Public License version 3, as |
2023 | +# published by the Free Software Foundation. |
2024 | + |
2025 | +# This program is distributed in the hope that it will be useful, but |
2026 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2027 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2028 | +# PURPOSE. See the GNU Affero General Public License for more details. |
2029 | + |
2030 | +# You should have received a copy of the GNU Affero General Public License |
2031 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2032 | |
2033 | import os |
2034 | import json |
2035 | @@ -9,7 +23,10 @@ |
2036 | |
2037 | import httplib2 |
2038 | |
2039 | -from tests.deployers import DEPLOYER_DIR, DeployerTest |
2040 | +import deployers |
2041 | + |
2042 | + |
2043 | +HERE = os.path.abspath(os.path.dirname(__file__)) |
2044 | |
2045 | |
2046 | def create_virtual_env(): |
2047 | @@ -24,10 +41,9 @@ |
2048 | |
2049 | def run_virtual_env(temp_dir, args, cwd=None): |
2050 | if args is 'create': |
2051 | - cwd = os.path.join(os.path.dirname(__file__), '../../cli') |
2052 | - changes = os.path.abspath(os.path.join( |
2053 | - os.path.dirname(__file__), |
2054 | - '../../cli/tests/data/foobar_0.1-1_source.changes')) |
2055 | + cwd = os.path.join(HERE, '../cli') |
2056 | + changes = os.path.abspath(os.path.join(HERE, |
2057 | + '../cli/tests/data/foobar_0.1-1_source.changes')) |
2058 | args = ('python ubuntu-ci create_ticket -t "New feature" -b 1234 -o ' |
2059 | 'someone@example.com -d "This is a cool new feature" -a foo ' |
2060 | '-s %s' % changes) |
2061 | @@ -39,18 +55,18 @@ |
2062 | subprocess.check_call(args, shell=True) |
2063 | |
2064 | |
2065 | -class IntegrationTest(DeployerTest): |
2066 | +class IntegrationTest(deployers.DeployerTest): |
2067 | """Integration tests for ticket-system service run on a juju deployment""" |
2068 | |
2069 | - deployer_cfg = os.path.join(DEPLOYER_DIR, 'ticket-system.yaml') |
2070 | + deployer_cfg = 'ticket-system.yaml' |
2071 | |
2072 | def setUp(self): |
2073 | super(IntegrationTest, self).setUp() |
2074 | self.temp_dir = create_virtual_env() |
2075 | - p = os.path.join(os.path.dirname(__file__), '../../ci-utils/setup.py') |
2076 | + p = os.path.join(HERE, '../ci-utils/setup.py') |
2077 | args = '%s develop' % p |
2078 | run_virtual_env(self.temp_dir, args) |
2079 | - p = os.path.join(os.path.dirname(__file__), '../../cli/setup.py') |
2080 | + p = os.path.join(HERE, '../cli/setup.py') |
2081 | args = '%s develop' % p |
2082 | run_virtual_env(self.temp_dir, args) |
2083 | config_url = 'http://{}:{}' |
2084 | @@ -94,5 +110,6 @@ |
2085 | self.assertEqual(resp['status'], '200') |
2086 | self.assertIn('<a href="/" class="first ">Ticket List</a>', content) |
2087 | |
2088 | + |
2089 | if __name__ == "__main__": |
2090 | unittest.main() |
2091 | |
2092 | === renamed file 'tests/juju_gui/test.py' => 'tests/test_juju_gui.py' |
2093 | --- tests/juju_gui/test.py 2014-01-29 17:27:07 +0000 |
2094 | +++ tests/test_juju_gui.py 2014-04-15 12:18:41 +0000 |
2095 | @@ -1,17 +1,30 @@ |
2096 | #!/usr/bin/env python |
2097 | - |
2098 | -import os |
2099 | +# Ubuntu Continuous Integration Engine |
2100 | +# Copyright 2014 Canonical Ltd. |
2101 | + |
2102 | +# This program is free software: you can redistribute it and/or modify it |
2103 | +# under the terms of the GNU Affero General Public License version 3, as |
2104 | +# published by the Free Software Foundation. |
2105 | + |
2106 | +# This program is distributed in the hope that it will be useful, but |
2107 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2108 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2109 | +# PURPOSE. See the GNU Affero General Public License for more details. |
2110 | + |
2111 | +# You should have received a copy of the GNU Affero General Public License |
2112 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2113 | + |
2114 | import unittest |
2115 | import urllib2 |
2116 | |
2117 | |
2118 | -from tests.deployers import DEPLOYER_DIR, DeployerTest |
2119 | - |
2120 | - |
2121 | -class JujuGuiTest(DeployerTest): |
2122 | +import deployers |
2123 | + |
2124 | + |
2125 | +class JujuGuiTest(deployers.DeployerTest): |
2126 | """Integration tests for juju-gui service run on a juju deployment""" |
2127 | |
2128 | - deployer_cfg = os.path.join(DEPLOYER_DIR, 'juju-gui.yaml') |
2129 | + deployer_cfg = 'juju-gui.yaml' |
2130 | |
2131 | def get_unit(self): |
2132 | unit = self.status['services']['ci-juju-gui']['units'].values()[0] |
2133 | |
2134 | === renamed file 'tests/ppa_assigner/test.py' => 'tests/test_ppa_assigner.py' |
2135 | --- tests/ppa_assigner/test.py 2014-03-14 10:37:28 +0000 |
2136 | +++ tests/test_ppa_assigner.py 2014-04-15 12:18:41 +0000 |
2137 | @@ -1,18 +1,32 @@ |
2138 | #!/usr/bin/env python |
2139 | - |
2140 | +# Ubuntu Continuous Integration Engine |
2141 | +# Copyright 2013, 2014 Canonical Ltd. |
2142 | + |
2143 | +# This program is free software: you can redistribute it and/or modify it |
2144 | +# under the terms of the GNU Affero General Public License version 3, as |
2145 | +# published by the Free Software Foundation. |
2146 | + |
2147 | +# This program is distributed in the hope that it will be useful, but |
2148 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2149 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2150 | +# PURPOSE. See the GNU Affero General Public License for more details. |
2151 | + |
2152 | +# You should have received a copy of the GNU Affero General Public License |
2153 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2154 | + |
2155 | +import httplib2 |
2156 | import json |
2157 | import os |
2158 | import unittest |
2159 | |
2160 | -import httplib2 |
2161 | - |
2162 | -from tests.deployers import DEPLOYER_DIR, DeployerTest |
2163 | - |
2164 | - |
2165 | -class PPAAssignerTest(DeployerTest): |
2166 | + |
2167 | +import deployers |
2168 | + |
2169 | + |
2170 | +class PPAAssignerTest(deployers.DeployerTest): |
2171 | """Integration tests for ppa-assigner service run on a juju deployment""" |
2172 | |
2173 | - deployer_cfg = os.path.join(DEPLOYER_DIR, 'ppa-assigner.yaml') |
2174 | + deployer_cfg = 'ppa-assigner.yaml' |
2175 | |
2176 | def get_server_status_and_content(self, url): |
2177 | final_url = url.format(*self.get_ip_and_port('ppa-django')) |
2178 | @@ -134,5 +148,6 @@ |
2179 | self.assertEqual('/api/v1/ppa/2/', |
2180 | content['objects'][0]['resource_uri']) |
2181 | |
2182 | + |
2183 | if __name__ == "__main__": |
2184 | unittest.main() |
2185 | |
2186 | === removed directory 'tests/test_runner' |
2187 | === renamed file 'tests/test_runner/test.py' => 'tests/test_test_runner.py' |
2188 | --- tests/test_runner/test.py 2014-04-02 11:56:54 +0000 |
2189 | +++ tests/test_test_runner.py 2014-04-15 12:18:41 +0000 |
2190 | @@ -1,18 +1,31 @@ |
2191 | #!/usr/bin/env python |
2192 | +# Ubuntu Continuous Integration Engine |
2193 | +# Copyright 2014 Canonical Ltd. |
2194 | + |
2195 | +# This program is free software: you can redistribute it and/or modify it |
2196 | +# under the terms of the GNU Affero General Public License version 3, as |
2197 | +# published by the Free Software Foundation. |
2198 | + |
2199 | +# This program is distributed in the hope that it will be useful, but |
2200 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2201 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2202 | +# PURPOSE. See the GNU Affero General Public License for more details. |
2203 | + |
2204 | +# You should have received a copy of the GNU Affero General Public License |
2205 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2206 | |
2207 | import json |
2208 | -import os |
2209 | import unittest |
2210 | |
2211 | import httplib2 |
2212 | |
2213 | -from tests import deployers |
2214 | +import deployers |
2215 | |
2216 | |
2217 | class TestTestRunner(deployers.DeployerTest): |
2218 | """Integration tests for test runner service run on a juju deployment""" |
2219 | |
2220 | - deployer_cfg = os.path.join(deployers.DEPLOYER_DIR, 'test-runner.yaml') |
2221 | + deployer_cfg = 'test-runner.yaml' |
2222 | |
2223 | def post_test_image(self, ticket_id, image_id, package_list, |
2224 | progress_trigger): |
2225 | |
2226 | === renamed file 'tests/ticket_system/test.py' => 'tests/test_ticket_system.py' |
2227 | --- tests/ticket_system/test.py 2014-04-01 15:02:54 +0000 |
2228 | +++ tests/test_ticket_system.py 2014-04-15 12:18:41 +0000 |
2229 | @@ -1,18 +1,31 @@ |
2230 | #!/usr/bin/env python |
2231 | +# Ubuntu Continuous Integration Engine |
2232 | +# Copyright 2014 Canonical Ltd. |
2233 | + |
2234 | +# This program is free software: you can redistribute it and/or modify it |
2235 | +# under the terms of the GNU Affero General Public License version 3, as |
2236 | +# published by the Free Software Foundation. |
2237 | + |
2238 | +# This program is distributed in the hope that it will be useful, but |
2239 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2240 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2241 | +# PURPOSE. See the GNU Affero General Public License for more details. |
2242 | + |
2243 | +# You should have received a copy of the GNU Affero General Public License |
2244 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2245 | |
2246 | import json |
2247 | -import os |
2248 | import unittest |
2249 | |
2250 | import httplib2 |
2251 | |
2252 | -from tests.deployers import DEPLOYER_DIR, DeployerTest |
2253 | - |
2254 | - |
2255 | -class TicketSystemTest(DeployerTest): |
2256 | +import deployers |
2257 | + |
2258 | + |
2259 | +class TicketSystemTest(deployers.DeployerTest): |
2260 | """Integration tests for ticket-system service run on a juju deployment""" |
2261 | |
2262 | - deployer_cfg = os.path.join(DEPLOYER_DIR, 'ticket-system.yaml') |
2263 | + deployer_cfg = 'ticket-system.yaml' |
2264 | |
2265 | def get_server_status_and_content(self, url): |
2266 | final_url = url.format(*self.get_ip_and_port('ts-django')) |
2267 | |
2268 | === removed directory 'tests/ticket_system' |
2269 | === modified file 'ticket_system/people/tests.py' |
2270 | --- ticket_system/people/tests.py 2014-02-08 10:43:30 +0000 |
2271 | +++ ticket_system/people/tests.py 2014-04-15 12:18:41 +0000 |
2272 | @@ -23,7 +23,7 @@ |
2273 | if 'people' in settings.LOCAL_APPS: |
2274 | return std_tests |
2275 | else: |
2276 | - # if people is not in LOCAL_APPS the tests cannot pass so get rid of |
2277 | + # If 'people' is not in LOCAL_APPS the tests cannot pass so get rid of |
2278 | # them by returning an empty test suite. |
2279 | return loader.suiteClass() |
2280 |
=== modified file 'branch- source- builder/ bsbuilder/ run_worker. py' (properties changed: +x to -x) builder/ imagebuilder/ run_worker. py' (properties changed: +x to -x) tstrun/ run_worker. py' (properties changed: +x to -x)
=== modified file 'image-
=== modified file 'test_runner/
:)