~costamagnagianfranco/ubuntu-archive-tools/sync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
#!/usr/bin/python2.7

# Copyright (C) 2013 Canonical Ltd.
# Author: Brian Murray <brian.murray@canonical.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

'''Increment the Phased-Update-Percentage for a package

Check to see whether or not there is a regression (new crash bucket or
increase in rate of errors about a package) using errors.ubuntu.com and if
not increment the Phased-Update-Percentage for the package.
Additionally, generate an html report regarding state of phasing of
packages and email uploaders regarding issues with their uploads.
'''

from __future__ import print_function

import apt
import codecs
import csv
import datetime
import lazr
import logging
import os
import simplejson as json
import time

from collections import defaultdict, OrderedDict
from email import utils
from functools import cmp_to_key
from optparse import OptionParser

import lputils

try:
    from urllib.parse import quote
    from urllib.request import urlopen
except ImportError:
    from urllib import quote, urlopen

from launchpadlib.launchpad import Launchpad


def get_primary_email(lp_user):
    try:
        lp_user_email = lp_user.preferred_email_address.email
    except ValueError as e:
        if 'server-side permission' in e.message:
            logging.info("%s has hidden their email addresses" %
                         lp_user.web_link)
            return ''
        logging.info("Error accessing %s's preferred email address: %s" %
                     (lp_user.web_link, e.message))
        return ''
    return lp_user_email


def set_pup(current_pup, new_pup, release, suite, src_pkg):
    options.series = release
    options.suite = suite
    options.pocket = 'Updates'
    options.version = None
    source = lputils.find_latest_published_source(options, src_pkg)
    publications = [
        binary for binary in source.getPublishedBinaries()
        if not binary.is_debug]

    for pub in publications:
        if pub.status != 'Published':
            continue
        pub.changeOverride(new_phased_update_percentage=new_pup)
        if new_pup != 0:
            logging.info('Incremented p-u-p for %s %s from %s%% to %s%%' %
                         (suite, pub.binary_package_name,
                          current_pup, new_pup))
        else:
            logging.info('Set p-u-p to 0%% from %s%% for %s %s' %
                         (current_pup, suite, pub.binary_package_name))


