Merge lp:~1chb1n/charm-helpers/heat-amulet into lp:charm-helpers

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 388
Proposed branch: lp:~1chb1n/charm-helpers/heat-amulet
Merge into: lp:charm-helpers
Diff against target: 481 lines (+227/-12)
4 files modified
charmhelpers/contrib/amulet/utils.py (+91/-6)
charmhelpers/contrib/openstack/amulet/deployment.py (+6/-2)
charmhelpers/contrib/openstack/amulet/utils.py (+122/-3)
charmhelpers/contrib/openstack/utils.py (+8/-1)
To merge this branch: bzr merge lp:~1chb1n/charm-helpers/heat-amulet
Reviewer Review Type Date Requested Status
Corey Bryant (community) Approve
James Page Pending
Review via email: mp+260723@code.launchpad.net

Description of the change

* Prep helpers for Wily/Liberty; resolve additional Vivid issues.

* Add heat client method.

* Add generic openstack resource status check and delete methods.

* Add create_or_get_keypair method.

* Add file_to_url method.

* Add get_ubuntu_releases - uses distro_info to create a list property containing Ubuntu release codenames, in order of release.

* Add get_ubuntu_release_from_sentry - return lsb_release codename from unit.

* Flag validate_services with deprecation warning. This method should be eventually be deprecated in favor of the following, after charm tests are updated to use validate_services_by_name.

* Add validate_services_by_name - uses a dictionary of units:service-names to check service status, automatically generating command based on init system type. Tests should now be updated to use this.

* Add debug logging.

Tracking bug: https://bugs.launchpad.net/charm-helpers/+bug/1461535

This should be landed in conjunction with the new heat amulet tests @ https://code.launchpad.net/~1chb1n/charms/trusty/heat/next-amulet-init/+merge/258105.

## resource status and resource delete rationale:
New and existing amulet tests can be made with less code effort or duplication.

Rather than having numerous delete_XYZ methods for each of the numerous openstack types/objects, use delete_resource and pass a pointer. Similarly, instead of having wait/check/timeout/confirm loops built into numerous methods, use resource_reaches_status + a pointer.

To post a comment you must log in.
Revision history for this message
Corey Bryant (corey.bryant) wrote :

The changes make sense to me. How about 'resource' instead of 'thing'? Something like delete_openstack_resource() seems like it would be understandable if someone was reading through code.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Also there's one comment inline.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Looks like there are also some lint errors.

Revision history for this message
Ryan Beisner (1chb1n) wrote :

See that's why I ask you! Resource makes too much sense. Ack on the lint, already resolved locally. Also fully agree on the inline comment about comments. Thanks Corey!

Revision history for this message
Ryan Beisner (1chb1n) wrote :

FYI - all amulet tests pass except Vivid, due to bug 1461535. Addressing @ charmhelpers as well as the heat branch.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Looking good. I'd just remove the old validate_services() now and rename validate_services_by_name() to validate_services(), and land all the charm test updates at the same time.

Revision history for this message
Ryan Beisner (1chb1n) wrote :

I'd prefer to not do that actually. I'd like to take it smaller chunks.

This is what I'm aiming for:

1. Land heat amulet tests, at the same time unblocking vivid for all of the other amulet tests.

2. Enable and land vivid coverage in all of the charms with existing amulet tests, while also updating each of them.

3. Deprecate the old validate_services after it is confirmed to be not-used.

4. Add amulet tests for charms which don't yet have them.

Whaddaya think?

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Also, rmq reply inline.

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Oh, add to (3): Deprecate delete_image after the relevant tests have been updated to use delete_resource.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

> I'd prefer to not do that actually. I'd like to take it smaller chunks.
>
> This is what I'm aiming for:
>
> 1. Land heat amulet tests, at the same time unblocking vivid for all of the
> other amulet tests.
>
> 2. Enable and land vivid coverage in all of the charms with existing amulet
> tests, while also updating each of them.
>
> 3. Deprecate the old validate_services after it is confirmed to be not-used.
>
> 4. Add amulet tests for charms which don't yet have them.
>
> Whaddaya think?

