Merge lp:~mthaddon/mojo/juju-intro-mojo-specs into lp:mojo/mojo-specs
- juju-intro-mojo-specs
- Merge into mojo-specs
Proposed by
Tom Haddon
Status: | Merged |
---|---|
Merged at revision: | 9 |
Proposed branch: | lp:~mthaddon/mojo/juju-intro-mojo-specs |
Merge into: | lp:mojo/mojo-specs |
Diff against target: |
2179 lines (+2033/-27) 21 files modified
juju-intro/README (+5/-0) juju-intro/collect (+5/-0) juju-intro/deploy (+14/-0) juju-intro/manifest (+10/-0) juju-intro/manifest-verify (+4/-0) juju-intro/verify-installed (+10/-0) mojo-how-to/devel/verify (+0/-26) mojo-how-to/manifest-verify (+3/-1) mojo-spec-helpers/tests/check-juju (+5/-0) mojo-spec-helpers/tests/verify-nrpe (+37/-0) mojo-spec-helpers/utils/add-floating-ip (+114/-0) mojo-spec-helpers/utils/cache_managers.py (+56/-0) mojo-spec-helpers/utils/container_managers.py (+265/-0) mojo-spec-helpers/utils/mojo_os_utils.py (+554/-0) mojo-spec-helpers/utils/mojo_utils.py (+356/-0) mojo-spec-helpers/utils/shyaml.py (+219/-0) mojo-spec-helpers/utils/tests/README.md (+67/-0) mojo-spec-helpers/utils/tests/run_tests.py (+18/-0) mojo-spec-helpers/utils/tests/test_cache_managers.py (+80/-0) mojo-spec-helpers/utils/tests/test_container_managers.py (+188/-0) mojo-spec-helpers/utils/tests/test_mojo_utils.py (+23/-0) |
To merge this branch: | bzr merge lp:~mthaddon/mojo/juju-intro-mojo-specs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Collins | Approve | ||
Review via email: mp+270005@code.launchpad.net |
Commit message
Description of the change
Add a "juju-intro" service which uses the charms described on https:/
To post a comment you must log in.
Revision history for this message
Tom Haddon (mthaddon) wrote : | # |
Revision history for this message
Tom Haddon (mthaddon) wrote : | # |
This is now ready for review. Has run as follows: http://
- 70. By Tom Haddon
-
Just use the current promulgated series of each charm
- 71. By Tom Haddon
-
Add an e2e check for the site being up after initial install
Revision history for this message
Paul Collins (pjdc) wrote : | # |
Approving, although I noticed:
- running this spec with the local provider fails here due to mysql's default dataset-size of 80%; not sure how likely it is for someone to be using it, however
- mojo-spec-helpers may need another refresh (or is itself stale); add-floating-ip was rewritten in 100% Python recently (see internal trunk)
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'juju-intro' |
2 | === added file 'juju-intro/README' |
3 | --- juju-intro/README 1970-01-01 00:00:00 +0000 |
4 | +++ juju-intro/README 2015-09-08 08:23:55 +0000 |
5 | @@ -0,0 +1,5 @@ |
6 | +This spec contains the services that are included in the introduction to Juju |
7 | +as part of [1], and is intended to be used with CI to confirm that the charms |
8 | +we're pointing new users at for Juju are always in a working state. |
9 | + |
10 | +[1] https://jujucharms.com/docs/stable/getting-started |
11 | |
12 | === added symlink 'juju-intro/check-juju' |
13 | === target is u'../mojo-spec-helpers/tests/check-juju' |
14 | === added file 'juju-intro/collect' |
15 | --- juju-intro/collect 1970-01-01 00:00:00 +0000 |
16 | +++ juju-intro/collect 2015-09-08 08:23:55 +0000 |
17 | @@ -0,0 +1,5 @@ |
18 | +wordpress lp:charms/wordpress |
19 | +mysql lp:charms/mysql |
20 | + |
21 | +# subordinates |
22 | +nrpe lp:charms/nrpe-external-master |
23 | |
24 | === added file 'juju-intro/deploy' |
25 | --- juju-intro/deploy 1970-01-01 00:00:00 +0000 |
26 | +++ juju-intro/deploy 2015-09-08 08:23:55 +0000 |
27 | @@ -0,0 +1,14 @@ |
28 | +wordpress: |
29 | + series: {{ series }} |
30 | + services: |
31 | + wordpress: |
32 | + charm: wordpress |
33 | + expose: true |
34 | + mysql: |
35 | + charm: mysql |
36 | + nrpe: |
37 | + charm: nrpe |
38 | + relations: |
39 | + - ["wordpress", "mysql"] |
40 | + - ["wordpress", "nrpe"] |
41 | + - ["mysql", "nrpe"] |
42 | |
43 | === added file 'juju-intro/manifest' |
44 | --- juju-intro/manifest 1970-01-01 00:00:00 +0000 |
45 | +++ juju-intro/manifest 2015-09-08 08:23:55 +0000 |
46 | @@ -0,0 +1,10 @@ |
47 | +collect |
48 | +deploy delay=0 |
49 | +include config=manifest-verify |
50 | + |
51 | +## This isn't included in the main verify manifest because after we've installed |
52 | +## the service this won't work as expected so it's a one time thing for the |
53 | +## initial deployment |
54 | + |
55 | +# Verify the site is installed |
56 | +verify config=verify-installed |
57 | |
58 | === added file 'juju-intro/manifest-verify' |
59 | --- juju-intro/manifest-verify 1970-01-01 00:00:00 +0000 |
60 | +++ juju-intro/manifest-verify 2015-09-08 08:23:55 +0000 |
61 | @@ -0,0 +1,4 @@ |
62 | +# Checking juju status |
63 | +verify config=check-juju |
64 | +# Running all naigos checks to confirm service is working as expected |
65 | +verify config=verify-nrpe |
66 | |
67 | === added file 'juju-intro/verify-installed' |
68 | --- juju-intro/verify-installed 1970-01-01 00:00:00 +0000 |
69 | +++ juju-intro/verify-installed 2015-09-08 08:23:55 +0000 |
70 | @@ -0,0 +1,10 @@ |
71 | +#!/bin/bash |
72 | + |
73 | +set -e |
74 | +set -u |
75 | + |
76 | +# Check the service is actually up and we can get to the install page |
77 | +# This isn't included in the main verify manifest because after we've installed |
78 | +# the service this won't work as expected so it's a one time thing for the |
79 | +# initial deployment |
80 | +juju ssh wordpress/0 "/usr/lib/nagios/plugins/check_http -I 127.0.0.1 -H localhost -f follow -s '<title>WordPress › Installation</title>'" |
81 | |
82 | === added symlink 'juju-intro/verify-nrpe' |
83 | === target is u'../mojo-spec-helpers/tests/verify-nrpe' |
84 | === removed file 'mojo-how-to/devel/verify' |
85 | --- mojo-how-to/devel/verify 2015-01-26 16:23:38 +0000 |
86 | +++ mojo-how-to/devel/verify 1970-01-01 00:00:00 +0000 |
87 | @@ -1,26 +0,0 @@ |
88 | -#!/bin/bash |
89 | - |
90 | -set -e |
91 | - |
92 | -# If we have any etc bzr nagios checks, we need to wait up to 15 minutes |
93 | -# for the cron to run to populate the check file, so just ignore those |
94 | -NAGIOS_OUTPUT=$(juju status | sed -rn 's/^ {8}public-address: //p'| xargs -I% ssh ubuntu@% 'egrep -oh /usr.*lib.* /etc/nagios/nrpe.d/check_* |grep -v check_etc_bzr.py |sed "s/.*/(set -x; &) || echo MOJO_NAGIOS_FAIL /"|sudo -u nagios -s bash |& sed "s/^/%: /"' 2>/dev/null) |
95 | - |
96 | -echo "${NAGIOS_OUTPUT}" |
97 | - |
98 | -NAGIOS_FAIL=$(echo "${NAGIOS_OUTPUT}" | grep MOJO_NAGIOS_FAIL) || true |
99 | - |
100 | -if [ -n "${NAGIOS_FAIL}" ]; then |
101 | - echo "########################" |
102 | - echo "# Nagios Checks Failed #" |
103 | - echo "########################" |
104 | - exit 1 |
105 | -else |
106 | - echo "########################" |
107 | - echo "# Nagios Checks Passed #" |
108 | - echo "########################" |
109 | -fi |
110 | - |
111 | -echo "#########################" |
112 | -echo "# Successfully verified #" |
113 | -echo "#########################" |
114 | |
115 | === modified file 'mojo-how-to/manifest-verify' |
116 | --- mojo-how-to/manifest-verify 2015-01-21 15:39:29 +0000 |
117 | +++ mojo-how-to/manifest-verify 2015-09-08 08:23:55 +0000 |
118 | @@ -1,2 +1,4 @@ |
119 | +# Check juju |
120 | +verify config=check-juju |
121 | # The service is up and running, let's verify it |
122 | -verify |
123 | +verify config=verify-nrpe |
124 | |
125 | === removed symlink 'mojo-how-to/production/verify' |
126 | === target was u'../devel/verify' |
127 | === added directory 'mojo-spec-helpers' |
128 | === added directory 'mojo-spec-helpers/tests' |
129 | === added file 'mojo-spec-helpers/tests/check-juju' |
130 | --- mojo-spec-helpers/tests/check-juju 1970-01-01 00:00:00 +0000 |
131 | +++ mojo-spec-helpers/tests/check-juju 2015-09-08 08:23:55 +0000 |
132 | @@ -0,0 +1,5 @@ |
133 | +#!/usr/bin/python |
134 | +import utils.mojo_utils as mojo_utils |
135 | + |
136 | +mojo_utils.juju_check_hooks_complete() |
137 | +mojo_utils.juju_status_check_and_wait() |
138 | |
139 | === added symlink 'mojo-spec-helpers/tests/utils' |
140 | === target is u'../utils' |
141 | === added file 'mojo-spec-helpers/tests/verify-nrpe' |
142 | --- mojo-spec-helpers/tests/verify-nrpe 1970-01-01 00:00:00 +0000 |
143 | +++ mojo-spec-helpers/tests/verify-nrpe 2015-09-08 08:23:55 +0000 |
144 | @@ -0,0 +1,37 @@ |
145 | +#!/bin/bash |
146 | + |
147 | +set -e |
148 | + |
149 | +# If we have any etc bzr nagios checks, we need to wait up to 15 minutes |
150 | +# for the cron to run to populate the check file, so just ignore those |
151 | +check() { |
152 | + juju ssh $1 'egrep -oh /usr.*lib.* /etc/nagios/nrpe.d/check_* |\ |
153 | + grep -v check_etc_bzr.py |sed "s/.*/(set -x; &) || \ |
154 | + echo MOJO_NAGIOS_FAIL /"|sudo -u nagios -s bash' 2>/dev/null |
155 | +} |
156 | + |
157 | +NRPE_UNITS=$(juju status | sed -rn 's/^ *(nrpe\/[0-9]*):$/\1/p') |
158 | +NAGIOS_OUTPUT=$( |
159 | + for unit in $NRPE_UNITS; do |
160 | + check $unit | sed --e "s#^#$unit: #" |
161 | + done |
162 | +) |
163 | + |
164 | +echo "${NAGIOS_OUTPUT}" |
165 | + |
166 | +NAGIOS_FAIL=$(echo "${NAGIOS_OUTPUT}" | grep MOJO_NAGIOS_FAIL) || true |
167 | + |
168 | +if [ -n "${NAGIOS_FAIL}" ]; then |
169 | + echo "########################" |
170 | + echo "# Nagios Checks Failed #" |
171 | + echo "########################" |
172 | + exit 1 |
173 | +else |
174 | + echo "########################" |
175 | + echo "# Nagios Checks Passed #" |
176 | + echo "########################" |
177 | +fi |
178 | + |
179 | +echo "########################" |
180 | +echo "# Succesfully verified #" |
181 | +echo "########################" |
182 | |
183 | === added directory 'mojo-spec-helpers/utils' |
184 | === added file 'mojo-spec-helpers/utils/__init__.py' |
185 | === added file 'mojo-spec-helpers/utils/add-floating-ip' |
186 | --- mojo-spec-helpers/utils/add-floating-ip 1970-01-01 00:00:00 +0000 |
187 | +++ mojo-spec-helpers/utils/add-floating-ip 2015-09-08 08:23:55 +0000 |
188 | @@ -0,0 +1,114 @@ |
189 | +#!/bin/sh |
190 | +# |
191 | +# Author: Paul Gear |
192 | +# Description: Manage floating IP allocations in mojo local directory for a juju service or unit. |
193 | +# NOTE: $MOJO_PROJECT and $MOJO_STAGE must be set before calling this script. |
194 | +# |
195 | + |
196 | +set -e |
197 | +set -u |
198 | + |
199 | + |
200 | +SECRETS_DIR="/srv/mojo/LOCAL/$MOJO_PROJECT/$MOJO_STAGE/" |
201 | + |
202 | +# Echo to standard error |
203 | +# Useful for printing from functions without polluting the "returned" output |
204 | +echo_stderr() { echo "$@" 1>&2; } |
205 | + |
206 | +# print the juju unit followed by the machine instance id |
207 | +get_juju_units() |
208 | +{ |
209 | + juju status "$@" | python -c ' |
210 | +import sys, yaml |
211 | +status = yaml.safe_load(sys.stdin) |
212 | +for serv in status["services"]: |
213 | + if status["services"][serv].get("units"): |
214 | + for unit in status["services"][serv]["units"]: |
215 | + machine = status["services"][serv]["units"][unit]["machine"] |
216 | + instance = status["machines"][machine]["instance-id"] |
217 | + print unit, instance |
218 | +' |
219 | +} |
220 | + |
221 | + |
222 | +# get the existing floating IP for this unit, or create a new one |
223 | +get_unit_floating_ip() |
224 | +{ |
225 | + UNIT="$1" |
226 | + MACHINE="$2" |
227 | + UNIT_FILE_NAME=$(echo "$UNIT" | tr '/' '_') |
228 | + FLOATING_IP_FILE="$SECRETS_DIR/$UNIT_FILE_NAME" |
229 | + IP="" |
230 | + if [ -s "$FLOATING_IP_FILE" ]; then |
231 | + IP=$(head -n 1 "$FLOATING_IP_FILE") |
232 | + |
233 | + echo_stderr "- Found IP "$IP" for "$UNIT |
234 | + |
235 | + # check how the IP is used now |
236 | + ALLOCATION=$(nova floating-ip-list | awk -v IP="$IP" '$2 == IP {print $5}') |
237 | + case $ALLOCATION in |
238 | + "-") |
239 | + # unallocated - we can use it |
240 | + echo_stderr "- IP "$IP" is currently unallocated" |
241 | + ;; |
242 | + "") |
243 | + # non-existent - we'll create one below |
244 | + IP="" |
245 | + echo_stderr "- No IP found for "$UNIT |
246 | + ;; |
247 | + *) |
248 | + # allocated to a unit already |
249 | + if nova show $MACHINE | awk '$3 == "network"' | sed -re 's/,? +/\n/g' | grep -q "^$IP$"; then |
250 | + # it's allocated to us; do nothing |
251 | + echo_stderr "- IP "$IP" is already allocated to "$UNIT |
252 | + return |
253 | + fi |
254 | + # it's allocated to another unit - create one below |
255 | + IP="" |
256 | + echo_stderr "- IP "$IP" is allocated to another unit" |
257 | + ;; |
258 | + esac |
259 | + fi |
260 | + if [ -z "$IP" ]; then |
261 | + IP=$(nova floating-ip-create | grep -wo '[0-9.a-f:]*') |
262 | + echo "$IP" > "$FLOATING_IP_FILE" |
263 | + echo_stderr "- Created new IP "$IP |
264 | + fi |
265 | + echo "$IP" |
266 | +} |
267 | + |
268 | + |
269 | +usage() |
270 | +{ |
271 | + cat <<EOF |
272 | +Usage: $0 {service|unit} |
273 | + |
274 | +# Add a floating IP to the apache2/0 unit: |
275 | +add-floating-ip apache2/0 |
276 | + |
277 | +# Add floating IPs to all units the jenkins-slave service: |
278 | +add-floating-ip jenkins-slave |
279 | + |
280 | +# Add floating IPs to all units in the haproxy and squid services: |
281 | +add-floating-ip haproxy squid |
282 | + |
283 | +EOF |
284 | + exit 2 |
285 | +} |
286 | + |
287 | + |
288 | +if [ "$#" -lt 1 ]; then |
289 | + usage |
290 | +fi |
291 | + |
292 | +for i in "$@"; do |
293 | + get_juju_units "$i" | while read unit machine; do |
294 | + echo_stderr "" |
295 | + echo_stderr "Assigning IPs for "$unit |
296 | + IP=$(get_unit_floating_ip "$unit" "$machine") |
297 | + if [ -n "$IP" ]; then |
298 | + echo_stderr "- Assigning IP "$IP" to "$unit |
299 | + nova floating-ip-associate "$machine" "$IP" |
300 | + fi |
301 | + done |
302 | +done |
303 | |
304 | === added file 'mojo-spec-helpers/utils/cache_managers.py' |
305 | --- mojo-spec-helpers/utils/cache_managers.py 1970-01-01 00:00:00 +0000 |
306 | +++ mojo-spec-helpers/utils/cache_managers.py 2015-09-08 08:23:55 +0000 |
307 | @@ -0,0 +1,56 @@ |
308 | +# System |
309 | +import os |
310 | +import json |
311 | + |
312 | + |
313 | +class JsonCache: |
314 | + """ |
315 | + Store key value pairs in a JSON file |
316 | + """ |
317 | + |
318 | + def __init__(self, cache_path): |
319 | + self.cache_path = cache_path |
320 | + |
321 | + def get_cache(self): |
322 | + """ |
323 | + Get the dictionary from the cache file |
324 | + or if it doesn't exist, return an empty dictionary |
325 | + """ |
326 | + |
327 | + if os.path.isfile(self.cache_path): |
328 | + with open(self.cache_path) as cache_file: |
329 | + return json.load(cache_file) |
330 | + else: |
331 | + return {} |
332 | + |
333 | + def put_cache(self, cache): |
334 | + """ |
335 | + Save a dictionary to the JSON cache file |
336 | + """ |
337 | + |
338 | + with open(self.cache_path, 'w') as cache_file: |
339 | + json.dump(cache, cache_file) |
340 | + |
341 | + def set(self, name, value): |
342 | + """ |
343 | + Set a key value pair to the cache |
344 | + """ |
345 | + |
346 | + cache = self.get_cache() |
347 | + cache[name] = value |
348 | + self.put_cache(cache) |
349 | + |
350 | + def get(self, name): |
351 | + """ |
352 | + Retrieve a value from the cache by key |
353 | + """ |
354 | + |
355 | + return self.get_cache().get(name) |
356 | + |
357 | + def wipe(self): |
358 | + """ |
359 | + Remove the cache file |
360 | + """ |
361 | + |
362 | + if os.path.isfile(self.cache_path): |
363 | + os.remove(self.cache_path) |
364 | |
365 | === added file 'mojo-spec-helpers/utils/container_managers.py' |
366 | --- mojo-spec-helpers/utils/container_managers.py 1970-01-01 00:00:00 +0000 |
367 | +++ mojo-spec-helpers/utils/container_managers.py 2015-09-08 08:23:55 +0000 |
368 | @@ -0,0 +1,265 @@ |
369 | +# Modules |
370 | +import requests |
371 | + |
372 | + |
373 | +class LocalEnvironmentSwiftContainer: |
374 | + """ |
375 | + Manage a Swift container for the local deployment environment |
376 | + for storing deployment settings |
377 | + |
378 | + swift_connection: A working swiftclient.client.Connection object |
379 | + container_name: The name of the swift container to use |
380 | + """ |
381 | + |
382 | + previous_build_obj = 'previous-build-label' |
383 | + deployed_build_obj = 'deployed-build-label' |
384 | + deployed_revno_obj = 'deployed-spec-revno' |
385 | + code_upgrade_succeeded_template = 'code-upgrade-{}-{}-succeeded' |
386 | + mojo_run_succeeded_template = 'mojo-run-{}-succeeded' |
387 | + |
388 | + def __init__(self, swift_connection, container_name): |
389 | + self.swift_connection = swift_connection |
390 | + self.container_name = container_name |
391 | + |
392 | + def previous_build_label(self): |
393 | + """ |
394 | + Get the build label that was previously deployed on this environment |
395 | + From the Swift account associated with this environment |
396 | + through environment variables "OS_AUTH_URL" etc. |
397 | + """ |
398 | + |
399 | + return self.swift_connection.get_object( |
400 | + container=self.container_name, |
401 | + obj=self.previous_build_obj |
402 | + )[1].strip() |
403 | + |
404 | + def deployed_build_label(self): |
405 | + """ |
406 | + Get the build label that is currently deployed on this environment |
407 | + From the Swift account associated with this environment |
408 | + through environment variables "OS_AUTH_URL" etc. |
409 | + """ |
410 | + |
411 | + return self.swift_connection.get_object( |
412 | + container=self.container_name, |
413 | + obj=self.deployed_build_obj |
414 | + )[1].strip() |
415 | + |
416 | + def deployed_spec_revno(self): |
417 | + """ |
418 | + Get the mojo spec revision number that is currently deployed |
419 | + on this environment from the Swift account associated with this |
420 | + environment through environment variables "OS_AUTH_URL" etc. |
421 | + """ |
422 | + |
423 | + return self.swift_connection.get_object( |
424 | + container=self.container_name, |
425 | + obj=self.deployed_revno_obj |
426 | + )[1].strip() |
427 | + |
428 | + def save_code_upgrade_succeeded( |
429 | + self, from_build_label, to_build_label |
430 | + ): |
431 | + """ |
432 | + Save an object into Swift |
433 | + to signify that a code-upgrade succeeded |
434 | + from one build_label to another |
435 | + """ |
436 | + |
437 | + upgrade_object_name = self.code_upgrade_succeeded_template.format( |
438 | + from_build_label, to_build_label |
439 | + ) |
440 | + |
441 | + self.swift_connection.put_object( |
442 | + container=self.container_name, |
443 | + obj=upgrade_object_name, |
444 | + contents='true' |
445 | + ) |
446 | + |
447 | + def save_mojo_run_succeeded(self, spec_revno): |
448 | + """ |
449 | + Save an object into Swift |
450 | + to signifiy that a mojo-run succeeded with |
451 | + a given revision number of the mojo spec |
452 | + """ |
453 | + |
454 | + run_object_name = self.mojo_run_succeeded_template.format( |
455 | + spec_revno |
456 | + ) |
457 | + |
458 | + self.swift_connection.put_object( |
459 | + container=self.container_name, |
460 | + obj=run_object_name, |
461 | + contents='true' |
462 | + ) |
463 | + |
464 | + def save_deployed_build_label(self, build_label): |
465 | + """ |
466 | + Save an object into swift containing |
467 | + the build_label which is deployed to this environment |
468 | + """ |
469 | + |
470 | + self.swift_connection.put_object( |
471 | + container=self.container_name, |
472 | + obj='deployed-build-label', |
473 | + contents=build_label |
474 | + ) |
475 | + |
476 | + def save_previous_build_label(self, build_label): |
477 | + """ |
478 | + Save an object into swift |
479 | + to record the previously deployed build_label |
480 | + """ |
481 | + |
482 | + self.swift_connection.put_object( |
483 | + container=self.container_name, |
484 | + obj='previous-build-label', |
485 | + contents=build_label |
486 | + ) |
487 | + |
488 | + def save_mojo_spec_revno(self, spec_revno): |
489 | + """ |
490 | + Save an object into swift containing |
491 | + the current mojo spec revision number |
492 | + """ |
493 | + |
494 | + self.swift_connection.put_object( |
495 | + container=self.container_name, |
496 | + obj='deployed-spec-revno', |
497 | + contents=spec_revno |
498 | + ) |
499 | + |
500 | + |
501 | +class HttpContainer: |
502 | + """ |
503 | + Methods for retrieving objects |
504 | + from a Swift HTTP storage container |
505 | + """ |
506 | + |
507 | + def __init__(self, container_url): |
508 | + """ |
509 | + container_url: The storage URL path for the swift container |
510 | + """ |
511 | + |
512 | + self.container_url = container_url |
513 | + |
514 | + def get(self, object_name): |
515 | + """ |
516 | + Retrieve the contents of an object |
517 | + """ |
518 | + |
519 | + object_url = '{}/{}'.format(self.container_url, object_name) |
520 | + |
521 | + response = requests.get(object_url) |
522 | + |
523 | + try: |
524 | + response.raise_for_status() |
525 | + except requests.exceptions.HTTPError as http_error: |
526 | + http_error.message += '. URL: {}'.format(object_url) |
527 | + http_error.args += ('URL: {}'.format(object_url),) |
528 | + raise http_error |
529 | + |
530 | + response.raise_for_status() |
531 | + |
532 | + return response.text |
533 | + |
534 | + def head(self, object_name): |
535 | + """ |
536 | + Retrieve the HEAD of an object |
537 | + """ |
538 | + |
539 | + object_url = '{}/{}'.format(self.container_url, object_name) |
540 | + |
541 | + return requests.head(object_url) |
542 | + |
543 | + def exists(self, object_name): |
544 | + """ |
545 | + Check an object exists |
546 | + """ |
547 | + |
548 | + return self.head(object_name).ok |
549 | + |
550 | + |
551 | +class CIContainer(HttpContainer): |
552 | + """ |
553 | + Methods for retrieving continuous integration |
554 | + resources from an http swift store |
555 | + """ |
556 | + |
557 | + code_upgrade_succeeded_template = 'code-upgrade-{}-{}-succeeded' |
558 | + mojo_run_succeeded_template = 'mojo-run-{}-succeeded' |
559 | + |
560 | + def has_code_upgrade_been_tested( |
561 | + self, |
562 | + from_build_label, |
563 | + to_build_label |
564 | + ): |
565 | + """ |
566 | + Check if a specific code upgrade has been tested |
567 | + (from one build_label to another) |
568 | + by checking if a specially named Swift object exists |
569 | + in the CI system's Swift HTTP store. |
570 | + (This object will have been created |
571 | + by self.save_code_upgrade_succeeded) |
572 | + """ |
573 | + |
574 | + return self.exists( |
575 | + self.code_upgrade_succeeded_template.format( |
576 | + from_build_label, |
577 | + to_build_label |
578 | + ) |
579 | + ) |
580 | + |
581 | + def has_mojo_run_been_tested(self, spec_revno): |
582 | + """ |
583 | + Check if a specific mojo spec revision number has been tested |
584 | + by checking if a specially named Swift object exists |
585 | + in the CI system's Swift HTTP store. |
586 | + (This object will have been created by self.save_mojo_run_succeeded) |
587 | + """ |
588 | + |
589 | + return self.exists( |
590 | + self.mojo_run_succeeded_template.format( |
591 | + spec_revno |
592 | + ) |
593 | + ) |
594 | + |
595 | + |
596 | +class BuildContainer(HttpContainer): |
597 | + """ |
598 | + Methods for interacting with the HTTP Swift store |
599 | + containing code builds |
600 | + """ |
601 | + |
602 | + def latest_build_label(self): |
603 | + """ |
604 | + Get the latest build label from the webteam's Swift HTTP object store |
605 | + """ |
606 | + |
607 | + return self.get('latest-build-label').strip() |
608 | + |
609 | + |
610 | +class DeployedEnvironmentContainer(HttpContainer): |
611 | + """ |
612 | + Methods for requesting information |
613 | + from the HTTP swift store for a |
614 | + depoyed environment (e.g. production, staging) |
615 | + """ |
616 | + |
617 | + def deployed_build_label(self): |
618 | + """ |
619 | + Get the build label which was most recently |
620 | + deployed to this environment |
621 | + from their Swift HTTP object store |
622 | + """ |
623 | + |
624 | + return self.get('deployed-build-label').strip() |
625 | + |
626 | + def deployed_spec_revno(self): |
627 | + """ |
628 | + Get the version of the mojo spec |
629 | + which was most recently run on this environment |
630 | + from their Swift HTTP object store |
631 | + """ |
632 | + |
633 | + return self.get('deployed-spec-revno').strip() |
634 | |
635 | === added file 'mojo-spec-helpers/utils/mojo_os_utils.py' |
636 | --- mojo-spec-helpers/utils/mojo_os_utils.py 1970-01-01 00:00:00 +0000 |
637 | +++ mojo-spec-helpers/utils/mojo_os_utils.py 2015-09-08 08:23:55 +0000 |
638 | @@ -0,0 +1,554 @@ |
639 | +#!/usr/bin/python |
640 | + |
641 | +import swiftclient |
642 | +import glanceclient |
643 | +from keystoneclient.v2_0 import client as keystoneclient |
644 | +import mojo_utils |
645 | +from novaclient.v1_1 import client as novaclient |
646 | +from neutronclient.v2_0 import client as neutronclient |
647 | +import logging |
648 | +import re |
649 | +import sys |
650 | +import tempfile |
651 | +import urllib |
652 | +import os |
653 | +import time |
654 | +import subprocess |
655 | +import paramiko |
656 | +import StringIO |
657 | + |
658 | + |
659 | +# Openstack Client helpers |
660 | +def get_nova_creds(cloud_creds): |
661 | + auth = { |
662 | + 'username': cloud_creds['OS_USERNAME'], |
663 | + 'api_key': cloud_creds['OS_PASSWORD'], |
664 | + 'auth_url': cloud_creds['OS_AUTH_URL'], |
665 | + 'project_id': cloud_creds['OS_TENANT_NAME'], |
666 | + 'region_name': cloud_creds['OS_REGION_NAME'], |
667 | + } |
668 | + return auth |
669 | + |
670 | + |
671 | +def get_ks_creds(cloud_creds): |
672 | + auth = { |
673 | + 'username': cloud_creds['OS_USERNAME'], |
674 | + 'password': cloud_creds['OS_PASSWORD'], |
675 | + 'auth_url': cloud_creds['OS_AUTH_URL'], |
676 | + 'tenant_name': cloud_creds['OS_TENANT_NAME'], |
677 | + 'region_name': cloud_creds['OS_REGION_NAME'], |
678 | + } |
679 | + return auth |
680 | + |
681 | + |
682 | +def get_swift_creds(cloud_creds): |
683 | + auth = { |
684 | + 'user': cloud_creds['OS_USERNAME'], |
685 | + 'key': cloud_creds['OS_PASSWORD'], |
686 | + 'authurl': cloud_creds['OS_AUTH_URL'], |
687 | + 'tenant_name': cloud_creds['OS_TENANT_NAME'], |
688 | + 'auth_version': '2.0', |
689 | + } |
690 | + return auth |
691 | + |
692 | + |
693 | +def get_nova_client(novarc_creds): |
694 | + nova_creds = get_nova_creds(novarc_creds) |
695 | + return novaclient.Client(**nova_creds) |
696 | + |
697 | + |
698 | +def get_neutron_client(novarc_creds): |
699 | + neutron_creds = get_ks_creds(novarc_creds) |
700 | + return neutronclient.Client(**neutron_creds) |
701 | + |
702 | + |
703 | +def get_keystone_client(novarc_creds): |
704 | + keystone_creds = get_ks_creds(novarc_creds) |
705 | + keystone_creds['insecure'] = True |
706 | + return keystoneclient.Client(**keystone_creds) |
707 | + |
708 | + |
709 | +def get_swift_client(novarc_creds, insecure=True): |
710 | + swift_creds = get_swift_creds(novarc_creds) |
711 | + swift_creds['insecure'] = insecure |
712 | + return swiftclient.client.Connection(**swift_creds) |
713 | + |
714 | + |
715 | +def get_glance_client(novarc_creds): |
716 | + kc = get_keystone_client(novarc_creds) |
717 | + glance_endpoint = kc.service_catalog.url_for(service_type='image', |
718 | + endpoint_type='publicURL') |
719 | + return glanceclient.Client('1', glance_endpoint, token=kc.auth_token) |
720 | + |
721 | + |
722 | +# Glance Helpers |
723 | +def download_image(image, image_glance_name=None): |
724 | + logging.info('Downloading ' + image) |
725 | + tmp_dir = tempfile.mkdtemp(dir='/tmp') |
726 | + if not image_glance_name: |
727 | + image_glance_name = image.split('/')[-1] |
728 | + local_file = os.path.join(tmp_dir, image_glance_name) |
729 | + urllib.urlretrieve(image, local_file) |
730 | + return local_file |
731 | + |
732 | + |
733 | +def upload_image(gclient, ifile, image_name, public, disk_format, |
734 | + container_format): |
735 | + logging.info('Uploading %s to glance ' % (image_name)) |
736 | + with open(ifile) as fimage: |
737 | + gclient.images.create( |
738 | + name=image_name, |
739 | + is_public=public, |
740 | + disk_format=disk_format, |
741 | + container_format=container_format, |
742 | + data=fimage) |
743 | + |
744 | + |
745 | +def get_images_list(gclient): |
746 | + return [image.name for image in gclient.images.list()] |
747 | + |
748 | + |
749 | +# Keystone helpers |
750 | +def tenant_create(kclient, tenants): |
751 | + current_tenants = [tenant.name for tenant in kclient.tenants.list()] |
752 | + for tenant in tenants: |
753 | + if tenant in current_tenants: |
754 | + logging.warning('Not creating tenant %s it already' |
755 | + 'exists' % (tenant)) |
756 | + else: |
757 | + logging.info('Creating tenant %s' % (tenant)) |
758 | + kclient.tenants.create(tenant_name=tenant) |
759 | + |
760 | + |
761 | +def user_create(kclient, users): |
762 | + current_users = [user.name for user in kclient.users.list()] |
763 | + for user in users: |
764 | + if user['username'] in current_users: |
765 | + logging.warning('Not creating user %s it already' |
766 | + 'exists' % (user['username'])) |
767 | + else: |
768 | + logging.info('Creating user %s' % (user['username'])) |
769 | + tenant_id = get_tenant_id(kclient, user['tenant']) |
770 | + kclient.users.create(name=user['username'], |
771 | + password=user['password'], |
772 | + email=user['email'], |
773 | + tenant_id=tenant_id) |
774 | + |
775 | + |
776 | +def get_roles_for_user(kclient, user_id, tenant_id): |
777 | + roles = [] |
778 | + ksuser_roles = kclient.roles.roles_for_user(user_id, tenant_id) |
779 | + for role in ksuser_roles: |
780 | + roles.append(role.id) |
781 | + return roles |
782 | + |
783 | + |
784 | +def add_users_to_roles(kclient, users): |
785 | + for user_details in users: |
786 | + tenant_id = get_tenant_id(kclient, user_details['tenant']) |
787 | + for role_name in user_details['roles']: |
788 | + role = kclient.roles.find(name=role_name) |
789 | + user = kclient.users.find(name=user_details['username']) |
790 | + users_roles = get_roles_for_user(kclient, user, tenant_id) |
791 | + if role.id in users_roles: |
792 | + logging.warning('Not adding role %s to %s it already has ' |
793 | + 'it' % (user_details['username'], role_name)) |
794 | + else: |
795 | + logging.info('Adding %s to role %s for tenant' |
796 | + '%s' % (user_details['username'], role_name, |
797 | + tenant_id)) |
798 | + kclient.roles.add_user_role(user_details['username'], role, |
799 | + tenant_id) |
800 | + |
801 | + |
802 | +def get_tenant_id(ks_client, tenant_name): |
803 | + for t in ks_client.tenants.list(): |
804 | + if t._info['name'] == tenant_name: |
805 | + return t._info['id'] |
806 | + return None |
807 | + |
808 | + |
809 | +# Neutron Helpers |
810 | +def get_gateway_uuids(): |
811 | + gateway_config = mojo_utils.get_juju_status('neutron-gateway') |
812 | + uuids = [] |
813 | + for machine in gateway_config['machines']: |
814 | + uuids.append(gateway_config['machines'][machine]['instance-id']) |
815 | + return uuids |
816 | + |
817 | + |
818 | +def get_net_uuid(neutron_client, net_name): |
819 | + network = neutron_client.list_networks(name=net_name)['networks'][0] |
820 | + return network['id'] |
821 | + |
822 | + |
823 | +def configure_gateway_ext_port(novaclient): |
824 | + uuids = get_gateway_uuids() |
825 | + for uuid in uuids: |
826 | + server = novaclient.servers.get(uuid) |
827 | + mac_addrs = [a.mac_addr for a in server.interface_list()] |
828 | + if len(mac_addrs) < 2: |
829 | + logging.info('Adding additional port to Neutron Gateway') |
830 | + server.interface_attach(port_id=None, net_id=None, fixed_ip=None) |
831 | + else: |
832 | + logging.warning('Neutron Gateway already has additional port') |
833 | + if uuids: |
834 | + logging.info('Seting Neutron Gateway external port to eth1') |
835 | + mojo_utils.juju_set('neutron-gateway', 'ext-port=eth1') |
836 | + |
837 | + |
838 | +def create_tenant_network(neutron_client, tenant_id, net_name='private', |
839 | + shared=False, network_type='gre'): |
840 | + networks = neutron_client.list_networks(name=net_name) |
841 | + if len(networks['networks']) == 0: |
842 | + logging.info('Creating network: %s', |
843 | + net_name) |
844 | + network_msg = { |
845 | + 'network': { |
846 | + 'name': net_name, |
847 | + 'shared': shared, |
848 | + 'tenant_id': tenant_id, |
849 | + } |
850 | + } |
851 | + if network_type == 'vxlan': |
852 | + network_msg['network']['provider:segmentation_id'] = 1233 |
853 | + network_msg['network']['provider:network_type'] = network_type |
854 | + network = neutron_client.create_network(network_msg)['network'] |
855 | + else: |
856 | + logging.warning('Network %s already exists.', net_name) |
857 | + network = networks['networks'][0] |
858 | + return network |
859 | + |
860 | + |
861 | +def create_external_network(neutron_client, tenant_id, net_name='ext_net', |
862 | + network_type='gre'): |
863 | + networks = neutron_client.list_networks(name=net_name) |
864 | + if len(networks['networks']) == 0: |
865 | + logging.info('Configuring external bridge') |
866 | + network_msg = { |
867 | + 'name': net_name, |
868 | + 'router:external': True, |
869 | + 'tenant_id': tenant_id, |
870 | + } |
871 | + if network_type == 'vxlan': |
872 | + network_msg['provider:segmentation_id'] = 1234 |
873 | + network_msg['provider:network_type'] = network_type |
874 | + |
875 | + logging.info('Creating new external network definition: %s', |
876 | + net_name) |
877 | + network = neutron_client.create_network( |
878 | + {'network': network_msg})['network'] |
879 | + logging.info('New external network created: %s', network['id']) |
880 | + else: |
881 | + logging.warning('Network %s already exists.', net_name) |
882 | + network = networks['networks'][0] |
883 | + return network |
884 | + |
885 | + |
886 | +def create_tenant_subnet(neutron_client, tenant_id, network, cidr, dhcp=True, |
887 | + subnet_name='private_subnet'): |
888 | + # Create subnet |
889 | + subnets = neutron_client.list_subnets(name=subnet_name) |
890 | + if len(subnets['subnets']) == 0: |
891 | + logging.info('Creating subnet') |
892 | + subnet_msg = { |
893 | + 'subnet': { |
894 | + 'name': subnet_name, |
895 | + 'network_id': network['id'], |
896 | + 'enable_dhcp': dhcp, |
897 | + 'cidr': cidr, |
898 | + 'ip_version': 4, |
899 | + 'tenant_id': tenant_id |
900 | + } |
901 | + } |
902 | + subnet = neutron_client.create_subnet(subnet_msg)['subnet'] |
903 | + else: |
904 | + logging.warning('Subnet %s already exists.', subnet_name) |
905 | + subnet = subnets['subnets'][0] |
906 | + return subnet |
907 | + |
908 | + |
909 | +def create_external_subnet(neutron_client, tenant_id, network, |
910 | + default_gateway=None, cidr=None, |
911 | + start_floating_ip=None, end_floating_ip=None, |
912 | + subnet_name='ext_net_subnet'): |
913 | + subnets = neutron_client.list_subnets(name=subnet_name) |
914 | + if len(subnets['subnets']) == 0: |
915 | + subnet_msg = { |
916 | + 'name': subnet_name, |
917 | + 'network_id': network['id'], |
918 | + 'enable_dhcp': False, |
919 | + 'ip_version': 4, |
920 | + 'tenant_id': tenant_id |
921 | + } |
922 | + |
923 | + if default_gateway: |
924 | + subnet_msg['gateway_ip'] = default_gateway |
925 | + if cidr: |
926 | + subnet_msg['cidr'] = cidr |
927 | + if (start_floating_ip and end_floating_ip): |
928 | + allocation_pool = { |
929 | + 'start': start_floating_ip, |
930 | + 'end': end_floating_ip, |
931 | + } |
932 | + subnet_msg['allocation_pools'] = [allocation_pool] |
933 | + |
934 | + logging.info('Creating new subnet') |
935 | + subnet = neutron_client.create_subnet({'subnet': subnet_msg})['subnet'] |
936 | + logging.info('New subnet created: %s', subnet['id']) |
937 | + else: |
938 | + logging.warning('Subnet %s already exists.', subnet_name) |
939 | + subnet = subnets['subnets'][0] |
940 | + return subnet |
941 | + |
942 | + |
943 | +def update_subnet_dns(neutron_client, subnet, dns_servers): |
944 | + msg = { |
945 | + 'subnet': { |
946 | + 'dns_nameservers': dns_servers.split(',') |
947 | + } |
948 | + } |
949 | + logging.info('Updating dns_nameservers (%s) for subnet', |
950 | + dns_servers) |
951 | + neutron_client.update_subnet(subnet['id'], msg) |
952 | + |
953 | + |
954 | +def create_provider_router(neutron_client, tenant_id): |
955 | + routers = neutron_client.list_routers(name='provider-router') |
956 | + if len(routers['routers']) == 0: |
957 | + logging.info('Creating provider router for external network access') |
958 | + router_info = { |
959 | + 'router': { |
960 | + 'name': 'provider-router', |
961 | + 'tenant_id': tenant_id |
962 | + } |
963 | + } |
964 | + router = neutron_client.create_router(router_info)['router'] |
965 | + logging.info('New router created: %s', (router['id'])) |
966 | + else: |
967 | + logging.warning('Router provider-router already exists.') |
968 | + router = routers['routers'][0] |
969 | + return router |
970 | + |
971 | + |
972 | +def plug_extnet_into_router(neutron_client, router, network): |
973 | + ports = neutron_client.list_ports(device_owner='network:router_gateway', |
974 | + network_id=network['id']) |
975 | + if len(ports['ports']) == 0: |
976 | + logging.info('Plugging router into ext_net') |
977 | + router = neutron_client.add_gateway_router( |
978 | + router=router['id'], |
979 | + body={'network_id': network['id']}) |
980 | + logging.info('Router connected') |
981 | + else: |
982 | + logging.warning('Router already connected') |
983 | + |
984 | + |
985 | +def plug_subnet_into_router(neutron_client, router, network, subnet): |
986 | + routers = neutron_client.list_routers(name=router) |
987 | + if len(routers['routers']) == 0: |
988 | + logging.error('Unable to locate provider router %s', router) |
989 | + sys.exit(1) |
990 | + else: |
991 | + # Check to see if subnet already plugged into router |
992 | + ports = neutron_client.list_ports( |
993 | + device_owner='network:router_interface', |
994 | + network_id=network['id']) |
995 | + if len(ports['ports']) == 0: |
996 | + logging.info('Adding interface from subnet to %s' % (router)) |
997 | + router = routers['routers'][0] |
998 | + neutron_client.add_interface_router(router['id'], |
999 | + {'subnet_id': subnet['id']}) |
1000 | + else: |
1001 | + logging.warning('Router already connected to subnet') |
1002 | + |
1003 | + |
1004 | +# Nova Helpers |
1005 | +def create_keypair(nova_client, keypair_name): |
1006 | + if nova_client.keypairs.findall(name=keypair_name): |
1007 | + _oldkey = nova_client.keypairs.find(name=keypair_name) |
1008 | + logging.info('Deleting key %s' % (keypair_name)) |
1009 | + nova_client.keypairs.delete(_oldkey) |
1010 | + logging.info('Creating key %s' % (keypair_name)) |
1011 | + new_key = nova_client.keypairs.create(name=keypair_name) |
1012 | + return new_key.private_key |
1013 | + |
1014 | + |
1015 | +def boot_instance(nova_client, image_name, flavor_name, key_name): |
1016 | + image = nova_client.images.find(name=image_name) |
1017 | + flavor = nova_client.flavors.find(name=flavor_name) |
1018 | + net = nova_client.networks.find(label="private") |
1019 | + nics = [{'net-id': net.id}] |
1020 | + # Obviously time may not produce a unique name |
1021 | + vm_name = time.strftime("%Y%m%d%H%M%S") |
1022 | + logging.info('Creating %s %s ' |
1023 | + 'instance %s' % (flavor_name, image_name, vm_name)) |
1024 | + instance = nova_client.servers.create(name=vm_name, |
1025 | + image=image, |
1026 | + flavor=flavor, |
1027 | + key_name=key_name, |
1028 | + nics=nics) |
1029 | + return instance |
1030 | + |
1031 | + |
1032 | +def wait_for_active(nova_client, vm_name, wait_time): |
1033 | + logging.info('Waiting %is for %s to reach ACTIVE ' |
1034 | + 'state' % (wait_time, vm_name)) |
1035 | + for counter in range(wait_time): |
1036 | + instance = nova_client.servers.find(name=vm_name) |
1037 | + if instance.status == 'ACTIVE': |
1038 | + logging.info('%s is ACTIVE' % (vm_name)) |
1039 | + return True |
1040 | + elif instance.status != 'BUILD': |
1041 | + logging.error('instance %s in unknown ' |
1042 | + 'state %s' % (instance.name, instance.status)) |
1043 | + return False |
1044 | + time.sleep(1) |
1045 | + logging.error('instance %s failed to reach ' |
1046 | + 'active state in %is' % (instance.name, wait_time)) |
1047 | + return False |
1048 | + |
1049 | + |
1050 | +def wait_for_cloudinit(nova_client, vm_name, bootstring, wait_time): |
1051 | + logging.info('Waiting %is for cloudinit on %s to ' |
1052 | + 'complete' % (wait_time, vm_name)) |
1053 | + instance = nova_client.servers.find(name=vm_name) |
1054 | + for counter in range(wait_time): |
1055 | + instance = nova_client.servers.find(name=vm_name) |
1056 | + console_log = instance.get_console_output() |
1057 | + if bootstring in console_log: |
1058 | + logging.info('Cloudinit for %s is complete' % (vm_name)) |
1059 | + return True |
1060 | + time.sleep(1) |
1061 | + logging.error('cloudinit for instance %s failed ' |
1062 | + 'to complete in %is' % (instance.name, wait_time)) |
1063 | + return False |
1064 | + |
1065 | + |
1066 | +def wait_for_boot(nova_client, vm_name, bootstring, active_wait, |
1067 | + cloudinit_wait): |
1068 | + if not wait_for_active(nova_client, vm_name, active_wait): |
1069 | + raise Exception('Error initialising %s' % vm_name) |
1070 | + if not wait_for_cloudinit(nova_client, vm_name, bootstring, |
1071 | + cloudinit_wait): |
1072 | + raise Exception('Cloudinit error %s' % vm_name) |
1073 | + |
1074 | + |
1075 | +def wait_for_ping(ip, wait_time): |
1076 | + logging.info('Waiting for ping to %s' % (ip)) |
1077 | + for counter in range(wait_time): |
1078 | + if ping(ip): |
1079 | + logging.info('Ping %s success' % (ip)) |
1080 | + return True |
1081 | + time.sleep(1) |
1082 | + logging.error('Ping failed for %s' % (ip)) |
1083 | + return False |
1084 | + |
1085 | + |
1086 | +def assign_floating_ip(nova_client, vm_name): |
1087 | + floating_ip = nova_client.floating_ips.create() |
1088 | + logging.info('Assigning floating IP %s to %s' % (floating_ip.ip, vm_name)) |
1089 | + instance = nova_client.servers.find(name=vm_name) |
1090 | + instance.add_floating_ip(floating_ip) |
1091 | + return floating_ip.ip |
1092 | + |
1093 | + |
1094 | +def add_secgroup_rules(nova_client): |
1095 | + secgroup = nova_client.security_groups.find(name="default") |
1096 | + # Using presence of a 22 rule to indicate whether secgroup rules |
1097 | + # have been added |
1098 | + port_rules = [rule['to_port'] for rule in secgroup.rules] |
1099 | + if 22 in port_rules: |
1100 | + logging.warn('Security group rules for ssh already added') |
1101 | + else: |
1102 | + logging.info('Adding ssh security group rule') |
1103 | + nova_client.security_group_rules.create(secgroup.id, |
1104 | + ip_protocol="tcp", |
1105 | + from_port=22, |
1106 | + to_port=22) |
1107 | + if -1 in port_rules: |
1108 | + logging.warn('Security group rules for ping already added') |
1109 | + else: |
1110 | + logging.info('Adding ping security group rule') |
1111 | + nova_client.security_group_rules.create(secgroup.id, |
1112 | + ip_protocol="icmp", |
1113 | + from_port=-1, |
1114 | + to_port=-1) |
1115 | + |
1116 | + |
1117 | +def ping(ip): |
1118 | + # Use the system ping command with count of 1 and wait time of 1. |
1119 | + ret = subprocess.call(['ping', '-c', '1', '-W', '1', ip], |
1120 | + stdout=open('/dev/null', 'w'), |
1121 | + stderr=open('/dev/null', 'w')) |
1122 | + return ret == 0 |
1123 | + |
1124 | + |
1125 | +def ssh_test(username, ip, vm_name, password=None, privkey=None): |
1126 | + logging.info('Attempting to ssh to %s(%s)' % (vm_name, ip)) |
1127 | + ssh = paramiko.SSHClient() |
1128 | + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
1129 | + if privkey: |
1130 | + key = paramiko.RSAKey.from_private_key(StringIO.StringIO(privkey)) |
1131 | + ssh.connect(ip, username=username, password='', pkey=key) |
1132 | + else: |
1133 | + ssh.connect(ip, username=username, password=password) |
1134 | + stdin, stdout, stderr = ssh.exec_command('uname -n') |
1135 | + return_string = stdout.readlines()[0].strip() |
1136 | + ssh.close() |
1137 | + if return_string == vm_name: |
1138 | + logging.info('SSH to %s(%s) succesfull' % (vm_name, ip)) |
1139 | + return True |
1140 | + else: |
1141 | + logging.info('SSH to %s(%s) failed' % (vm_name, ip)) |
1142 | + return False |
1143 | + |
1144 | + |
1145 | +def boot_and_test(nova_client, image_name, flavor_name, number, privkey, |
1146 | + active_wait=180, cloudinit_wait=180, ping_wait=180): |
1147 | + image_config = mojo_utils.get_mojo_config('images.yaml') |
1148 | + for counter in range(number): |
1149 | + instance = boot_instance(nova_client, |
1150 | + image_name=image_name, |
1151 | + flavor_name=flavor_name, |
1152 | + key_name='mojo') |
1153 | + wait_for_boot(nova_client, instance.name, |
1154 | + image_config[image_name]['bootstring'], active_wait, |
1155 | + cloudinit_wait) |
1156 | + ip = assign_floating_ip(nova_client, instance.name) |
1157 | + wait_for_ping(ip, ping_wait) |
1158 | + if not wait_for_ping(ip, ping_wait): |
1159 | + raise Exception('Ping of %s failed' % (ip)) |
1160 | + ssh_test_args = { |
1161 | + 'username': image_config[image_name]['username'], |
1162 | + 'ip': ip, |
1163 | + 'vm_name': instance.name, |
1164 | + } |
1165 | + if image_config[image_name]['auth_type'] == 'password': |
1166 | + ssh_test_args['password'] = image_config[image_name]['password'] |
1167 | + elif image_config[image_name]['auth_type'] == 'privkey': |
1168 | + ssh_test_args['privkey'] = privkey |
1169 | + if not ssh_test(**ssh_test_args): |
1170 | + raise Exception('SSH failed' % (ip)) |
1171 | + |
1172 | + |
1173 | +# Hacluster helper |
1174 | +def get_crm_leader(service, resource=None): |
1175 | + if not resource: |
1176 | + resource = 'res_.*_vip' |
1177 | + leader = set() |
1178 | + for unit in mojo_utils.get_juju_units(service=service): |
1179 | + crm_out = mojo_utils.remote_run(unit, 'sudo crm status')[0] |
1180 | + for line in crm_out.splitlines(): |
1181 | + line = line.lstrip() |
1182 | + if re.match(resource, line): |
1183 | + leader.add(line.split()[-1]) |
1184 | + if len(leader) != 1: |
1185 | + raise Exception('Unexpected leader count: ' + str(len(leader))) |
1186 | + return leader.pop().split('-')[-1] |
1187 | + |
1188 | + |
1189 | +def delete_crm_leader(service, resource=None): |
1190 | + mach_no = get_crm_leader(service, resource) |
1191 | + unit = mojo_utils.convert_machineno_to_unit(mach_no) |
1192 | + mojo_utils.delete_unit(unit) |
1193 | |
1194 | === added file 'mojo-spec-helpers/utils/mojo_utils.py' |
1195 | --- mojo-spec-helpers/utils/mojo_utils.py 1970-01-01 00:00:00 +0000 |
1196 | +++ mojo-spec-helpers/utils/mojo_utils.py 2015-09-08 08:23:55 +0000 |
1197 | @@ -0,0 +1,356 @@ |
1198 | +#!/usr/bin/python |
1199 | + |
1200 | +import subprocess |
1201 | +import yaml |
1202 | +import os |
1203 | +import mojo |
1204 | +import logging |
1205 | +import time |
1206 | +from collections import Counter |
1207 | +from swiftclient.client import Connection |
1208 | + |
1209 | +JUJU_STATUSES = { |
1210 | + 'good': ['ACTIVE', 'started'], |
1211 | + 'bad': ['error'], |
1212 | + 'transitional': ['pending', 'pending', 'down', 'installed', 'stopped'], |
1213 | +} |
1214 | + |
1215 | + |
1216 | +def get_juju_status(service=None): |
1217 | + cmd = ['juju', 'status'] |
1218 | + if service: |
1219 | + cmd.append(service) |
1220 | + status_file = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout |
1221 | + return yaml.load(status_file) |
1222 | + |
1223 | + |
1224 | +def get_juju_units(juju_status=None, service=None): |
1225 | + if not juju_status: |
1226 | + juju_status = get_juju_status() |
1227 | + units = [] |
1228 | + if service: |
1229 | + services = [service] |
1230 | + else: |
1231 | + services = [juju_service for juju_service in juju_status['services']] |
1232 | + for svc in services: |
1233 | + if 'units' in juju_status['services'][svc]: |
1234 | + for unit in juju_status['services'][svc]['units']: |
1235 | + units.append(unit) |
1236 | + return units |
1237 | + |
1238 | + |
1239 | +def convert_machineno_to_unit(machineno, juju_status=None): |
1240 | + if not juju_status: |
1241 | + juju_status = get_juju_status() |
1242 | + services = [service for service in juju_status['services']] |
1243 | + for svc in services: |
1244 | + if 'units' in juju_status['services'][svc]: |
1245 | + for unit in juju_status['services'][svc]['units']: |
1246 | + unit_info = juju_status['services'][svc]['units'][unit] |
1247 | + if unit_info['machine'] == machineno: |
1248 | + return unit |
1249 | + |
1250 | + |
1251 | +def remote_shell_check(unit): |
1252 | + cmd = ['juju', 'run', '--unit', unit, 'uname -a'] |
1253 | + FNULL = open(os.devnull, 'w') |
1254 | + return not subprocess.call(cmd, stdout=FNULL, stderr=subprocess.STDOUT) |
1255 | + |
1256 | + |
1257 | +def remote_run(unit, remote_cmd=None): |
1258 | + cmd = ['juju', 'run', '--unit', unit] |
1259 | + if remote_cmd: |
1260 | + cmd.append(remote_cmd) |
1261 | + else: |
1262 | + cmd.append('uname -a') |
1263 | + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) |
1264 | + output = p.communicate() |
1265 | + if p.returncode != 0: |
1266 | + raise Exception('Error running nagios checks') |
1267 | + return output |
1268 | + |
1269 | + |
1270 | +def remote_upload(unit, script, remote_dir=None): |
1271 | + if remote_dir: |
1272 | + dst = unit + ':' + remote_dir |
1273 | + else: |
1274 | + dst = unit + ':/tmp/' |
1275 | + cmd = ['juju', 'scp', script, dst] |
1276 | + return subprocess.check_call(cmd) |
1277 | + |
1278 | + |
1279 | +def delete_unit(unit): |
1280 | + service = unit.split('/')[0] |
1281 | + unit_count = len(get_juju_units(service=service)) |
1282 | + logging.info('Removing unit ' + unit) |
1283 | + cmd = ['juju', 'destroy-unit', unit] |
1284 | + subprocess.check_call(cmd) |
1285 | + target_num = unit_count - 1 |
1286 | + # Wait for the unit to disappear from juju status |
1287 | + while len(get_juju_units(service=service)) > target_num: |
1288 | + time.sleep(5) |
1289 | + juju_wait_finished() |
1290 | + |
1291 | + |
1292 | +def add_unit(service, unit_num=None): |
1293 | + unit_count = len(get_juju_units(service=service)) |
1294 | + if unit_num: |
1295 | + additional_units = int(unit_num) |
1296 | + else: |
1297 | + additional_units = 1 |
1298 | + logging.info('Adding %i unit(s) to %s' % (additional_units, service)) |
1299 | + cmd = ['juju', 'add-unit', service, '-n', str(additional_units)] |
1300 | + subprocess.check_call(cmd) |
1301 | + target_num = unit_count + additional_units |
1302 | + # Wait for the new unit to appear in juju status |
1303 | + while len(get_juju_units(service=service)) < target_num: |
1304 | + time.sleep(5) |
1305 | + juju_wait_finished() |
1306 | + |
1307 | + |
1308 | +def juju_set(service, option): |
1309 | + subprocess.check_call(['juju', 'set', service, option]) |
1310 | + juju_wait_finished() |
1311 | + |
1312 | + |
1313 | +def juju_set_config_option(service, option_name, value): |
1314 | + option = "{}={}".format(option_name, value) |
1315 | + juju_set(service, option) |
1316 | + |
1317 | + |
1318 | +def juju_get(service, option): |
1319 | + cmd = ['juju', 'get', service] |
1320 | + juju_get_output = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout |
1321 | + service_config = yaml.load(juju_get_output) |
1322 | + if 'value' in service_config['settings'][option]: |
1323 | + return service_config['settings'][option]['value'] |
1324 | + |
1325 | + |
1326 | +def get_undercload_auth(): |
1327 | + juju_env = subprocess.check_output(['juju', 'switch']).strip('\n') |
1328 | + juju_env_file = open(os.environ['HOME'] + "/.juju/environments.yaml", 'r') |
1329 | + juju_env_contents = yaml.load(juju_env_file) |
1330 | + novarc_settings = juju_env_contents['environments'][juju_env] |
1331 | + auth_settings = { |
1332 | + 'OS_AUTH_URL': novarc_settings['auth-url'], |
1333 | + 'OS_TENANT_NAME': novarc_settings['tenant-name'], |
1334 | + 'OS_USERNAME': novarc_settings['username'], |
1335 | + 'OS_PASSWORD': novarc_settings['password'], |
1336 | + 'OS_REGION_NAME': novarc_settings['region'], |
1337 | + } |
1338 | + return auth_settings |
1339 | + |
1340 | + |
1341 | +# Openstack Client helpers |
1342 | +def get_auth_url(juju_status=None): |
1343 | + if juju_get('keystone', 'vip'): |
1344 | + return juju_get('keystone', 'vip') |
1345 | + if not juju_status: |
1346 | + juju_status = get_juju_status() |
1347 | + unit = juju_status['services']['keystone']['units'].itervalues().next() |
1348 | + return unit['public-address'] |
1349 | + |
1350 | + |
1351 | +def get_overcloud_auth(juju_status=None): |
1352 | + if not juju_status: |
1353 | + juju_status = get_juju_status() |
1354 | + if juju_get('keystone', 'use-https').lower() == 'yes': |
1355 | + transport = 'https' |
1356 | + port = 35357 |
1357 | + else: |
1358 | + transport = 'http' |
1359 | + port = 5000 |
1360 | + address = get_auth_url() |
1361 | + auth_settings = { |
1362 | + 'OS_AUTH_URL': '%s://%s:%i/v2.0' % (transport, address, port), |
1363 | + 'OS_TENANT_NAME': 'admin', |
1364 | + 'OS_USERNAME': 'admin', |
1365 | + 'OS_PASSWORD': 'openstack', |
1366 | + 'OS_REGION_NAME': 'RegionOne', |
1367 | + } |
1368 | + return auth_settings |
1369 | + |
1370 | + |
1371 | +def get_mojo_file(filename): |
1372 | + spec = mojo.Spec(os.environ['MOJO_SPEC_DIR']) |
1373 | + return spec.get_config(filename, stage=os.environ['MOJO_STAGE']) |
1374 | + |
1375 | + |
1376 | +def get_mojo_spec_revno(): |
1377 | + """ |
1378 | + Get the current revision number of the mojo spec |
1379 | + """ |
1380 | + |
1381 | + revno_command = 'bzr revno {}'.format(os.environ['MOJO_SPEC_DIR']) |
1382 | + return subprocess.check_output(revno_command.split()).strip() |
1383 | + |
1384 | + |
1385 | +def get_mojo_config(filename): |
1386 | + config_file = get_mojo_file(filename) |
1387 | + logging.info('Using config %s' % (config_file)) |
1388 | + return yaml.load(file(config_file, 'r')) |
1389 | + |
1390 | + |
1391 | +def get_charm_dir(): |
1392 | + return os.path.join(os.environ['MOJO_REPO_DIR'], |
1393 | + os.environ['MOJO_SERIES']) |
1394 | + |
1395 | + |
1396 | +def sync_charmhelpers(charmdir): |
1397 | + p = subprocess.Popen(['make', 'sync'], cwd=charmdir) |
1398 | + p.communicate() |
1399 | + |
1400 | + |
1401 | +def sync_all_charmhelpers(): |
1402 | + charm_base_dir = get_charm_dir() |
1403 | + for direc in os.listdir(charm_base_dir): |
1404 | + charm_dir = os.path.join(charm_base_dir, direc) |
1405 | + if os.path.isdir(charm_dir): |
1406 | + sync_charmhelpers(charm_dir) |
1407 | + |
1408 | + |
1409 | +def parse_mojo_arg(options, mojoarg, multiargs=False): |
1410 | + if mojoarg.upper() in os.environ: |
1411 | + if multiargs: |
1412 | + return os.environ[mojoarg.upper()].split() |
1413 | + else: |
1414 | + return os.environ[mojoarg.upper()] |
1415 | + else: |
1416 | + return getattr(options, mojoarg) |
1417 | + |
1418 | + |
1419 | +def get_machine_state(juju_status, state_type): |
1420 | + states = Counter() |
1421 | + for machine_no in juju_status['machines']: |
1422 | + if state_type in juju_status['machines'][machine_no]: |
1423 | + state = juju_status['machines'][machine_no][state_type] |
1424 | + else: |
1425 | + state = 'unknown' |
1426 | + states[state] += 1 |
1427 | + return states |
1428 | + |
1429 | + |
1430 | +def get_machine_agent_states(juju_status): |
1431 | + return get_machine_state(juju_status, 'agent-state') |
1432 | + |
1433 | + |
1434 | +def get_machine_instance_states(juju_status): |
1435 | + return get_machine_state(juju_status, 'instance-state') |
1436 | + |
1437 | + |
1438 | +def get_service_agent_states(juju_status): |
1439 | + service_state = Counter() |
1440 | + for service in juju_status['services']: |
1441 | + if 'units' in juju_status['services'][service]: |
1442 | + for unit in juju_status['services'][service]['units']: |
1443 | + unit_info = juju_status['services'][service]['units'][unit] |
1444 | + service_state[unit_info['agent-state']] += 1 |
1445 | + if 'subordinates' in unit_info: |
1446 | + for sub_unit in unit_info['subordinates']: |
1447 | + sub_sstate = \ |
1448 | + unit_info['subordinates'][sub_unit]['agent-state'] |
1449 | + service_state[sub_sstate] += 1 |
1450 | + return service_state |
1451 | + |
1452 | + |
1453 | +def juju_status_summary(heading, statetype, states): |
1454 | + print heading |
1455 | + print " " + statetype |
1456 | + for state in states: |
1457 | + print " %s: %i" % (state, states[state]) |
1458 | + |
1459 | + |
1460 | +def juju_status_error_check(states): |
1461 | + for state in states: |
1462 | + if state in JUJU_STATUSES['bad']: |
1463 | + logging.error('Some statuses are in a bad state') |
1464 | + return True |
1465 | + logging.info('No statuses are in a bad state') |
1466 | + return False |
1467 | + |
1468 | + |
1469 | +def juju_status_all_stable(states): |
1470 | + for state in states: |
1471 | + if state in JUJU_STATUSES['transitional']: |
1472 | + logging.info('Some statuses are in a transitional state') |
1473 | + return False |
1474 | + logging.info('Statuses are in a stable state') |
1475 | + return True |
1476 | + |
1477 | + |
1478 | +def juju_status_check_and_wait(): |
1479 | + checks = { |
1480 | + 'Machines': [{ |
1481 | + 'Heading': 'Instance State', |
1482 | + 'check_func': get_machine_instance_states, |
1483 | + }, |
1484 | + { |
1485 | + 'Heading': 'Agent State', |
1486 | + 'check_func': get_machine_agent_states, |
1487 | + }], |
1488 | + 'Services': [{ |
1489 | + 'Heading': 'Agent State', |
1490 | + 'check_func': get_service_agent_states, |
1491 | + }] |
1492 | + } |
1493 | + stable_state = [False] |
1494 | + while False in stable_state: |
1495 | + juju_status = get_juju_status() |
1496 | + stable_state = [] |
1497 | + for juju_objtype, check_info in checks.iteritems(): |
1498 | + for check in check_info: |
1499 | + check_function = check['check_func'] |
1500 | + states = check_function(juju_status) |
1501 | + if juju_status_error_check(states): |
1502 | + raise Exception("Error in juju status") |
1503 | + stable_state.append(juju_status_all_stable(states)) |
1504 | + time.sleep(5) |
1505 | + for juju_objtype, check_info in checks.iteritems(): |
1506 | + for check in check_info: |
1507 | + check_function = check['check_func'] |
1508 | + states = check_function(juju_status) |
1509 | + juju_status_summary(juju_objtype, check['Heading'], states) |
1510 | + |
1511 | + |
1512 | +def remote_runs(units): |
1513 | + for unit in units: |
1514 | + if not remote_shell_check(unit): |
1515 | + raise Exception('Juju run failed on ' + unit) |
1516 | + |
1517 | + |
1518 | +def juju_check_hooks_complete(): |
1519 | + juju_units = get_juju_units() |
1520 | + remote_runs(juju_units) |
1521 | + remote_runs(juju_units) |
1522 | + |
1523 | + |
1524 | +def juju_wait_finished(): |
1525 | + # Wait till all statuses are green |
1526 | + juju_status_check_and_wait() |
1527 | + # juju status may report all has finished but hooks are still firing. |
1528 | + # So check.. |
1529 | + juju_check_hooks_complete() |
1530 | + # Check nothing has subsequently gone bad |
1531 | + juju_status_check_and_wait() |
1532 | + |
1533 | + |
1534 | +def build_swift_connection(): |
1535 | + """ |
1536 | + Create a Swift Connection object from the environment variables |
1537 | + OS_TENANT_NAME, OS_STORAGE_URL, OS_AUTH_URL, OS_USERNAME |
1538 | + OS_PASSWORD |
1539 | + """ |
1540 | + |
1541 | + # Get extra Swift options like tenant name and storage URL |
1542 | + os_options = {'tenant_name': os.environ.get('OS_TENANT_NAME')} |
1543 | + storage_url = os.environ.get('OS_STORAGE_URL') |
1544 | + if storage_url: |
1545 | + os_options['object_storage_url'] = storage_url |
1546 | + |
1547 | + return Connection( |
1548 | + os.environ.get('OS_AUTH_URL'), |
1549 | + os.environ.get('OS_USERNAME'), |
1550 | + os.environ.get('OS_PASSWORD'), |
1551 | + auth_version='2.0', |
1552 | + os_options=os_options |
1553 | + ) |
1554 | |
1555 | === added file 'mojo-spec-helpers/utils/shyaml.py' |
1556 | --- mojo-spec-helpers/utils/shyaml.py 1970-01-01 00:00:00 +0000 |
1557 | +++ mojo-spec-helpers/utils/shyaml.py 2015-09-08 08:23:55 +0000 |
1558 | @@ -0,0 +1,219 @@ |
1559 | +#!/usr/bin/env python |
1560 | + |
1561 | +# Note: to launch test, you can use: |
1562 | +# python -m doctest -d shyaml.py |
1563 | +# or |
1564 | +# nosetests |
1565 | + |
1566 | +from __future__ import print_function |
1567 | + |
1568 | +import sys |
1569 | +import yaml |
1570 | +import os.path |
1571 | +import re |
1572 | + |
1573 | +EXNAME = os.path.basename(sys.argv[0]) |
1574 | + |
1575 | + |
1576 | +def tokenize(s): |
1577 | + r"""Returns an iterable in all subpart of a '.' separated string |
1578 | + So: |
1579 | + >>> list(tokenize('foo.bar.wiz')) |
1580 | + ['foo', 'bar', 'wiz'] |
1581 | + this function has to deal with any type of data in the string. So it |
1582 | + actually interprets the string. Characters with meaning are '.' and '\'. |
1583 | + Both of these can be included in a token by quoting them with '\'. |
1584 | + So dot of slashes can be contained in token: |
1585 | + >>> print('\n'.join(tokenize(r'foo.dot<\.>.slash<\\>'))) |
1586 | + foo |
1587 | + dot<.> |
1588 | + slash<\> |
1589 | + Notice that empty keys are also supported: |
1590 | + >>> list(tokenize(r'foo..bar')) |
1591 | + ['foo', '', 'bar'] |
1592 | + Given an empty string: |
1593 | + >>> list(tokenize(r'')) |
1594 | + [''] |
1595 | + And a None value: |
1596 | + >>> list(tokenize(None)) |
1597 | + [] |
1598 | + """ |
1599 | + if s is None: |
1600 | + raise StopIteration |
1601 | + tokens = (re.sub(r'\\(\\|\.)', r'\1', m.group(0)) |
1602 | + for m in re.finditer(r'((\\.|[^.\\])*)', s)) |
1603 | + # an empty string superfluous token is added |
1604 | + # after all non-empty string token: |
1605 | + for token in tokens: |
1606 | + if len(token) != 0: |
1607 | + next(tokens) |
1608 | + yield token |
1609 | + |
1610 | + |
1611 | +def mget(dct, key, default=None): |
1612 | + r"""Allow to get values deep in a dict with doted keys |
1613 | + Accessing leaf values is quite straightforward: |
1614 | + >>> dct = {'a': {'x': 1, 'b': {'c': 2}}} |
1615 | + >>> mget(dct, 'a.x') |
1616 | + 1 |
1617 | + >>> mget(dct, 'a.b.c') |
1618 | + 2 |
1619 | + But you can also get subdict if your key is not targeting a |
1620 | + leaf value: |
1621 | + >>> mget(dct, 'a.b') |
1622 | + {'c': 2} |
1623 | + As a special feature, list access is also supported by providing a |
1624 | + (possibily signed) integer, it'll be interpreted as usual python |
1625 | + sequence access using bracket notation: |
1626 | + >>> mget({'a': {'x': [1, 5], 'b': {'c': 2}}}, 'a.x.-1') |
1627 | + 5 |
1628 | + >>> mget({'a': {'x': 1, 'b': [{'c': 2}]}}, 'a.b.0.c') |
1629 | + 2 |
1630 | + Keys that contains '.' can be accessed by escaping them: |
1631 | + >>> dct = {'a': {'x': 1}, 'a.x': 3, 'a.y': 4} |
1632 | + >>> mget(dct, 'a.x') |
1633 | + 1 |
1634 | + >>> mget(dct, r'a\.x') |
1635 | + 3 |
1636 | + >>> mget(dct, r'a.y') |
1637 | + >>> mget(dct, r'a\.y') |
1638 | + 4 |
1639 | + As a consequence, if your key contains a '\', you should also escape it: |
1640 | + >>> dct = {r'a\x': 3, r'a\.x': 4, 'a.x': 5, 'a\\': {'x': 6}} |
1641 | + >>> mget(dct, r'a\\x') |
1642 | + 3 |
1643 | + >>> mget(dct, r'a\\\.x') |
1644 | + 4 |
1645 | + >>> mget(dct, r'a\\.x') |
1646 | + 6 |
1647 | + >>> mget({'a\\': {'b': 1}}, r'a\\.b') |
1648 | + 1 |
1649 | + >>> mget({r'a.b\.c': 1}, r'a\.b\\\.c') |
1650 | + 1 |
1651 | + And even empty strings key are supported: |
1652 | + >>> dct = {r'a': {'': {'y': 3}, 'y': 4}, 'b': {'': {'': 1}}, '': 2} |
1653 | + >>> mget(dct, r'a..y') |
1654 | + 3 |
1655 | + >>> mget(dct, r'a.y') |
1656 | + 4 |
1657 | + >>> mget(dct, r'') |
1658 | + 2 |
1659 | + >>> mget(dct, r'b..') |
1660 | + 1 |
1661 | + mget support also default value if the key is not found: |
1662 | + >>> mget({'a': 1}, 'b.y', default='N/A') |
1663 | + 'N/A' |
1664 | + but will complain if you are trying to get into a leaf: |
1665 | + >>> mget({'a': 1}, 'a.y', default='N/A') # doctest: +ELLIPSIS |
1666 | + Traceback (most recent call last): |
1667 | + ... |
1668 | + TypeError: 'int' object ... |
1669 | + if the key is None, the whole dct should be sent back: |
1670 | + >>> mget({'a': 1}, None) |
1671 | + {'a': 1} |
1672 | + """ |
1673 | + return aget(dct, tokenize(key), default) |
1674 | + |
1675 | + |
1676 | +def aget(dct, key, default=None): |
1677 | + r"""Allow to get values deep in a dict with iterable keys |
1678 | + Accessing leaf values is quite straightforward: |
1679 | + >>> dct = {'a': {'x': 1, 'b': {'c': 2}}} |
1680 | + >>> aget(dct, ('a', 'x')) |
1681 | + 1 |
1682 | + >>> aget(dct, ('a', 'b', 'c')) |
1683 | + 2 |
1684 | + If key is empty, it returns unchanged the ``dct`` value. |
1685 | + >>> aget({'x': 1}, ()) |
1686 | + {'x': 1} |
1687 | + """ |
1688 | + key = iter(key) |
1689 | + try: |
1690 | + head = next(key) |
1691 | + except StopIteration: |
1692 | + return dct |
1693 | + try: |
1694 | + value = dct[int(head)] if isinstance(dct, list) else dct[head] |
1695 | + except KeyError: |
1696 | + return default |
1697 | + return aget(value, key, default) |
1698 | + |
1699 | + |
1700 | +def stderr(msg): |
1701 | + sys.stderr.write(msg + "\n") |
1702 | + |
1703 | + |
1704 | +def die(msg, errlvl=1, prefix="Error: "): |
1705 | + stderr("%s%s" % (prefix, msg)) |
1706 | + sys.exit(errlvl) |
1707 | + |
1708 | +SIMPLE_TYPES = (str, int, float) |
1709 | +COMPLEX_TYPES = (list, dict) |
1710 | + |
1711 | + |
1712 | +def dump(value): |
1713 | + return value if isinstance(value, SIMPLE_TYPES) \ |
1714 | + else yaml.dump(value, default_flow_style=False) |
1715 | + |
1716 | + |
1717 | +def type_name(value): |
1718 | + """Returns pseudo-YAML type name of given value.""" |
1719 | + return "struct" if isinstance(value, dict) else \ |
1720 | + "sequence" if isinstance(value, (tuple, list)) else \ |
1721 | + type(value).__name__ |
1722 | + |
1723 | + |
1724 | +def stdout(value): |
1725 | + sys.stdout.write(value) |
1726 | + |
1727 | + |
1728 | +def main(args): |
1729 | + usage = """usage: |
1730 | + %(exname)s {get-value{,-0},get-type,keys{,-0},values{,-0}} KEY DEFAULT |
1731 | + """ % {"exname": EXNAME} |
1732 | + if len(args) == 0: |
1733 | + die(usage, errlvl=0, prefix="") |
1734 | + action = args[0] |
1735 | + key_value = None if len(args) == 1 else args[1] |
1736 | + default = args[2] if len(args) > 2 else "" |
1737 | + contents = yaml.load(sys.stdin) |
1738 | + try: |
1739 | + value = mget(contents, key_value, default) |
1740 | + except IndexError: |
1741 | + die("list index error in path %r." % key_value) |
1742 | + except (KeyError, TypeError): |
1743 | + die("invalid path %r." % key_value) |
1744 | + |
1745 | + tvalue = type_name(value) |
1746 | + termination = "\0" if action.endswith("-0") else "\n" |
1747 | + |
1748 | + if action == "get-value": |
1749 | + print(dump(value), end='') |
1750 | + elif action in ("get-values", "get-values-0"): |
1751 | + if isinstance(value, dict): |
1752 | + for k, v in value.iteritems(): |
1753 | + stdout("%s%s%s%s" % (dump(k), termination, |
1754 | + dump(v), termination)) |
1755 | + elif isinstance(value, list): |
1756 | + for l in value: |
1757 | + stdout("%s%s" % (dump(l), termination)) |
1758 | + else: |
1759 | + die("%s does not support %r type. " |
1760 | + "Please provide or select a sequence or struct." |
1761 | + % (action, tvalue)) |
1762 | + elif action == "get-type": |
1763 | + print(tvalue) |
1764 | + elif action in ("keys", "keys-0", "values", "values-0"): |
1765 | + if isinstance(value, dict): |
1766 | + method = value.keys if action.startswith("keys") else value.values |
1767 | + for k in method(): |
1768 | + stdout("%s%s" % (dump(k), termination)) |
1769 | + else: |
1770 | + die("%s does not support %r type. " |
1771 | + "Please provide or select a struct." % (action, tvalue)) |
1772 | + else: |
1773 | + die("Invalid argument.\n%s" % usage) |
1774 | + |
1775 | + |
1776 | +if __name__ == "__main__": |
1777 | + sys.exit(main(sys.argv[1:])) |
1778 | |
1779 | === added directory 'mojo-spec-helpers/utils/tests' |
1780 | === added file 'mojo-spec-helpers/utils/tests/README.md' |
1781 | --- mojo-spec-helpers/utils/tests/README.md 1970-01-01 00:00:00 +0000 |
1782 | +++ mojo-spec-helpers/utils/tests/README.md 2015-09-08 08:23:55 +0000 |
1783 | @@ -0,0 +1,67 @@ |
1784 | +Tests for python managers |
1785 | +=== |
1786 | + |
1787 | +These are tests for `container_managers.py`, `cache_managers.py` |
1788 | +and some functions in `mojo_utils.py`. |
1789 | + |
1790 | +Setup |
1791 | +--- |
1792 | + |
1793 | +### A test container |
1794 | + |
1795 | +Tests for `container_managers.py` require a test container to be setup with objects named as follows: |
1796 | + |
1797 | +- latest-build-label |
1798 | +- deployed-build-label |
1799 | +- deployed-spec-revno |
1800 | +- code-upgrade-test-build-label-01-test-build-label-02-succeeded |
1801 | +- mojo-run-155-succeeded |
1802 | + |
1803 | +This container should be openly readable: |
1804 | + |
1805 | +``` bash |
1806 | +swift post --read-acl .r:* ${TEST_CONTAINER_NAME} |
1807 | +``` |
1808 | + |
1809 | +You should then set the URL for this container in the environment variable `TEST_CONTAINER_URL`: |
1810 | + |
1811 | +``` bash |
1812 | +export TEST_CONTAINER_URL=$(swift stat -v ${TEST_CONTAINER_NAME} | egrep -o 'http.*$') |
1813 | +``` |
1814 | + |
1815 | +### A local swift account |
1816 | + |
1817 | +You also need to make the credentials for connecting to a swift account available with `OS_*` environment variables: |
1818 | + |
1819 | +``` bash |
1820 | +export OS_USERNAME=${USERNAME} |
1821 | +export OS_TENANT_NAME=${TENANT_NAME} |
1822 | +export OS_PASSWORD=${PASSWORD} |
1823 | +export OS_STORAGE_URL=${OPTIONAL_STORAGE_URL} |
1824 | +export OS_AUTH_URL=${AUTH_URL} |
1825 | +export OS_REGION_NAME=${REGION_NAME} |
1826 | +``` |
1827 | + |
1828 | +### PYTHONPATH |
1829 | + |
1830 | +You also need to add the parent directory to your python path to run the tests: |
1831 | + |
1832 | +``` bash |
1833 | +export PYTHONPATH=.. |
1834 | +``` |
1835 | + |
1836 | +Running the tests |
1837 | +--- |
1838 | + |
1839 | +You can either run the tests with native python: |
1840 | + |
1841 | +``` bash |
1842 | +./run_tests.py |
1843 | +``` |
1844 | + |
1845 | +Or with `pytest`: |
1846 | + |
1847 | +``` bash |
1848 | +py.test |
1849 | +``` |
1850 | + |
1851 | |
1852 | === added file 'mojo-spec-helpers/utils/tests/run_tests.py' |
1853 | --- mojo-spec-helpers/utils/tests/run_tests.py 1970-01-01 00:00:00 +0000 |
1854 | +++ mojo-spec-helpers/utils/tests/run_tests.py 2015-09-08 08:23:55 +0000 |
1855 | @@ -0,0 +1,18 @@ |
1856 | +#!/usr/bin/env python |
1857 | + |
1858 | +""" |
1859 | +These are tests for container_managers.py, cache_managers.py |
1860 | +and some scripts in mojo_utils.py |
1861 | + |
1862 | +To run the container_tests, you'll need to set several environment |
1863 | +variables - see test_container_managers.py |
1864 | +""" |
1865 | + |
1866 | +from test_container_managers import container_tests |
1867 | +from test_cache_managers import test_cache_managers |
1868 | +from test_mojo_utils import test_get_mojo_spec_revno |
1869 | + |
1870 | + |
1871 | +container_tests() |
1872 | +test_cache_managers() |
1873 | +test_get_mojo_spec_revno() |
1874 | |
1875 | === added file 'mojo-spec-helpers/utils/tests/test_cache_managers.py' |
1876 | --- mojo-spec-helpers/utils/tests/test_cache_managers.py 1970-01-01 00:00:00 +0000 |
1877 | +++ mojo-spec-helpers/utils/tests/test_cache_managers.py 2015-09-08 08:23:55 +0000 |
1878 | @@ -0,0 +1,80 @@ |
1879 | +# System imports |
1880 | +import os |
1881 | +import subprocess |
1882 | +import json |
1883 | + |
1884 | +# Local imports |
1885 | +from cache_managers import JsonCache |
1886 | + |
1887 | + |
1888 | +def _check_cache_file(cache, cache_path): |
1889 | + # Check file exists |
1890 | + assert os.path.isfile(cache_path), "Cache file not created" |
1891 | + print u"\u2713 Cache file exists: {}".format(cache_path) |
1892 | + |
1893 | + # Check the data |
1894 | + with open(cache_path) as cache_file: |
1895 | + assert json.load(cache_file) == cache.get_cache(), "Bad data" |
1896 | + print u"\u2713 Cache file data is correct" |
1897 | + |
1898 | + |
1899 | +def test_cache_managers(): |
1900 | + print ( |
1901 | + "\n===\n" |
1902 | + "Test JsonCache" |
1903 | + "\n===\n" |
1904 | + ) |
1905 | + |
1906 | + cache_path = subprocess.check_output( |
1907 | + 'mktemp -u /tmp/cache-XXXX.json'.split() |
1908 | + ).strip() |
1909 | + |
1910 | + cache = JsonCache(cache_path=cache_path) |
1911 | + |
1912 | + fake_data = { |
1913 | + 'sentence': 'hello world', |
1914 | + 'number': 12, |
1915 | + 'array': [1, 2, 3, "fish"] |
1916 | + } |
1917 | + |
1918 | + # Insert items using "set" |
1919 | + for key, value in fake_data.iteritems(): |
1920 | + cache.set(key, value) |
1921 | + |
1922 | + # Check data against inserted data |
1923 | + assert fake_data == cache.get_cache(), "Data not set correctly" |
1924 | + print u"\u2713 Data correctly inserted" |
1925 | + |
1926 | + # Verify cache file |
1927 | + _check_cache_file(cache, cache_path) |
1928 | + |
1929 | + # Check retrieving each key with "get" |
1930 | + for key, value in fake_data.iteritems(): |
1931 | + assert cache.get(key) == value |
1932 | + |
1933 | + print u"\u2713 Successfully retrieved items with 'get'" |
1934 | + |
1935 | + # Check wiping the cache |
1936 | + cache.wipe() |
1937 | + |
1938 | + assert not os.path.isfile(cache_path), "Cache file shouldn't exist!" |
1939 | + assert not cache.get(fake_data.keys()[0]), "Cache still returning data" |
1940 | + print u"\u2713 Cache successfully wiped" |
1941 | + |
1942 | + # Recreate cache with "put_cache" |
1943 | + cache.put_cache(fake_data) |
1944 | + |
1945 | + # Check data against inserted data |
1946 | + assert fake_data == cache.get_cache() |
1947 | + print u"\u2713 Data correctly inserted" |
1948 | + |
1949 | + # Check file integrity |
1950 | + _check_cache_file(cache, cache_path) |
1951 | + |
1952 | + # Clean up |
1953 | + cache.wipe() |
1954 | + assert not os.path.isfile(cache_path), "Cache file shouldn't exist!" |
1955 | + print u"\u2713 Deleted {}".format(cache_path) |
1956 | + |
1957 | +if __name__ == "__main__": |
1958 | + test_cache_managers() |
1959 | |
1960 | === added file 'mojo-spec-helpers/utils/tests/test_container_managers.py' |
1961 | --- mojo-spec-helpers/utils/tests/test_container_managers.py 1970-01-01 00:00:00 +0000 |
1962 | +++ mojo-spec-helpers/utils/tests/test_container_managers.py 2015-09-08 08:23:55 +0000 |
1963 | @@ -0,0 +1,188 @@ |
1964 | +# System |
1965 | +import os |
1966 | + |
1967 | +# Local imports |
1968 | +from container_managers import ( |
1969 | + BuildContainer, |
1970 | + CIContainer, |
1971 | + DeployedEnvironmentContainer, |
1972 | + LocalEnvironmentSwiftContainer |
1973 | +) |
1974 | +from mojo_utils import build_swift_connection |
1975 | + |
1976 | +""" |
1977 | +Tests for container_managers. |
1978 | + |
1979 | +To test these you'll need to set a TEST_CONTAINER_URL, |
1980 | +which should be the HTTP URL to a swift container which contains |
1981 | +the following publicly readable objects: |
1982 | +- latest-build-label |
1983 | +- deployed-build-label |
1984 | +- deployed-spec-revno |
1985 | +- code-upgrade-test-build-label-01-test-build-label-02-succeeded |
1986 | +- mojo-run-155-succeeded |
1987 | + |
1988 | +You'll also need to have the following environment variables setup |
1989 | +with credentials to connect to a valid (testing) Swift account: |
1990 | + |
1991 | +OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_REGION_NAME |
1992 | +""" |
1993 | + |
1994 | +test_container_url = os.environ.get('TEST_CONTAINER_URL') |
1995 | + |
1996 | + |
1997 | +def test_deployed_environment_container(): |
1998 | + print ( |
1999 | + "\n===\n" |
2000 | + "Test DeployedEnvironmentContainer" |
2001 | + "\n===\n" |
2002 | + ) |
2003 | + |
2004 | + deployed_env_container = DeployedEnvironmentContainer( |
2005 | + container_url=test_container_url, |
2006 | + ) |
2007 | + |
2008 | + build_label = deployed_env_container.deployed_build_label() |
2009 | + assert build_label, "Build label missing" |
2010 | + print u"\u2713 Found build label: {}".format(build_label) |
2011 | + |
2012 | + spec_revno = deployed_env_container.deployed_spec_revno() |
2013 | + assert spec_revno, "Spec revno missing" |
2014 | + print u"\u2713 Found spec revno (from web): {}".format(spec_revno) |
2015 | + |
2016 | + |
2017 | +def test_local_environment_swift_container(): |
2018 | + print ( |
2019 | + "\n===\n" |
2020 | + "Test LocalEnvironmentSwiftContainer" |
2021 | + "\n===\n" |
2022 | + ) |
2023 | + |
2024 | + swift_connection = build_swift_connection() |
2025 | + |
2026 | + container_name = 'test-container' |
2027 | + |
2028 | + swift_connection.put_container(container_name) |
2029 | + |
2030 | + container = LocalEnvironmentSwiftContainer( |
2031 | + swift_connection=swift_connection, |
2032 | + container_name=container_name |
2033 | + ) |
2034 | + |
2035 | + deployed_build_label = "fiddlesticksandfishes" |
2036 | + previous_build_label = "fishesandfiddlesticks" |
2037 | + deployed_spec_revno = '111' |
2038 | + |
2039 | + container.save_deployed_build_label(deployed_build_label) |
2040 | + container.save_previous_build_label(previous_build_label) |
2041 | + container.save_mojo_spec_revno(deployed_spec_revno) |
2042 | + |
2043 | + assert container.deployed_build_label() == deployed_build_label |
2044 | + assert container.previous_build_label() == previous_build_label |
2045 | + assert container.deployed_spec_revno() == deployed_spec_revno |
2046 | + print u"\u2713 Saved and retrieved build_labels and spec_revno" |
2047 | + |
2048 | + # Get names for objects |
2049 | + previous_obj = container.previous_build_obj |
2050 | + deployed_obj = container.deployed_build_obj |
2051 | + revno_obj = container.deployed_revno_obj |
2052 | + upgrade_object = container.code_upgrade_succeeded_template.format( |
2053 | + previous_build_label, deployed_build_label |
2054 | + ) |
2055 | + run_object = container.mojo_run_succeeded_template.format( |
2056 | + deployed_spec_revno |
2057 | + ) |
2058 | + |
2059 | + # Remove the objects if they exist |
2060 | + for object_name in [previous_obj, deployed_obj, revno_obj, upgrade_object]: |
2061 | + try: |
2062 | + swift_connection.delete_object(container_name, object_name) |
2063 | + except: |
2064 | + pass |
2065 | + |
2066 | + container.save_code_upgrade_succeeded( |
2067 | + previous_build_label, deployed_build_label |
2068 | + ) |
2069 | + |
2070 | + container.save_mojo_run_succeeded(deployed_spec_revno) |
2071 | + |
2072 | + upgrade_head = swift_connection.head_object(container_name, upgrade_object) |
2073 | + |
2074 | + run_head = swift_connection.head_object(container_name, run_object) |
2075 | + |
2076 | + # Make sure the objects were saved to swift |
2077 | + assert int(upgrade_head['content-length']) > 0 |
2078 | + assert int(run_head['content-length']) > 0 |
2079 | + |
2080 | + print u"\u2713 Code upgrade and mojo run successfully saved" |
2081 | + |
2082 | + |
2083 | +def test_latest_build(): |
2084 | + print ( |
2085 | + "\n===\n" |
2086 | + "Test BuildContainer" |
2087 | + "\n===\n" |
2088 | + ) |
2089 | + |
2090 | + build_container = BuildContainer( |
2091 | + container_url=test_container_url |
2092 | + ) |
2093 | + |
2094 | + build_label = build_container.latest_build_label() |
2095 | + assert build_label, "Build label missing" |
2096 | + print u"\u2713 Found latest build label: {}".format(build_label) |
2097 | + |
2098 | + |
2099 | +def test_ci_container(): |
2100 | + print ( |
2101 | + "\n===\n" |
2102 | + "Test CIContainer" |
2103 | + "\n===\n" |
2104 | + ) |
2105 | + |
2106 | + ci_container = CIContainer( |
2107 | + container_url=test_container_url |
2108 | + ) |
2109 | + |
2110 | + build_one = "test-build-label-01" |
2111 | + build_two = "test-build-label-02" |
2112 | + |
2113 | + good_upgrade = ci_container.has_code_upgrade_been_tested( |
2114 | + from_build_label=build_one, |
2115 | + to_build_label=build_two |
2116 | + ) |
2117 | + bad_upgrade = ci_container.has_code_upgrade_been_tested( |
2118 | + from_build_label=build_two, |
2119 | + to_build_label=build_one |
2120 | + ) |
2121 | + |
2122 | + good_upgrade = ci_container.has_code_upgrade_been_tested( |
2123 | + from_build_label=build_one, |
2124 | + to_build_label=build_two |
2125 | + ) |
2126 | + bad_upgrade = ci_container.has_code_upgrade_been_tested( |
2127 | + from_build_label=build_two, |
2128 | + to_build_label=build_one |
2129 | + ) |
2130 | + |
2131 | + assert good_upgrade, "Good upgrade test missing" |
2132 | + assert bad_upgrade is False, "Bad upgrade isn't false" |
2133 | + print u"\u2713 Upgrades checked successfully" |
2134 | + |
2135 | + good_mojo_test = ci_container.has_mojo_run_been_tested(spec_revno="155") |
2136 | + bad_mojo_test = ci_container.has_mojo_run_been_tested(spec_revno="99999") |
2137 | + |
2138 | + assert good_mojo_test, "Good mojo test missing" |
2139 | + assert bad_mojo_test is False, "Bad mojo test isn't false" |
2140 | + print u"\u2713 Mojo runs checked succeeded" |
2141 | + |
2142 | + |
2143 | +def container_tests(): |
2144 | + test_deployed_environment_container() |
2145 | + test_local_environment_swift_container() |
2146 | + test_latest_build() |
2147 | + test_ci_container() |
2148 | + |
2149 | + |
2150 | +if __name__ == "__main__": |
2151 | + container_tests() |
2152 | |
2153 | === added file 'mojo-spec-helpers/utils/tests/test_mojo_utils.py' |
2154 | --- mojo-spec-helpers/utils/tests/test_mojo_utils.py 1970-01-01 00:00:00 +0000 |
2155 | +++ mojo-spec-helpers/utils/tests/test_mojo_utils.py 2015-09-08 08:23:55 +0000 |
2156 | @@ -0,0 +1,23 @@ |
2157 | +# System imports |
2158 | +import os |
2159 | + |
2160 | +# Loca imports |
2161 | +from mojo_utils import get_mojo_spec_revno |
2162 | + |
2163 | + |
2164 | +def test_get_mojo_spec_revno(): |
2165 | + print ( |
2166 | + "\n===\n" |
2167 | + "Test get_mojo_spec_revno" |
2168 | + "\n===\n" |
2169 | + ) |
2170 | + |
2171 | + os.environ['MOJO_SPEC_DIR'] = os.path.abspath( |
2172 | + __file__ + '/../../../..' |
2173 | + ) |
2174 | + |
2175 | + assert get_mojo_spec_revno().isdigit() |
2176 | + print u"\u2713 Successfully retrieved revno" |
2177 | + |
2178 | +if __name__ == "__main__": |
2179 | + test_get_mojo_spec_revno() |
I'm going to test this in CI first before putting this MP up for review.