def generate_html_report(releases, buckets):
    import tempfile
    import shutil
    with tempfile.NamedTemporaryFile() as report:
        report.write('''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Released Ubuntu SRUs</title>
  <style type="text/css">
    body { background: #CCCCB0; color: black; }
    a { text-decoration: none; }
    table { border-collapse: collapse; border-style: solid none;
            border-width: 3px; margin-bottom: 3ex; empty-cells: show; }
    table th { text-align: left; border-style: none none dotted none;
               border-width: 1px; padding-right: 10px; }
    table td { text-align: left; border-style: none none dotted none;
               border-width: 1px; padding-right: 10px; }
    .noborder { border-style: none; }
    a { color: blue; }
    a:visited { color: black; }
  </style>
</head>
<body>
<h1>Phasing %sUbuntu Stable Release Updates</h1>
''' % ('', 'and Released ')[options.fully_phased])
        report.write(
            '<p>Generated: %s by '
            '<a href="http://bazaar.launchpad.net/'
            '~ubuntu-archive/ubuntu-archive-tools/trunk/annotate/head%%3A/'
            'phased-updater">phased-updater</a></p>' %
            time.strftime('%F %T UTC', time.gmtime()))
        report.write('''<p>A <a
href="https://wiki.ubuntu.com/StableReleaseUpdates">stable release
update</a> is either being phased (update percentage is not 0%) or phasing has
been stopped (update percentage is 0%) for the following packages and releases.
<p>The phasing stops if there is either an increased rate of crashes or an
error has been found that has only been seen with the SRU'ed version of the
package.<p>
<a href="https://wiki.ubuntu.com/StableReleaseUpdates#Phasing">Learn more</a>
about investigating halted phased updates.''')
        for release in releases:
            rname = release.name
            if not buckets[rname]:
                continue
            report.write('''<h3>%s</h3>\n''' % rname)
            report.write('''<table>\n''')
            report.write('''<tr>
      <th>Package</th>
      <th>Version (signer, creator)</th>
      <th>Update Percentage</th>
      <th>Rate Increase</th>
      <th>Errors</th>
      <th>Days</th>
    </tr>''')
            for pub_source in buckets[rname]:
                pkg = pub_source.source_package_name
                version = pub_source.source_package_version
                # Identify security updates
                if options.fully_phased:
                    sec_pubs = [(ps.source_package_name,
                                 ps.source_package_version)
                                for ps in archive.getPublishedSources(
                                distro_series=release, pocket='Security',
                                source_name=pkg, version=version,
                                exact_match=True)]
                    if (pkg, version) in sec_pubs:
                        version += ' (security)'
                age = (datetime.datetime.now() -
                       pub_source.date_published.replace(tzinfo=None)).days
                update_percentage = buckets[rname][pub_source].get('pup', 100)
                if not options.fully_phased and update_percentage == 100:
                    continue
                signer = str(pub_source.package_signer).split('~')[-1]
                uploaders = '<a href="%s/~%s">%s</a>' % \
                    (LP_BASE_URL, signer, signer)
                if pub_source.package_creator \
                        and pub_source.package_creator != pub_source.package_signer:
                    creator = str(pub_source.package_creator).split('~')[-1]
                    uploaders += ', <a href="%s/~%s">%s</a>' % \
                        (LP_BASE_URL, creator, creator)
                lpurl = '%s/ubuntu/+source/%s/' % (LP_BASE_URL, pkg)
                report.write('''<tr>
      <td><a href="%s">%s</a></td>
      <td><a href="%s">%s</a> (%s)</td>\n''' %
                             (lpurl, pkg, lpurl + version, version, uploaders))
                report.write('  <td>')
                if update_percentage == 0:
                    binary_pub = pub_source.getPublishedBinaries()[0]
                    arch = binary_pub.distro_arch_series.architecture_tag
                    bpph_url = ('%s/ubuntu/%s/%s/%s' %
                                (LP_BASE_URL, rname, arch,
                                 binary_pub.binary_package_name))
                    report.write('<a href="%s">%s%% of users' %
                                 (bpph_url, update_percentage))
                    previous_pup = \
                        buckets[rname][pub_source]['previous_pup']
                    if previous_pup != 0:
                        report.write(' (was %s%%)</a>' % previous_pup)
                    else:
                        report.write('</a>')
                else:
                    report.write('%s%% of users' % update_percentage)
                report.write('</td>\n')
                if 'rate' in buckets[rname][pub_source]:
                    data = buckets[rname][pub_source]['rate']
                    report.write('  <td><a href="%s">+%s</a></td>\n' %
                                 (data[1], data[0]))
                else:
                    report.write('  <td></td>\n')
                report.write('  <td>')
                if 'buckets' in buckets[rname][pub_source]:
                    # TODO: it'd be great if these were sorted
                    for bucket in buckets[rname][pub_source]['buckets']:
                        if 'problem' in bucket:
                            # create a short version of the problem's hash
                            phash = bucket.replace(
                                'https://errors.ubuntu.com/problem/', '')[0:6]
                            report.write('<a href="%s">%s</a> ' % (bucket,
                                         phash))
                        else:
                            report.write('<a href="%s">problem</a> ' % bucket)
                else:
                    report.write('')
                report.write('</td>\n')
                report.write('  <td>%s</td>\n' % age)
                report.write('</tr>\n')
            report.write('''</table>\n''')
        report.write('''</body>\n''')
        report.write('''</html>''')
        report.flush()
        shutil.copy2(report.name, '%s/%s' % (os.getcwd(), REPORT_FILE))
        os.chmod('%s/%s' % (os.getcwd(), REPORT_FILE), 0o644)