I just prefer not to land code that's going to be pulled back out in a week. But I can't land to c-h anyway so it's not up to me.

Now that I think of it though, and more importantly, I don't know what the process is for deprecating functions that aren't openstack specific. Someone else might already be using them and some of these changes would break them. I wonder what the precedence is.

Revision history for this message
Ryan Beisner (1chb1n) wrote :

I can definitely see your point, which raises more caution:

The original validate_services method isn't actually broken in any way. Misnamed, maybe, but not broken. It just runs commands and reports back pass/fail. If the test writer passes the proper service-checking (or other arbitrary) commands, it will happily run them. A more appropriate name for the method may have been something like run_these_commands_on_these_units(), as it's really not service-aware at all.

Maybe we shouldn't deprecate or change the behavior of validate_services at all, as there is no way to know who that will break.

I'd suggest that we:

leave validate_services untouched, not "fixed"

add validate_services_by_name, as proposed, and roll tests to it as we update each charm.

discuss and handle the topic of deprecation separately, if at all.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Good points. I think that sounds like a good plan, and keep your deprecation comments to steer folks to the new functions.

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Ok, thanks - let's proceed as such.

Just a reminder, the "create or get keypair" method will be movedto this MP once that's reworked from the heat MP.

lp:~1chb1n/charm-helpers/heat-amulet updated
385. By Ryan Beisner

Return validate_services to original functionality
Add create_or_get_keypair
Update comments and docstrings

386. By Ryan Beisner

Add Wily/Liberty awareness.

387. By Ryan Beisner

add file_to_url

Revision history for this message
Ryan Beisner (1chb1n) wrote :

Ready for review.

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Looks good. 2 really minor inline comments.

review: Approve
lp:~1chb1n/charm-helpers/heat-amulet updated
388. By Ryan Beisner

