Merge lp:~brian-murray/ubuntu-archive-tools/phased-updater into lp:ubuntu-archive-tools

Proposed by Brian Murray
Status: Merged
Merged at revision: 744
Proposed branch: lp:~brian-murray/ubuntu-archive-tools/phased-updater
Merge into: lp:ubuntu-archive-tools
Diff against target: 522 lines (+518/-0)
1 file modified
phased-updater (+518/-0)
To merge this branch: bzr merge lp:~brian-murray/ubuntu-archive-tools/phased-updater
Reviewer Review Type Date Requested Status
Colin Watson Approve
Steve Langasek Needs Fixing
Review via email: mp+171142@code.launchpad.net

Description of the change

This new tool, phased-updater, will be the glue between Launchpad (for setting the phased updater percentage) and the Ubuntu Error Tracker (for finding regressions in package from -updates).

In the event that no regressions (defined as errors about the version of the package in -updates that were not seen in the previous version, or an increased rate of crashes) are found about the package the phased-update-percentage for the package will be incremented by 10%. If regressions are found the phased-update-percentage for the package will be set to 0%. There is also a mechanism for indicating that individual buckets are not regressions in a particular version of the package.

In either case an html report (sample visible at http://people.canonical.com/~brian/tmp/phased-updates.html) will be generated that will display the phased update percentage for the package and any issues found with that package. This will also be useful for seeing which packages are currently undergoing phasing.

In the event that a regression is found about a package in -updates the uploader and signer (if different from the uploader) wil be emailed of the regression or regressions in that upload. There is a csv file used to keep track of email notifications sent to uploaders to ensure that we do not email them multiple times about the same issue.

Its possible to test this on dogfood by initially setting the phased update percentage to something less than one hundred percent and observe the script increment the phased update percentage. However, this needs to be done using quantal and a start date in early May as the database for dogfood is rather old.

To post a comment you must log in.
758. By Brian Murray

log original phased update percentage

759. By Brian Murray

clarify last todo

Revision history for this message
Steve Langasek (vorlon) wrote :

+def set_pup(new_pup, suite, src_pkg):
+ import subprocess
+ with open(os.devnull, 'w') as fnull:
+ retcode = subprocess.call([
+ "%s/change-override" % os.getcwd(),
+ "--confirm-all",
+ "--percentage", '%s' % new_pup,
+ "--suite", suite,
+ "--source-and-binary", src_pkg,
+ "--launchpad", options.launchpad_instance],
+ stdout = fnull)
+ if retcode != 0:
+ logging.error("There was an error (%s) running change-override."
+ % retcode)
+ return retcode

os.getcwd() doesn't do what you want here in all cases. I, for example, have ubuntu-archive-tools on my path, so my getcwd() returns a location completely unrelated to where the tools are.

Instead, I think you want:

  os.path.dirname(sys.argv[0]) or "."

The other use of os.getcwd() seem fine, as they refer to the output directory and I think it's reasonable to use the current dir as the convention for that. But in production, I think we aren't going to be writing the report to the checkout dir.

Otherwise, this looks good to me and ready to merge.

review: Needs Fixing
Revision history for this message
Colin Watson (cjwatson) wrote :

On Mon, Jun 24, 2013 at 10:54:25PM -0000, Steve Langasek wrote:
> os.getcwd() doesn't do what you want here in all cases. I, for example, have ubuntu-archive-tools on my path, so my getcwd() returns a location completely unrelated to where the tools are.
>
> Instead, I think you want:
>
> os.path.dirname(sys.argv[0]) or "."

I would prefer doing the override change directly using API calls,
rather than shelling out to change-override.

760. By Brian Murray

directly call changeOverride in the Launchpad API rather than using subprocess to call change-override

Revision history for this message
Brian Murray (brian-murray) wrote :

I've switched to using the API directly instead of using change-override which I believe addresses both comments about the merge proposal.

Revision history for this message
Colin Watson (cjwatson) wrote :

OK, thanks for that fix. Let's give this a go ...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'phased-updater'
--- phased-updater 1970-01-01 00:00:00 +0000
+++ phased-updater 2013-06-27 16:06:38 +0000
@@ -0,0 +1,518 @@
1#!/usr/bin/python
2
3# Copyright (C) 2013 Canonical Ltd.
4# Author: Brian Murray <brian.murray@canonical.com>
5
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; version 3 of the License.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18'''Increment the Phased-Update-Percentage for a package
19
20Check to see whether or not there is a regression (new crash bucket or
21increase rate of errors about a package) using errors.ubuntu.com and if
22not increment the Phased-Update-Percentage for the package.
23Additionally, generate an html report regarding state of phasing of
24packages and email uploaders regarding issues with their uploads.
25'''
26
27from __future__ import print_function
28
29import apt
30import csv
31import datetime
32import logging
33import os
34import simplejson as json
35import time
36
37from collections import defaultdict, OrderedDict
38from optparse import OptionParser
39
40import lputils
41
42try:
43 from urllib.parse import quote
44 from urllib.request import urlopen
45except ImportError:
46 from urllib import quote, urlopen
47
48from launchpadlib.launchpad import Launchpad
49
50def get_primary_email(lp_user):
51 try:
52 lp_user_email = lp_user.preferred_email_address.email
53 except ValueError as e:
54 if 'server-side permission' in e.message:
55 logging.info("%s has hidden their email addresses" %
56 lp_user.web_link)
57 return ''
58 logging.info("Error accessing %s's preferred email address: %s" %
59 (lp_user.web_link, e.message))
60 return ''
61 return lp_user_email
62
63def set_pup(current_pup, new_pup, release, suite, src_pkg):
64 options.series = release
65 options.suite = suite
66 options.pocket = 'Updates'
67 options.version = None
68 source = lputils.find_latest_published_source(options, src_pkg)
69 publications = [binary for binary in source.getPublishedBinaries()]
70
71 for pub in publications:
72 pub.changeOverride(new_phased_update_percentage=new_pup)
73 if new_pup != 0:
74 logging.info('Incremented p-u-p for %s %s from %s%% to %s%%' %
75 (suite, pub.binary_package_name, current_pup, new_pup))
76 else:
77 logging.info('Set p-u-p to 0%% from %s%% for %s %s'
78 % (current_pup, suite, pub.binary_package_name))
79
80def generate_html_report(releases, buckets):
81 import tempfile
82 import shutil
83 with tempfile.NamedTemporaryFile() as report:
84 report.write('''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
85 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
86<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
87<head>
88 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
89 <title>Released Ubuntu SRUs</title>
90 <style type="text/css">
91 body { background: #CCCCB0; color: black; }
92 a { text-decoration: none; }
93 table { border-collapse: collapse; border-style: solid none;
94 border-width: 3px; margin-bottom: 3ex; empty-cells: show; }
95 table th { text-align: left; border-style: none none dotted none;
96 border-width: 1px; padding-right: 10px; }
97 table td { text-align: left; border-style: none none dotted none;
98 border-width: 1px; padding-right: 10px; }
99 .noborder { border-style: none; }
100 a { color: blue; }
101 a:visited { color: black; }
102 </style>
103</head>
104<body>
105<h1>Phasing %sUbuntu Stable Release Updates</h1>
106''' % ('', 'and Released ')[options.fully_phased])
107 report.write('<p>Generated: %s by '
108 '<a href="http://bazaar.launchpad.net/'
109 '~ubuntu-archive/ubuntu-archive-tools/trunk/annotate/head%%3A/'
110 'phased-updater">phased-updater</a></p>' %
111 time.strftime('%x %X UTC', time.gmtime()))
112 report.write('''<p>A <a
113href="https://wiki.ubuntu.com/StableReleaseUpdates ">stable release
114update</a> has been created for the following packages, i. e. they have
115a new version in -updates, and either an increased rate of crashes has
116been detected or an error has been found that only exists with the new
117version of the package.\n''')
118 for release in releases:
119 rname = release.name
120 if not buckets[rname]:
121 continue
122 report.write('''<h3>%s</h3>\n''' % rname)
123 report.write('''<table>\n''')
124 report.write('''<tr>
125 <th>Package</th>
126 <th>Version</th>
127 <th>Update Percentage</th>
128 <th>Rate Increase</th>
129 <th>Problems</th>
130 <th>Days</th>
131 </tr>''')
132 for pub_source in buckets[rname]:
133 pkg = pub_source.source_package_name
134 version = pub_source.source_package_version
135 age = (datetime.datetime.now() -
136 pub_source.date_published.replace(tzinfo=None)).days
137 update_percentage = buckets[rname][pub_source].get('pup',
138 100)
139 if not options.fully_phased and update_percentage == 100:
140 continue
141 lpurl = '%s/ubuntu/+source/%s/' % (LP_BASE_URL, pkg)
142 report.write('''<tr>
143 <td><a href="%s">%s</a></td>
144 <td><a href="%s">%s</a></td>\n''' %
145 (lpurl, pkg, lpurl + version, version))
146 report.write(' <td>')
147 if update_percentage == 0:
148 binary_pub = pub_source.getPublishedBinaries()[0]
149 arch = binary_pub.distro_arch_series.architecture_tag
150 bpph_url = ('%s/ubuntu/%s/%s/%s'
151 % (LP_BASE_URL, rname, arch,
152 binary_pub.binary_package_name))
153 report.write('<a href="%s">%s%% of users' %
154 (bpph_url, update_percentage))
155 previous_pup = \
156 buckets[rname][pub_source]['previous_pup']
157 if previous_pup != 0:
158 report.write(' (was %s%%)</a>' % previous_pup)
159 else:
160 report.write('</a>')
161 else:
162 report.write('%s%% of users' % update_percentage)
163 report.write('</td>\n')
164 if 'rate' in buckets[rname][pub_source]:
165 data = buckets[rname][pub_source]['rate']
166 report.write(' <td><a href="%s">+%s</a></td>\n' %
167 (data[1], data[0]))
168 else:
169 report.write(' <td></td>\n')
170 report.write(' <td>')
171 if 'buckets' in buckets[rname][pub_source]:
172 for bucket in buckets[rname][pub_source]['buckets']:
173 if 'problem' in bucket:
174 # create a short version of the problem's hash
175 phash = bucket.replace(
176 'https://errors.ubuntu.com/problem/', '')[0:6]
177 report.write('<a href="%s">%s</a> ' % (bucket,
178 phash))
179 else:
180 report.write('<a href="%s">problem</a> ' % bucket)
181 else:
182 report.write('')
183 report.write('</td>\n')
184 report.write(' <td>%s</td>\n' % age)
185 report.write('</tr>\n')
186 report.write('''</table>\n''')
187 report.write('''</body>\n''')
188 report.write('''</html>''')
189 report.flush()
190 shutil.copy2(report.name, '%s/phased-updates.html' % os.getcwd())
191
192def create_email_notifications(releases, spph_buckets):
193 import smtplib
194 from email.mime.text import MIMEText
195 with open(NOTIFICATIONS, 'r') as notify_file:
196 notify_csv = csv.reader(notify_file)
197 notifications = defaultdict(list)
198 for row in notify_csv:
199 # LP name, problem, pkg_version
200 person = row[0].strip()
201 problem = row[1].strip()
202 pkg = row[2].strip()
203 pkg_version = row[3].strip()
204 notifications[person].append((problem, pkg, pkg_version))
205 b_body = ('Your upload of %s version %s to %s has resulted in %s'
206 'error%s that %s first reported about this version of the '
207 'package. The error%s follow%s:\n\n'
208 '%s\n\n')
209 i_body = ('Your upload of %s version %s to %s has resulted in an '
210 'increased daily rate of errors for the package compared '
211 'to the previous two weeks. For problems currently being '
212 'reported about the package see:\n\n'
213 '%s\n\n')
214 remedy = ('Further phasing of this update has been stopped until the '
215 'errors have either been fixed or determined to not be a '
216 'result of this Stable Release Update. In the event of '
217 'the latter please let a member of the Ubuntu Stable Release '
218 'Updates team (~ubuntu-sru) know so that phasing of the update '
219 'can proceed.')
220 for release in releases:
221 rname = release.name
222 for spph in spph_buckets[rname]:
223 signer = spph.package_signer
224 # not an active user of Launchpad
225 if not signer.is_valid:
226 logging.info('%s not mailed as they are not a valid LP user'
227 % signer)
228 continue
229 signer_email = get_primary_email(signer)
230 if not signer_email:
231 continue
232 pkg = spph.source_package_name
233 version = spph.source_package_version
234 signer_name = signer.name
235 changer = spph.package_creator
236 if 'buckets' in spph_buckets[rname][spph]:
237 # see if they've been emailed about the bucket before
238 notices = []
239 if signer_name in notifications:
240 notices = notifications[signer_name]
241 for notice, notified_pkg, notified_version in notices:
242 if notice in spph_buckets[rname][spph]['buckets']:
243 if (notified_pkg != pkg and
244 notified_version != version):
245 continue
246 spph_buckets[rname][spph]['buckets'].remove(notice)
247 if len(spph_buckets[rname][spph]['buckets']) == 0:
248 continue
249 receivers = []
250 quantity = len(spph_buckets[rname][spph]['buckets'])
251 msg = MIMEText(b_body % (pkg, version, rname,
252 ('an ', '')[quantity != 1], ('', 's')[quantity != 1],
253 ('was', 'were')[quantity != 1],
254 ('', 's')[quantity != 1], ('s', '')[quantity != 1],
255 '\n'.join(spph_buckets[rname][spph]['buckets'])) + remedy)
256 subject = '[%s/%s] Possible Regression' % (rname, pkg)
257 msg['Subject'] = subject
258 msg['From'] = EMAIL_SENDER
259 receivers.append(signer_email)
260 msg['To'] = signer_email
261 if changer != signer and changer.is_valid:
262 changer_email = get_primary_email(changer)
263 if changer_email:
264 receivers.append(changer_email)
265 msg['Cc'] = '%s' % changer_email
266 smtp = smtplib.SMTP('localhost')
267 smtp.sendmail(EMAIL_SENDER, receivers,
268 msg.as_string())
269 logging.info('%s mailed about %s' % (receivers, subject))
270 smtp.quit()
271 # add signer, problem, pkg, version to notifications csv file
272 with open(NOTIFICATIONS, 'a') as notify_file:
273 for bucket in spph_buckets[rname][spph]['buckets']:
274 notify_file.write('%s, %s, %s, %s\n' % (signer.name,
275 bucket, pkg, version))
276 if 'rate' in spph_buckets[rname][spph]:
277 # see if they have been emailed about the increased rate
278 # for this package version before
279 notices = []
280 if signer_name in notifications:
281 notices = notifications[signer_name]
282 if ('increased-rate', pkg, version) in notices:
283 continue
284 receivers = []
285 msg = MIMEText(i_body % (pkg, version, rname,
286 spph_buckets[rname][spph]['rate'][1]) + remedy)
287 subject = '[%s/%s] Increase in crash rate' % (rname, pkg)
288 msg['Subject'] = subject
289 msg['From'] = EMAIL_SENDER
290 receivers.append(signer_email)
291 msg['To'] = signer_email
292 if changer != signer and changer.is_valid:
293 changer_email = get_primary_email(changer)
294 if changer_email:
295 receivers.append(changer_email)
296 msg['Cc'] = '%s' % changer_email
297 smtp = smtplib.SMTP('localhost')
298 smtp.sendmail(EMAIL_SENDER, receivers,
299 msg.as_string())
300 logging.info('%s mailed about %s' % (receivers, subject))
301 # add signer, increased-rate, pkg, version to
302 # notifications csv
303 with open(NOTIFICATIONS, 'a') as notify_file:
304 notify_file.write('%s, increased-rate, %s, %s\n' %
305 (signer.name, pkg, version))
306 smtp.quit()
307
308def new_buckets(archive, release, src_pkg, version):
309 # can't use created_since here because it have may been uploaded
310 # before the release date
311 spph = archive.getPublishedSources(distro_series=release,
312 source_name=src_pkg, exact_match=True)
313 pubs = [(ph.date_published, ph.source_package_version) for ph in spph
314 if ph.status != 'Deleted' and ph.pocket != 'Backports'
315 and ph.pocket != 'Proposed'
316 and ph.date_published is not None]
317 pubs = sorted(pubs)
318 # it is possible for the same version to appear multiple times
319 numbers = set([pub[1] for pub in pubs])
320 versions = sorted(numbers, cmp=apt.apt_pkg.version_compare)
321 # it never appeared in release e.g. cedarview-drm-drivers in precise
322 try:
323 previous_version = versions[-2]
324 except IndexError:
325 return False
326 new_version = versions[-1]
327 new_buckets_url = '%spackage-version-new-buckets/?format=json&' % \
328 (BASE_ERRORS_URL) + \
329 'package=%s&previous_version=%s&new_version=%s' % \
330 (quote(src_pkg), quote(previous_version), quote(new_version))
331 new_buckets_file = urlopen(new_buckets_url)
332 # LP: #1193022 - 404 returned when a package version is not found
333 if new_buckets_file.getcode() == 404:
334 logging.error('404 with %s' % new_buckets_url)
335 return False
336 try:
337 new_buckets_data = json.load(new_buckets_file)
338 except json.decoder.JSONDecodeError:
339 logging.error("Error decoding %s" % new_buckets_url)
340 return False
341 if 'error_message' in new_buckets_data.keys():
342 logging.error('Error getting new buckets at %s' % new_buckets_url)
343 return False
344 if len(new_buckets_data['objects']) == 0:
345 return False
346 buckets = []
347 for bucket in new_buckets_data['objects']:
348 # In the event that package install failures are not useful
349 #if 'dpkg: error' in bucket['function']:
350 # continue
351 # Skip failed buckets as they don't have useful tracebacks
352 if 'failed:' in bucket['function']:
353 logging.info('Skipped failed to retrace bucket %s' %
354 bucket['web_link'])
355 continue
356 buckets.append(bucket['web_link'])
357 return buckets
358
359def crash_rate_increase(release_version, src_pkg, version):
360 release = 'Ubuntu ' + release_version
361 # Is this not being package version specific a problem?
362 rate_url = BASE_ERRORS_URL + 'package-rate-of-crashes/?format=json' + \
363 '&release=%s&package=%s' % \
364 (quote(release), quote(src_pkg))
365 rate_file = urlopen(rate_url)
366 # LP: #1193022 - 404 returned when a package version is not found
367 if rate_file.getcode() == 404:
368 logging.error('404 with %s' % rate_url)
369 return False
370 rate_data = json.load(rate_file)
371 if 'error_message' in rate_data.keys():
372 logging.error('Error getting rate at %s' % rate_url)
373 return False
374 # this may not be useful if the buckets creating the increase have
375 # failed to retrace
376 for data in rate_data['objects']:
377 if data['increase']:
378 previous_amount = data['previous_average']
379 # appears in errors r439
380 if 'difference' in data:
381 increase = data['difference']
382 elif 'this_count' in data:
383 # 2013-06-17 this can be negative due to the portion of the
384 # day math (we take the average crashes and multiple them by
385 # the fraction of hours that have passed so far in the day)
386 current_amount = data['this_count']
387 increase = current_amount - previous_amount
388 # 2013-06-20 - Fix errors api to add this
389 link = data['web_link'] + '&version=%s' % version
390 return(increase, link)
391
392def main():
393 # TODO: make email code less redundant
394 # TODO: Determine how to override an increased rate
395 # TODO: modify HTTP_USER_AGENT (both versions of urllib)
396 # TODO: Open bugs for regressions when false positives reduced
397 # TODO: determine if starting p-u-p over from 0% is an issue
398 ubuntu = launchpad.distributions['ubuntu']
399 archive = ubuntu.getArchive(name='primary')
400 options.archive = archive
401
402 overrides = defaultdict(list)
403 override_file = csv.reader(open(OVERRIDES, 'r'))
404 for row in override_file:
405 # package, version, problem
406 if row[0].startswith('#'):
407 continue
408 package = row[0].strip()
409 version = row[1].strip()
410 problem = row[2].strip()
411 overrides[(package, version)].append(problem)
412
413 releases = []
414 for series in ubuntu.series:
415 if series.active:
416 if series.status == 'Active Development':
417 continue
418 releases.append(series)
419 releases.reverse()
420 issues = {}
421 for release in releases:
422 rname = release.name
423 rvers = release.version
424 issues[rname] = OrderedDict()
425 # XXX - starting with raring
426 if rname != 'raring':
427 continue
428 # XXX - only look at updates since 21/06/2013
429 start_date = datetime.datetime(2013, 6, 21, 0, 0, 0, 0)
430 pub_sources = archive.getPublishedSources(
431 created_since_date=start_date,
432 pocket='Updates', status='Published', distro_series=release)
433 sorted_pub_sources = sorted([(ps.date_published, ps)
434 for ps in pub_sources if ps.date_published is not None])
435 for date, pub_source in sorted_pub_sources:
436 src_pkg = pub_source.source_package_name
437 version = pub_source.source_package_version
438 problems = new_buckets(archive, release, src_pkg, version)
439 if problems:
440 if (src_pkg, version) in overrides:
441 not_overrode = set(problems).difference(
442 set(overrides[(src_pkg, version)]))
443 if len(not_overrode) > 0:
444 issues[rname][pub_source] = {}
445 issues[rname][pub_source]['buckets'] = not_overrode
446 else:
447 issues[rname][pub_source] = {}
448 issues[rname][pub_source]['buckets'] = problems
449 rate_increase = crash_rate_increase(rvers, src_pkg, version)
450 if rate_increase:
451 if pub_source not in issues[rname]:
452 issues[rname][pub_source] = {}
453 issues[rname][pub_source]['rate'] = rate_increase
454 pups = [pb.phased_update_percentage
455 for pb in pub_source.getPublishedBinaries()
456 if pb.phased_update_percentage]
457 if pups:
458 if pub_source not in issues[rname]:
459 issues[rname][pub_source] = {}
460 issues[rname][pub_source]['pup'] = pups[0]
461 suite = rname + '-updates'
462 if pub_source not in issues[rname]:
463 continue
464 elif ('rate' not in issues[rname][pub_source] and
465 'buckets' not in issues[rname][pub_source] and
466 pups):
467 # there is not an error so increment the phasing
468 current_pup = issues[rname][pub_source]['pup']
469 new_pup = current_pup + PUP_INCREMENT
470 if not options.no_act:
471 set_pup(current_pup, new_pup, release, suite, src_pkg)
472 issues[rname][pub_source]['pup'] = new_pup
473 elif pups:
474 # there is an error and pup is not None so stop the phasing
475 current_pup = issues[rname][pub_source]['pup']
476 issues[rname][pub_source]['previous_pup'] = current_pup
477 new_pup = 0
478 if (not options.no_act and
479 issues[rname][pub_source]['pup'] != 0):
480 set_pup(current_pup, new_pup, release, suite, src_pkg)
481 issues[rname][pub_source]['pup'] = new_pup
482 generate_html_report(releases, issues)
483 if options.email:
484 create_email_notifications(releases, issues)
485
486if __name__ == '__main__':
487 start_time = time.time()
488 BASE_ERRORS_URL = 'https://errors.ubuntu.com/api/1.0/'
489 LOCAL_ERRORS_URL = 'http://10.0.3.182/api/1.0/'
490 LP_BASE_URL = 'https://launchpad.net'
491 OVERRIDES = 'phased-updates-overrides.txt'
492 NOTIFICATIONS = 'phased-updates-emails.txt'
493 EMAIL_SENDER = 'brian.murray@ubuntu.com'
494 PUP_INCREMENT = 10
495 parser = OptionParser(usage = "usage: %prog [options]")
496 parser.add_option(
497 "-l", "--launchpad", dest="launchpad_instance", default="production")
498 parser.add_option(
499 "-n", "--no-act", default=False, action="store_true",
500 help="do not modify phased update percentages")
501 parser.add_option(
502 "-e", "--email", default=False, action="store_true",
503 help="send email notifications to uploaders")
504 parser.add_option(
505 "-f", "--fully-phased", default=False, action="store_true",
506 help="show packages which have been fully phased")
507 options, args = parser.parse_args()
508 if options.launchpad_instance != 'production':
509 LP_BASE_URL = 'https://%s.launchpad.net' % options.launchpad_instance
510 launchpad = Launchpad.login_with(
511 'phased-updater', options.launchpad_instance, version='devel')
512 logging.basicConfig(filename='phased-updates.log',
513 format='%(asctime)s - %(levelname)s - %(message)s',
514 level=logging.INFO)
515 logging.info('Starting phased-updater')
516 main()
517 end_time = time.time()
518 logging.info("Elapsed time was %g seconds" % (end_time - start_time))

Subscribers

People subscribed via source and target branches