def create_email_notifications(releases, spph_buckets):
    import smtplib
    from email.mime.text import MIMEText
    notifications = defaultdict(list)
    try:
        with codecs.open(NOTIFICATIONS, 'r', encoding='utf-8') as notify_file:
            for line in notify_file.readlines():
                line = line.strip('\n').split(', ')
                # LP name, problem, pkg_version
                person = line[0]
                problem = line[1]
                pkg = line[2]
                pkg_version = line[3]
                notifications[person].append((problem, pkg, pkg_version))
    except IOError:
        pass
    bdmurray_mail = 'brian@ubuntu.com'
    b_body = ('Your upload of %s version %s to %s has resulted in %s'
              'error%s[1] that %s first reported about this version of the '
              'package.  The error%s follow%s:\n\n'
              '%s\n\n')
    i_body = ('Your upload of %s version %s to %s has resulted in an '
              'increased daily rate of errors[1] for the package compared '
              'to the previous two weeks. For problems currently being '
              'reported about the package see:\n\n'
              '%s&period=week\n\n')
    remedy = ('The current status of the phasing of all stable release '
              'updates, including yours, is available at:\n\n'
              'http://people.canonical.com/~ubuntu-archive/%s\n\n'
              'Further phasing of this update has been stopped until the '
              'errors have either been fixed or determined[2] to not be a '
              'result of this stable release update.  In the event of '
              'the latter please let a member of the Ubuntu Stable Release '
              'Updates team[3] know so that phasing of the update can '
              'proceed.\n\n'
              '[1] Request access to the Ubuntu Error Tracker at '
              'https://forms.canonical.com/reports/.\n'
              '[2] Learn more about investigating at '
              'https://wiki.ubuntu.com/StableReleaseUpdates#Phasing\n' 
              '[3] https://launchpad.net/~ubuntu-sru' %
              (REPORT_FILE))
    for release in releases:
        rname = release.name
        for spph in spph_buckets[rname]:
            update_percentage = spph_buckets[rname][spph].get('pup', 100)
            # never send emails about updates that are fully phased
            if update_percentage == 100:
                continue
            if 'buckets' not in spph_buckets[rname][spph] and \
                    'rate' not in spph_buckets[rname][spph]:
                continue
            signer = spph.package_signer
            # copies of packages from debian won't have a signer
            if not signer:
                continue
            # not an active user of Launchpad
            if not signer.is_valid:
                logging.info('%s not mailed as they are not a valid LP user' %
                             signer)
                continue
            signer_email = get_primary_email(signer)
            signer_name = signer.name
            # use the changes file as a backup method for determining email addresses
            changes_file_url = spph.changesFileUrl()
            changer_name = ''
            changer_email = ''
            try:
                changes_file = urlopen(changes_file_url)
                for line in changes_file.readlines():
                    line = line.strip()
                    if line.startswith('Changed-By:'):
                        changer = line.lstrip('Changed-By: ').decode('utf-8')
                        changer_name, changer_email = utils.parseaddr(changer.strip())
                        break
            except IOError:
                pass
            creator = spph.package_creator
            creator_email = ''
            pkg = spph.source_package_name
            version = spph.source_package_version
            if not signer_email and signer_name == creator.name:
                if not changer_email:
                    logging.info("No contact email found for %s %s %s" %
                                 (rname, pkg, version))
                    continue
                signer_email = changer_email
                logging.info("Used changes file to find contact email for %s %s %s" %
                             (rname, pkg, version))
            if 'buckets' in spph_buckets[rname][spph]:
                # see if they've been emailed about the bucket before
                notices = []
                if signer_name in notifications:
                    notices = notifications[signer_name]
                for notice, notified_pkg, notified_version in notices:
                    if notice in spph_buckets[rname][spph]['buckets']:
                        if (notified_pkg != pkg and
                                notified_version != version):
                            continue
                        spph_buckets[rname][spph]['buckets'].remove(notice)
                if len(spph_buckets[rname][spph]['buckets']) == 0:
                    continue
                receivers = [bdmurray_mail]
                quantity = len(spph_buckets[rname][spph]['buckets'])
                msg = MIMEText(
                    b_body % (pkg, version, rname, ('an ', '')[quantity != 1],
                              ('', 's')[quantity != 1],
                              ('was', 'were')[quantity != 1],
                              ('', 's')[quantity != 1],
                              ('s', '')[quantity != 1],
                              '\n'.join(spph_buckets[rname][spph]['buckets']))
                    + remedy)
                subject = '[%s/%s] Package Phasing Stopped' % (rname, pkg)
                msg['Subject'] = subject
                msg['From'] = EMAIL_SENDER
                msg['Reply-To'] = bdmurray_mail
                receivers.append(signer_email)
                msg['To'] = signer_email
                if creator != signer and creator.is_valid:
                    creator_email = get_primary_email(creator)
                    # fall back to the email found in the changes file
                    if not creator_email:
                        creator_email = changer_email
                    receivers.append(creator_email)
                    msg['Cc'] = '%s' % changer_email
                smtp = smtplib.SMTP('localhost')
                smtp.sendmail(EMAIL_SENDER, receivers,
                              msg.as_string())
                smtp.quit()
                logging.info('%s mailed about %s' % (receivers, subject))
                # add signer, problem, pkg, version to notifications csv file
                with codecs.open(NOTIFICATIONS, 'a', encoding='utf-8') as notify_file:
                    for bucket in spph_buckets[rname][spph]['buckets']:
                        notify_file.write('%s, %s, %s, %s\n' % \
                                          (signer_name, bucket,
                                           pkg, version))
                        if changer_email:
                            notify_file.write('%s, %s, %s, %s\n' % \
                                              (creator.name, bucket,
                                               pkg, version))
            if 'rate' in spph_buckets[rname][spph]:
                # see if they have been emailed about the increased rate
                # for this package version before
                notices = []
                if signer_name in notifications:
                    notices = notifications[signer_name]
                if ('increased-rate', pkg, version) in notices:
                    continue
                receivers = [bdmurray_mail]
                msg = MIMEText(i_body % (pkg, quote(version), rname,
                                         spph_buckets[rname][spph]['rate'][1])
                               + remedy)
                subject = '[%s/%s] Package Phasing Stopped' % (rname, pkg)
                msg['Subject'] = subject
                msg['From'] = EMAIL_SENDER
                msg['Reply-To'] = bdmurray_mail
                receivers.append(signer_email)
                msg['To'] = signer_email
                if creator != signer and creator.is_valid:
                    # fall back to the email found in the changes file
                    if not creator_email:
                        creator_email = changer_email
                    receivers.append(creator_email)
                    msg['Cc'] = '%s' % creator_email
                smtp = smtplib.SMTP('localhost')
                smtp.sendmail(EMAIL_SENDER, receivers,
                              msg.as_string())
                smtp.quit()
                logging.info('%s mailed about %s' % (receivers, subject))
                # add signer, increased-rate, pkg, version to
                # notifications csv
                with codecs.open(NOTIFICATIONS, 'a', encoding='utf-8') as notify_file:
                    notify_file.write('%s, increased-rate, %s, %s\n' %
                                      (signer_name, pkg, version))
                    if creator_email:
                        notify_file.write('%s, increased-rate, %s, %s\n' %
                                          (creator.name, pkg, version))