light cleanup and comment correction

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/amulet/utils.py'
2--- charmhelpers/contrib/amulet/utils.py 2015-04-21 15:40:51 +0000
3+++ charmhelpers/contrib/amulet/utils.py 2015-06-11 19:47:33 +0000
4@@ -15,13 +15,15 @@
5 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
6
7 import ConfigParser
8+import distro_info
9 import io
10 import logging
11+import os
12 import re
13+import six
14 import sys
15 import time
16-
17-import six
18+import urlparse
19
20
21 class AmuletUtils(object):
22@@ -33,6 +35,7 @@
23
24 def __init__(self, log_level=logging.ERROR):
25 self.log = self.get_logger(level=log_level)
26+ self.ubuntu_releases = self.get_ubuntu_releases()
27
28 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
29 """Get a logger object that will log to stdout."""
30@@ -70,12 +73,44 @@
31 else:
32 return False
33
34+ def get_ubuntu_release_from_sentry(self, sentry_unit):
35+ """Get Ubuntu release codename from sentry unit.
36+
37+ :param sentry_unit: amulet sentry/service unit pointer
38+ :returns: list of strings - release codename, failure message
39+ """
40+ msg = None
41+ cmd = 'lsb_release -cs'
42+ release, code = sentry_unit.run(cmd)
43+ if code == 0:
44+ self.log.debug('{} lsb_release: {}'.format(
45+ sentry_unit.info['unit_name'], release))
46+ else:
47+ msg = ('{} `{}` returned {} '
48+ '{}'.format(sentry_unit.info['unit_name'],
49+ cmd, release, code))
50+ if release not in self.ubuntu_releases:
51+ msg = ("Release ({}) not found in Ubuntu releases "
52+ "({})".format(release, self.ubuntu_releases))
53+ return release, msg
54+
55 def validate_services(self, commands):
56- """Validate services.
57-
58- Verify the specified services are running on the corresponding
59+ """Validate that lists of commands succeed on service units. Can be
60+ used to verify system services are running on the corresponding
61 service units.
62- """
63+
64+ :param commands: dict with sentry keys and arbitrary command list values
65+ :returns: None if successful, Failure string message otherwise
66+ """
67+ self.log.debug('Checking status of system services...')
68+
69+ # /!\ DEPRECATION WARNING (beisner):
70+ # New and existing tests should be rewritten to use
71+ # validate_services_by_name() as it is aware of init systems.
72+ self.log.warn('/!\\ DEPRECATION WARNING: use '
73+ 'validate_services_by_name instead of validate_services '
74+ 'due to init system differences.')
75+
76 for k, v in six.iteritems(commands):
77 for cmd in v:
78 output, code = k.run(cmd)
79@@ -86,6 +121,41 @@
80 return "command `{}` returned {}".format(cmd, str(code))
81 return None
82
83+ def validate_services_by_name(self, sentry_services):
84+ """Validate system service status by service name, automatically
85+ detecting init system based on Ubuntu release codename.
86+
87+ :param sentry_services: dict with sentry keys and svc list values
88+ :returns: None if successful, Failure string message otherwise
89+ """
90+ self.log.debug('Checking status of system services...')
91+
92+ # Point at which systemd became a thing
93+ systemd_switch = self.ubuntu_releases.index('vivid')
94+
95+ for sentry_unit, services_list in six.iteritems(sentry_services):
96+ # Get lsb_release codename from unit
97+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
98+ if ret:
99+ return ret
100+
101+ for service_name in services_list:
102+ if (self.ubuntu_releases.index(release) >= systemd_switch or
103+ service_name == "rabbitmq-server"):
104+ # init is systemd
105+ cmd = 'sudo service {} status'.format(service_name)
106+ elif self.ubuntu_releases.index(release) < systemd_switch:
107+ # init is upstart
108+ cmd = 'sudo status {}'.format(service_name)
109+
110+ output, code = sentry_unit.run(cmd)
111+ self.log.debug('{} `{}` returned '
112+ '{}'.format(sentry_unit.info['unit_name'],
113+ cmd, code))
114+ if code != 0:
115+ return "command `{}` returned {}".format(cmd, str(code))
116+ return None
117+
118 def _get_config(self, unit, filename):
119 """Get a ConfigParser object for parsing a unit's config file."""
120 file_contents = unit.file_contents(filename)
121@@ -104,6 +174,9 @@
122 Verify that the specified section of the config file contains
123 the expected option key:value pairs.
124 """
125+ self.log.debug('Validating config file data ({} in {} on {})'
126+ '...'.format(section, config_file,
127+ sentry_unit.info['unit_name']))
128 config = self._get_config(sentry_unit, config_file)
129
130 if section != 'DEFAULT' and not config.has_section(section):
131@@ -321,3 +394,15 @@
132
133 def endpoint_error(self, name, data):
134 return 'unexpected endpoint data in {} - {}'.format(name, data)
135+
136+ def get_ubuntu_releases(self):
137+ """Return a list of all Ubuntu releases in order of release."""
138+ _d = distro_info.UbuntuDistroInfo()
139+ _release_list = _d.all
140+ self.log.debug('Ubuntu release list: {}'.format(_release_list))
141+ return _release_list
142+
143+ def file_to_url(self, file_rel_path):
144+ """Convert a relative file path to a file URL."""
145+ _abs_path = os.path.abspath(file_rel_path)
146+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
147
148=== modified file 'charmhelpers/contrib/openstack/amulet/deployment.py'
149--- charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-21 15:40:51 +0000
150+++ charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-11 19:47:33 +0000
151@@ -110,7 +110,8 @@
152 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
153 self.precise_havana, self.precise_icehouse,
154 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
155- self.trusty_kilo, self.vivid_kilo) = range(10)
156+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
157+ self.wily_liberty) = range(12)
158
159 releases = {
160 ('precise', None): self.precise_essex,
161@@ -121,8 +122,10 @@
162 ('trusty', None): self.trusty_icehouse,
163 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
164 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
165+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
166 ('utopic', None): self.utopic_juno,
167- ('vivid', None): self.vivid_kilo}
168+ ('vivid', None): self.vivid_kilo,
169+ ('wily', None): self.wily_liberty}
170 return releases[(self.series, self.openstack)]
171
172 def _get_openstack_release_string(self):
173@@ -138,6 +141,7 @@
174 ('trusty', 'icehouse'),
175 ('utopic', 'juno'),
176 ('vivid', 'kilo'),
177+ ('wily', 'liberty'),
178 ])
179 if self.openstack:
180 os_origin = self.openstack.split(':')[1]
181
182=== modified file 'charmhelpers/contrib/openstack/amulet/utils.py'
183--- charmhelpers/contrib/openstack/amulet/utils.py 2015-01-22 06:06:03 +0000
184+++ charmhelpers/contrib/openstack/amulet/utils.py 2015-06-11 19:47:33 +0000
185@@ -16,15 +16,15 @@
186
187 import logging
188 import os
189+import six
190 import time
191 import urllib
192
193 import glanceclient.v1.client as glance_client
194+import heatclient.v1.client as heat_client
195 import keystoneclient.v2_0 as keystone_client
196 import novaclient.v1_1.client as nova_client
197
198-import six
199-
200 from charmhelpers.contrib.amulet.utils import (
201 AmuletUtils
202 )
203@@ -37,7 +37,7 @@
204 """OpenStack amulet utilities.
205
206 This class inherits from AmuletUtils and has additional support
207- that is specifically for use by OpenStack charms.
208+ that is specifically for use by OpenStack charm tests.
209 """
210
211 def __init__(self, log_level=ERROR):
212@@ -51,6 +51,8 @@
213 Validate actual endpoint data vs expected endpoint data. The ports
214 are used to find the matching endpoint.
215 """
216+ self.log.debug('Validating endpoint data...')
217+ self.log.debug('actual: {}'.format(repr(endpoints)))
218 found = False
219 for ep in endpoints:
220 self.log.debug('endpoint: {}'.format(repr(ep)))
221@@ -77,6 +79,7 @@
222 Validate a list of actual service catalog endpoints vs a list of
223 expected service catalog endpoints.
224 """
225+ self.log.debug('Validating service catalog endpoint data...')
226 self.log.debug('actual: {}'.format(repr(actual)))
227 for k, v in six.iteritems(expected):
228 if k in actual:
229@@ -93,6 +96,7 @@
230 Validate a list of actual tenant data vs list of expected tenant
231 data.
232 """
233+ self.log.debug('Validating tenant data...')
234 self.log.debug('actual: {}'.format(repr(actual)))
235 for e in expected:
236 found = False
237@@ -114,6 +118,7 @@
238 Validate a list of actual role data vs a list of expected role
239 data.
240 """
241+ self.log.debug('Validating role data...')
242 self.log.debug('actual: {}'.format(repr(actual)))
243 for e in expected:
244 found = False
245@@ -134,6 +139,7 @@
246 Validate a list of actual user data vs a list of expected user
247 data.
248 """
249+ self.log.debug('Validating user data...')
250 self.log.debug('actual: {}'.format(repr(actual)))
251 for e in expected:
252 found = False
253@@ -155,17 +161,20 @@
254
255 Validate a list of actual flavors vs a list of expected flavors.
256 """
257+ self.log.debug('Validating flavor data...')
258 self.log.debug('actual: {}'.format(repr(actual)))
259 act = [a.name for a in actual]
260 return self._validate_list_data(expected, act)
261
262 def tenant_exists(self, keystone, tenant):
263 """Return True if tenant exists."""
264+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
265 return tenant in [t.name for t in keystone.tenants.list()]
266
267 def authenticate_keystone_admin(self, keystone_sentry, user, password,
268 tenant):
269 """Authenticates admin user with the keystone admin endpoint."""
270+ self.log.debug('Authenticating keystone admin...')
271 unit = keystone_sentry
272 service_ip = unit.relation('shared-db',
273 'mysql:shared-db')['private-address']
274@@ -175,6 +184,7 @@
275
276 def authenticate_keystone_user(self, keystone, user, password, tenant):
277 """Authenticates a regular user with the keystone public endpoint."""
278+ self.log.debug('Authenticating keystone user ({})...'.format(user))
279 ep = keystone.service_catalog.url_for(service_type='identity',
280 endpoint_type='publicURL')
281 return keystone_client.Client(username=user, password=password,
282@@ -182,12 +192,21 @@
283
284 def authenticate_glance_admin(self, keystone):
285 """Authenticates admin user with glance."""
286+ self.log.debug('Authenticating glance admin...')
287 ep = keystone.service_catalog.url_for(service_type='image',
288 endpoint_type='adminURL')
289 return glance_client.Client(ep, token=keystone.auth_token)
290
291+ def authenticate_heat_admin(self, keystone):
292+ """Authenticates the admin user with heat."""
293+ self.log.debug('Authenticating heat admin...')
294+ ep = keystone.service_catalog.url_for(service_type='orchestration',
295+ endpoint_type='publicURL')
296+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
297+
298 def authenticate_nova_user(self, keystone, user, password, tenant):
299 """Authenticates a regular user with nova-api."""
300+ self.log.debug('Authenticating nova user ({})...'.format(user))
301 ep = keystone.service_catalog.url_for(service_type='identity',
302 endpoint_type='publicURL')
303 return nova_client.Client(username=user, api_key=password,
304@@ -195,6 +214,7 @@
305
306 def create_cirros_image(self, glance, image_name):
307 """Download the latest cirros image and upload it to glance."""
308+ self.log.debug('Creating glance image ({})...'.format(image_name))
309 http_proxy = os.getenv('AMULET_HTTP_PROXY')
310 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
311 if http_proxy:
312@@ -235,6 +255,11 @@
313
314 def delete_image(self, glance, image):
315 """Delete the specified image."""
316+
317+ # /!\ DEPRECATION WARNING
318+ self.log.warn('/!\\ DEPRECATION WARNING: use '
319+ 'delete_resource instead of delete_image.')
320+ self.log.debug('Deleting glance image ({})...'.format(image))
321 num_before = len(list(glance.images.list()))
322 glance.images.delete(image)
323
324@@ -254,6 +279,8 @@
325
326 def create_instance(self, nova, image_name, instance_name, flavor):
327 """Create the specified instance."""
328+ self.log.debug('Creating instance '
329+ '({}|{}|{})'.format(instance_name, image_name, flavor))
330 image = nova.images.find(name=image_name)
331 flavor = nova.flavors.find(name=flavor)
332 instance = nova.servers.create(name=instance_name, image=image,
333@@ -276,6 +303,11 @@
334
335 def delete_instance(self, nova, instance):
336 """Delete the specified instance."""
337+
338+ # /!\ DEPRECATION WARNING
339+ self.log.warn('/!\\ DEPRECATION WARNING: use '
340+ 'delete_resource instead of delete_instance.')
341+ self.log.debug('Deleting instance ({})...'.format(instance))
342 num_before = len(list(nova.servers.list()))
343 nova.servers.delete(instance)
344
345@@ -292,3 +324,90 @@
346 return False
347
348 return True
349+
350+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
351+ """Create a new keypair, or return pointer if it already exists."""
352+ try:
353+ _keypair = nova.keypairs.get(keypair_name)
354+ self.log.debug('Keypair ({}) already exists, '
355+ 'using it.'.format(keypair_name))
356+ return _keypair
357+ except:
358+ self.log.debug('Keypair ({}) does not exist, '
359+ 'creating it.'.format(keypair_name))
360+
361+ _keypair = nova.keypairs.create(name=keypair_name)
362+ return _keypair
363+
364+ def delete_resource(self, resource, resource_id,
365+ msg="resource", max_wait=120):
366+ """Delete one openstack resource, such as one instance, keypair,
367+ image, volume, stack, etc., and confirm deletion within max wait time.
368+
369+ :param resource: pointer to os resource type, ex:glance_client.images
370+ :param resource_id: unique name or id for the openstack resource
371+ :param msg: text to identify purpose in logging
372+ :param max_wait: maximum wait time in seconds
373+ :returns: True if successful, otherwise False
374+ """
375+ num_before = len(list(resource.list()))
376+ resource.delete(resource_id)
377+
378+ tries = 0
379+ num_after = len(list(resource.list()))
380+ while num_after != (num_before - 1) and tries < (max_wait / 4):
381+ self.log.debug('{} delete check: '
382+ '{} [{}:{}] {}'.format(msg, tries,
383+ num_before,
384+ num_after,
385+ resource_id))
386+ time.sleep(4)
387+ num_after = len(list(resource.list()))
388+ tries += 1
389+
390+ self.log.debug('{}: expected, actual count = {}, '
391+ '{}'.format(msg, num_before - 1, num_after))
392+
393+ if num_after == (num_before - 1):
394+ return True
395+ else:
396+ self.log.error('{} delete timed out'.format(msg))
397+ return False
398+
399+ def resource_reaches_status(self, resource, resource_id,
400+ expected_stat='available',
401+ msg='resource', max_wait=120):
402+ """Wait for an openstack resources status to reach an
403+ expected status within a specified time. Useful to confirm that
404+ nova instances, cinder vols, snapshots, glance images, heat stacks
405+ and other resources eventually reach the expected status.
406+
407+ :param resource: pointer to os resource type, ex: heat_client.stacks
408+ :param resource_id: unique id for the openstack resource
409+ :param expected_stat: status to expect resource to reach
410+ :param msg: text to identify purpose in logging
411+ :param max_wait: maximum wait time in seconds
412+ :returns: True if successful, False if status is not reached
413+ """
414+
415+ tries = 0
416+ resource_stat = resource.get(resource_id).status
417+ while resource_stat != expected_stat and tries < (max_wait / 4):
418+ self.log.debug('{} status check: '
419+ '{} [{}:{}] {}'.format(msg, tries,
420+ resource_stat,
421+ expected_stat,
422+ resource_id))
423+ time.sleep(4)
424+ resource_stat = resource.get(resource_id).status
425+ tries += 1
426+
427+ self.log.debug('{}: expected, actual status = {}, '
428+ '{}'.format(msg, resource_stat, expected_stat))
429+
430+ if resource_stat == expected_stat:
431+ return True
432+ else:
433+ self.log.debug('{} never reached expected status: '
434+ '{}'.format(resource_id, expected_stat))
435+ return False
436
437=== modified file 'charmhelpers/contrib/openstack/utils.py'
438--- charmhelpers/contrib/openstack/utils.py 2015-05-11 18:53:44 +0000
439+++ charmhelpers/contrib/openstack/utils.py 2015-06-11 19:47:33 +0000
440@@ -79,6 +79,7 @@
441 ('trusty', 'icehouse'),
442 ('utopic', 'juno'),
443 ('vivid', 'kilo'),
444+ ('wily', 'liberty'),
445 ])
446
447
448@@ -91,6 +92,7 @@
449 ('2014.1', 'icehouse'),
450 ('2014.2', 'juno'),
451 ('2015.1', 'kilo'),
452+ ('2015.2', 'liberty'),
453 ])
454
455 # The ugly duckling
456@@ -113,6 +115,7 @@
457 ('2.2.0', 'juno'),
458 ('2.2.1', 'kilo'),
459 ('2.2.2', 'kilo'),
460+ ('2.3.0', 'liberty'),
461 ])
462
463 DEFAULT_LOOPBACK_SIZE = '5G'
464@@ -321,6 +324,9 @@
465 'kilo': 'trusty-updates/kilo',
466 'kilo/updates': 'trusty-updates/kilo',
467 'kilo/proposed': 'trusty-proposed/kilo',
468+ 'liberty': 'trusty-updates/liberty',
469+ 'liberty/updates': 'trusty-updates/liberty',
470+ 'liberty/proposed': 'trusty-proposed/liberty',
471 }
472
473 try:
474@@ -641,7 +647,8 @@
475 subprocess.check_call(cmd)
476 except subprocess.CalledProcessError:
477 package = os.path.basename(package_dir)
478- error_out("Error updating {} from global-requirements.txt".format(package))
479+ error_out("Error updating {} from "
480+ "global-requirements.txt".format(package))
481 os.chdir(orig_dir)
482
483

Subscribers

People subscribed via source and target branches