def new_buckets(archive, release, src_pkg, version):
    # can't use created_since here because it have may been uploaded
    # before the release date
    spph = archive.getPublishedSources(distro_series=release,
                                       source_name=src_pkg, exact_match=True)
    pubs = [(ph.date_published, ph.source_package_version) for ph in spph
            if ph.status != 'Deleted' and ph.pocket != 'Backports'
            and ph.pocket != 'Proposed'
            and ph.date_published is not None]
    pubs = sorted(pubs)
    # it is possible for the same version to appear multiple times
    numbers = set([pub[1] for pub in pubs])
    versions = sorted(numbers, key=cmp_to_key(apt.apt_pkg.version_compare))
    # it never appeared in release e.g. cedarview-drm-drivers in precise
    try:
        previous_version = versions[-2]
    except IndexError:
        return False
    new_version = versions[-1]
    new_buckets_url = '%spackage-version-new-buckets/?format=json&' % \
        (BASE_ERRORS_URL) + \
        'package=%s&previous_version=%s&new_version=%s' % \
        (quote(src_pkg), quote(previous_version), quote(new_version))
    try:
        new_buckets_file = urlopen(new_buckets_url)
    except IOError:
        return 'error'
    # If we don't receive an OK response from the Error Tracker we should not
    # increment the phased-update-percentage.
    if new_buckets_file.getcode() != 200:
        logging.error('HTTP error retrieving %s' % new_buckets_url)
        return 'error'
    try:
        new_buckets_data = json.load(new_buckets_file)
    except json.decoder.JSONDecodeError:
        logging.error('Error getting new buckets at %s' % new_buckets_url)
        return 'error'
    if 'error_message' in new_buckets_data.keys():
        logging.error('Error getting new buckets at %s' % new_buckets_url)
        return 'error'
    if len(new_buckets_data['objects']) == 0:
        return False
    buckets = []
    for bucket in new_buckets_data['objects']:
        # Do not consider package install failures until they have more
        # information added to the instances.
        if bucket['function'].startswith('package:'):
            continue
        # 16.04's duplicate signature for ProblemType: Package doesn't
        # start with 'package:' so check for strings in the bucket.
        if 'is already installed and configured' in bucket['function']:
            logging.info('Skipped already installed bucket %s' %
                         bucket['web_link'])
            continue
        # Skip failed buckets as they don't have useful tracebacks
        if bucket['function'].startswith('failed:'):
            logging.info('Skipped failed to retrace bucket %s' %
                         bucket['web_link'])
            continue
        # check to see if the version appears for the affected release
        versions_url = '%sversions/?format=json&id=%s' % \
            ((BASE_ERRORS_URL) , quote(bucket['function'].encode('utf-8')))
        try:
            versions_data_file = urlopen(versions_url)
        except IOError:
            logging.error('Error getting release versions at %s' % versions_url)
            # don't return an error because its better to have a false positive
            # in this case
            buckets.append(bucket['web_link'])
            continue
        try:
            versions_data = json.load(versions_data_file)
        except json.decoder.JSONDecodeError:
            logging.error('Error getting release versions at %s' % versions_url)
            # don't return an error because its better to have a false positive
            # in this case
            buckets.append(bucket['web_link'])
            continue
        if 'error_message' in versions_data:
            # don't return an error because its better to have a false positive
            # in this case
            buckets.append(bucket['web_link'])
            continue
        # -1 means that release isn't affected
        if len([vd[release.name] for vd in versions_data['objects'] \
                if vd['version'] == new_version and vd[release.name] != -1]) == 0:
            continue
        buckets.append(bucket['web_link'])
    logging.info('Details (new buckets): %s' % new_buckets_url)
    return buckets


def package_previous_version(release, src_pkg, version):
    # return previous package version from updates or release and
    # the publication date of the current package version
    ubuntu = launchpad.distributions['ubuntu']
    primary = ubuntu.getArchive(name='primary')
    current_version_date = None
    previous_version = None
    # Archive.getPublishedSources returns results ordered by
    # (name, id) where the id number is autocreated, subsequently
    # the newest package versions are returned first
    for spph in primary.getPublishedSources(source_name=src_pkg,
                                            distro_series=release,
                                            exact_match=True):
        if spph.pocket == 'Proposed':
            continue
        if spph.status == 'Deleted':
            continue
        if spph.source_package_version == version:
            if not current_version_date:
                current_version_date = spph.date_published.date()
            elif spph.date_published.date() > current_version_date:
                current_version_date = spph.date_published.date()
        if spph.pocket == 'Updates' and spph.status == 'Superseded':
            return (spph.source_package_version, current_version_date)
        if spph.pocket == 'Release' and spph.status == 'Published':
            return (spph.source_package_version, current_version_date)
    return (None, None)


def crash_rate_increase(release, src_pkg, version, last_pup):
    pvers, date = package_previous_version(release, src_pkg, version)
    date = str(date).replace('-', '')
    if not pvers:
        # joyent-mdata-client was put in updates w/o being in the release
        # pocket
        return False
    release_name = 'Ubuntu ' + release.version
    rate_url = BASE_ERRORS_URL + 'package-rate-of-crashes/?format=json' + \
        '&exclude_proposed=True' + \
        '&release=%s&package=%s&old_version=%s&new_version=%s&phased_update_percentage=%s&date=%s' % \
        (quote(release_name), quote(src_pkg), quote(pvers), quote(version),
         last_pup, date)
    try:
        rate_file = urlopen(rate_url)
    except IOError:
        return 'error'
    # If we don't receive an OK response from the Error Tracker we should not
    # increment the phased-update-percentage.
    if rate_file.getcode() != 200:
        logging.error('HTTP error retrieving %s' % rate_url)
        return 'error'
    try:
        rate_data = json.load(rate_file)
    except json.decoder.JSONDecodeError:
        logging.error('Error getting rate at %s' % rate_url)
        return 'error'
    if 'error_message' in rate_data.keys():
        logging.error('Error getting rate at %s' % rate_url)
        return 'error'
    logging.info('Details (rate increase): %s' % rate_url)
    # this may not be useful if the buckets creating the increase have
    # failed to retrace
    for data in rate_data['objects']:
        if data['increase']:
            previous_amount = data['previous_average']
            # this may happen if there were no crashes reported about
            # the previous version of the package
            if not previous_amount:
                logging.info('No previous crash data found for %s %s' %
                             (src_pkg, pvers))
                previous_amount = 0
            if 'difference' in data:
                increase = data['difference']
            elif 'this_count' in data:
            # 2013-06-17 this can be negative due to the portion of the
            # day math (we take the average crashes and multiple them by
            # the fraction of hours that have passed so far in the day)
                current_amount = data['this_count']
                increase = current_amount - previous_amount
            logging.info('[%s/%s] increase: %s, previous_avg: %s' %
                         (release_name.replace('Ubuntu ', ''), src_pkg,
                          increase, previous_amount))
            if '&version=' not in data['web_link']:
                link = data['web_link'] + '&version=%s' % version
            else:
                link = data['web_link']
            logging.info('Details (rate increase): %s' % link)
            return(increase, link)


def main():
    #  TODO: make email code less redundant
    #  TODO: modify HTTP_USER_AGENT (both versions of urllib)
    #  TODO: Open bugs for regressions when false positives reduced
    options.archive = archive

    overrides = defaultdict(list)
    rate_overrides = []
    override_file = csv.reader(open(OVERRIDES, 'r'))
    for row in override_file:
        if len(row) < 3:
            continue
        # package, version, problem
        if row[0].startswith('#'):
            continue
        package = row[0].strip()
        version = row[1].strip()
        problem = row[2].strip()
        if problem == 'increased-rate':
            rate_overrides.append((package, version))
        else:
            overrides[(package, version)].append(problem)

    releases = []
    for series in ubuntu.series:
        if series.active:
            if series.status == 'Active Development':
                continue
            releases.append(series)
    releases.reverse()
    issues = {}
    for release in releases:
        # We can't use release.datereleased because some SRUs are 0 day
        cdate = release.date_created
        rname = release.name
        rvers = release.version
        issues[rname] = OrderedDict()
        # XXX - starting with raring
        if rname == 'precise':
            continue
        pub_sources = archive.getPublishedSources(
            created_since_date=cdate,
            order_by_date=True,
            pocket='Updates', status='Published', distro_series=release)
        for pub_source in pub_sources:
            src_pkg = pub_source.source_package_name
            version = pub_source.source_package_version
            pbs = None
            try:
                pbs = [pb for pb in pub_source.getPublishedBinaries()
                       if pb.phased_update_percentage is not None]
            # workaround for LP: #1695113
            except lazr.restfulclient.errors.ServerError as e:
                if 'HTTP Error 503' in str(e):
                    logging.info('Skipping 503 Error for %s' % src_pkg)
                    pass
            if not pbs:
                continue
            if pbs:
                # the p-u-p is currently the same for all binary packages
                last_pup = pbs[0].phased_update_percentage
            else:
                last_pup = None
            max_pup = 0
            if last_pup == 0:
                    for allpb in archive.getPublishedBinaries(
                            exact_match=True, pocket='Updates',
                            binary_name=pbs[0].binary_package_name):
                        if allpb.distro_arch_series.distroseries == release:
                            if allpb.phased_update_percentage > 0:
                                max_pup = allpb.phased_update_percentage
                                break
            if max_pup and last_pup == 0:
                rate_increase = crash_rate_increase(release, src_pkg, version, max_pup)
            else:
                rate_increase = crash_rate_increase(release, src_pkg, version, last_pup)
            problems = new_buckets(archive, release, src_pkg, version)
            # In the event that there as an error connecting to errors.ubuntu.com then
            # neither increase nor stop the phased-update.
            if rate_increase == 'error' or problems == 'error':
                logging.info("Skipping %s due to failure to get data from Errors." % src_pkg)
                continue
            if problems:
                if (src_pkg, version) in overrides:
                    not_overrode = set(problems).difference(
                        set(overrides[(src_pkg, version)]))
                    if len(not_overrode) > 0:
                        issues[rname][pub_source] = {}
                        issues[rname][pub_source]['buckets'] = not_overrode
                else:
                    issues[rname][pub_source] = {}
                    issues[rname][pub_source]['buckets'] = problems
            if rate_increase and (src_pkg, version) not in rate_overrides:
                if pub_source not in issues[rname]:
                    issues[rname][pub_source] = {}
                issues[rname][pub_source]['rate'] = rate_increase
            if pbs:
                if pub_source not in issues[rname]:
                    issues[rname][pub_source] = {}
                # phasing has stopped so check what the max value was
                if last_pup == 0:
                    issues[rname][pub_source]['max_pup'] = max_pup
                issues[rname][pub_source]['pup'] = last_pup
            suite = rname + '-updates'
            if pub_source not in issues[rname]:
                continue
            elif ('rate' not in issues[rname][pub_source] and
                  'buckets' not in issues[rname][pub_source] and
                  pbs):
                # there is not an error so increment the phasing
                current_pup = issues[rname][pub_source]['pup']
                # if this is an update that is restarting we want to start at
                # the same percentage the stoppage happened at
                if 'max_pup' in issues[rname][pub_source]:
                    current_pup = issues[rname][pub_source]['max_pup']
                new_pup = current_pup + PUP_INCREMENT
                if not options.no_act:
                    set_pup(current_pup, new_pup, release, suite, src_pkg)
                issues[rname][pub_source]['pup'] = new_pup
            elif pbs:
                # there is an error and pup is not None so stop the phasing
                current_pup = issues[rname][pub_source]['pup']
                if 'max_pup' in issues[rname][pub_source]:
                    issues[rname][pub_source]['previous_pup'] = \
                        issues[rname][pub_source]['max_pup']
                else:
                    issues[rname][pub_source]['previous_pup'] = \
                        current_pup
                new_pup = 0
                if (not options.no_act and
                        issues[rname][pub_source]['pup'] != 0):
                    set_pup(current_pup, new_pup, release, suite, src_pkg)
                issues[rname][pub_source]['pup'] = new_pup
    generate_html_report(releases, issues)
    if options.email:
        create_email_notifications(releases, issues)

if __name__ == '__main__':
    start_time = time.time()
    BASE_ERRORS_URL = 'https://errors.ubuntu.com/api/1.0/'
    LOCAL_ERRORS_URL = 'http://10.0.3.182/api/1.0/'
    LP_BASE_URL = 'https://launchpad.net'
    OVERRIDES = 'phased-updates-overrides.txt'
    NOTIFICATIONS = 'phased-updates-emails.txt'
    EMAIL_SENDER = 'brian.murray@ubuntu.com'
    PUP_INCREMENT = 10
    REPORT_FILE = 'phased-updates.html'
    parser = OptionParser(usage="usage: %prog [options]")
    parser.add_option(
        "-l", "--launchpad", dest="launchpad_instance", default="production")
    parser.add_option(
        "-n", "--no-act", default=False, action="store_true",
        help="do not modify phased update percentages")
    parser.add_option(
        "-e", "--email", default=False, action="store_true",
        help="send email notifications to uploaders")
    parser.add_option(
        "-f", "--fully-phased", default=False, action="store_true",
        help="show packages which have been fully phased")
    options, args = parser.parse_args()
    if options.launchpad_instance != 'production':
        LP_BASE_URL = 'https://%s.launchpad.net' % options.launchpad_instance
    if options.email and options.fully_phased:
        parser.error("Sending emails and fully phased updates are mutually \
exclusive.")
    launchpad = Launchpad.login_with(
        'phased-updater', options.launchpad_instance, version='devel')
    logging.basicConfig(filename='phased-updates.log',
                        format='%(asctime)s - %(levelname)s - %(message)s',
                        level=logging.INFO)
    logging.info('Starting phased-updater')
    ubuntu = launchpad.distributions['ubuntu']
    archive = ubuntu.getArchive(name='primary')
    main()
    end_time = time.time()
    logging.info("Elapsed time was %g seconds" % (end_time - start_time))