Exemplo n.º 1
0
def _send_mail(from_addr, to_addr, body):
    """
    Send emails with smtplib. This is a lower level function than send_e-mail().

    Args:
        from_addr (str): The e-mail address to use in the envelope from field.
        to_addr (str): The e-mail address to use in the envelope to field.
        body (str): The body of the e-mail.
    """
    smtp_server = config.get('smtp_server')
    if not smtp_server:
        log.info('Not sending email: No smtp_server defined')
        return
    smtp = None
    try:
        log.debug('Connecting to %s', smtp_server)
        smtp = smtplib.SMTP(smtp_server)
        smtp.sendmail(from_addr, [to_addr], body)
    except smtplib.SMTPRecipientsRefused as e:
        log.warning('"recipient refused" for %r, %r' % (to_addr, e))
    except Exception:
        log.exception('Unable to send mail')
    finally:
        if smtp:
            smtp.quit()
Exemplo n.º 2
0
def exception_json_view(exc, request):
    """
    Return a json error response upon generic errors (404s, 403s, 500s, etc..).

    This is here to catch everything that isn't caught by our cornice error
    handlers.  When we do catch something, we transform it into a cornice
    Errors object and pass it to our nice cornice error handler.  That way, all
    the exception presentation and rendering we can keep in one place.

    Args:
        exc (Exception): The unhandled exception.
        request (pyramid.request.Request): The current request.
    Returns:
        bodhi.server.services.errors.json_handler: A pyramid.httpexceptions.HTTPError to be rendered
            to the user for the given exception.
    """
    errors = getattr(request, 'errors', [])
    status = getattr(exc, 'status_code', 500)

    if status not in (404, 403):
        log.exception("Error caught.  Handling JSON response.")
    else:
        log.warning(str(exc))

    if not len(errors):
        description = getattr(exc, 'explanation', None) or str(exc)

        errors = cornice.errors.Errors(status=status)
        errors.add('body',
                   description=description,
                   name=exc.__class__.__name__)
        request.errors = errors

    return bodhi.server.services.errors.json_handler(request)
Exemplo n.º 3
0
def new_comment(request):
    """
    Add a new comment to an update.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes
            an iterable of messages to display to the user.
    """
    data = request.validated

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token')

    update = data.pop('update')
    author = request.user and request.user.name

    try:
        comment, caveats = update.comment(session=request.db, author=author, **data)
    except ValueError as e:
        request.errors.add('body', 'comment', str(e))
        return
    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'comment', 'Unable to create comment')
        return

    return dict(comment=comment, caveats=caveats)
Exemplo n.º 4
0
def new_comment(request):
    """
    Add a new comment to an update.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes
            an iterable of messages to display to the user.
    """
    data = request.validated

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token')

    update = data.pop('update')
    email = data.pop('email', None)
    author = email or (request.user and request.user.name)
    anonymous = bool(email) or not author

    if not author:
        request.errors.add('body', 'email', 'You must provide an author')
        request.errors.status = HTTPBadRequest.code
        return

    try:
        comment, caveats = update.comment(
            session=request.db, author=author, anonymous=anonymous, **data)
    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'comment', 'Unable to create comment')
        return

    return dict(comment=comment, caveats=caveats)
Exemplo n.º 5
0
def waive_test_results(request):
    """
    Waive all blocking test results on a given update when gating is on.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary mapping the key "update" to the update.
    """
    update = request.validated['update']
    comment = request.validated.pop('comment', None)
    tests = request.validated.pop('tests', None)

    try:
        update.waive_test_results(request.user.name, comment, tests)
    except LockedUpdateException as e:
        log.warning(str(e))
        request.errors.add('body', 'request', str(e))
    except BodhiException as e:
        log.error("Failed to waive the test results: %s", e)
        request.errors.add('body', 'request', str(e))
    except Exception as e:
        log.exception("Unhandled exception in waive_test_results")
        request.errors.add('body', 'request', str(e))

    return dict(update=update)
Exemplo n.º 6
0
    def __call__(self):
        """
        Manage a database Session object for the life of the context.

        Yields a database Session object, then either commits the tranaction if there were no
        Exceptions or rolls back the transaction. In either case, it also will close and remove the
        Session.
        """
        session = Session()
        try:
            yield session
            session.commit()
        except Exception as e:
            # It is possible for session.rolback() to raise Exceptions, so we will wrap it in an
            # Exception handler as well so we can log the rollback failure and still raise the
            # original Exception.
            try:
                session.rollback()
            except Exception:
                log.exception(
                    'An Exception was raised while rolling back a transaction.'
                )
            raise e
        finally:
            session.close()
            Session.remove()
Exemplo n.º 7
0
def get_test_results(request):
    """
    Get the test results on a given update when gating is on.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary mapping the key 'decisions' to a list of result dictionaries.
    """
    update = request.validated['update']

    decisions = []
    try:
        decisions = update.get_test_gating_info()
    except RequestsTimeout as e:
        log.error("Error querying greenwave for test results - timed out")
        request.errors.add('body', 'request', str(e))
        request.errors.status = 504
    except (RequestException, RuntimeError) as e:
        log.error("Error querying greenwave for test results: %s", e)
        request.errors.add('body', 'request', str(e))
        request.errors.status = 502
    except BodhiException as e:
        log.error("Failed to query greenwave for test results: %s", e)
        request.errors.add('body', 'request', str(e))
        request.errors.status = 501
    except Exception as e:
        log.exception("Unhandled exception in get_test_results")
        request.errors.add('body', 'request', str(e))
        request.errors.status = 500

    return dict(decisions=decisions)
Exemplo n.º 8
0
def save_release(request):
    """
    Save a release.

    This entails either creating a new release, or editing an existing one. To
    edit an existing release, the release's original name must be specified in
    the ``edited`` parameter.

    Args:
        request (pyramid.request): The current request.
    Returns:
        bodhi.server.models.Request: The created or edited Request.
    """
    data = request.validated

    edited = data.pop("edited", None)

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token', None)

    try:
        if edited is None:
            log.info("Creating a new release: %s" % data['name'])
            r = Release(**data)

        else:
            log.info("Editing release: %s" % edited)
            r = request.db.query(Release).filter(Release.name == edited).one()
            for k, v in data.items():
                # We have to change updates status to obsolete
                # if state of release changes to archived
                if k == "state" and v == ReleaseState.archived and \
                        r.state != ReleaseState.archived:
                    updates = request.db.query(Update).filter(Update.release_id == r.id).filter(
                        Update.status.notin_(
                            [UpdateStatus.obsolete, UpdateStatus.stable, UpdateStatus.unpushed]
                        )
                    ).all()
                    for u in updates:
                        u.status = UpdateStatus.obsolete
                        u.comment(
                            request.db,
                            'This update is marked obsolete because '
                            'the {} release is archived.'.format(u.release.name),
                            author='bodhi',
                        )
                setattr(r, k, v)

    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'release',
                           'Unable to create/edit release: %s' % e)
        return

    request.db.add(r)
    request.db.flush()

    return r
Exemplo n.º 9
0
 def expire_buildroot_overrides(self):
     """ Expire any buildroot overrides that are in this push """
     for update in self.updates:
         if update.request is UpdateRequest.stable:
             for build in update.builds:
                 if build.override:
                     try:
                         build.override.expire()
                     except Exception:
                         log.exception('Problem expiring override')
Exemplo n.º 10
0
def set_request(request):
    """
    Set a specific :class:`bodhi.server.models.UpdateRequest` on a given update.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary mapping the key "update" to the update that was modified.
    """
    update = request.validated['update']
    action = request.validated['request']

    if update.locked:
        request.errors.add('body', 'request',
                           "Can't change request on a locked update")
        return

    if update.release.state is ReleaseState.archived:
        request.errors.add('body', 'request',
                           "Can't change request for an archived release")
        return

    if update.status == UpdateStatus.stable and action == UpdateRequest.testing:
        request.errors.add(
            'body', 'request',
            "Pushing back to testing a stable update is not allowed")
        return

    if action == UpdateRequest.stable:
        settings = request.registry.settings
        result, reason = update.check_requirements(request.db, settings)
        log.info(
            f'Unable to set request for {update.alias} to {action} due to failed requirements: '
            f'{reason}')
        if not result:
            request.errors.add('body', 'request',
                               'Requirement not met %s' % reason)
            return

    try:
        update.set_request(request.db, action, request.user.name)
    except BodhiException as e:
        log.info("Failed to set the request: %s", e)
        request.errors.add('body', 'request', str(e))
    except Exception as e:
        log.exception("Unhandled exception in set_request")
        request.errors.add('body', 'request', str(e))

    return dict(update=update)
Exemplo n.º 11
0
def taskotron_results(settings,
                      entity='results/latest',
                      max_queries=10,
                      **kwargs):
    """
    Yield resultsdb results using query arguments.

    Args:
        settings (bodhi.server.config.BodhiConfig): Bodhi's settings.
        entity (str): The API endpoint to use (see resultsdb documentation).
        max_queries (int): The maximum number of queries to perform (pages to retrieve). ``1`` means
            just a single page. ``None`` or ``0`` means no limit. Please note some tests might have
            thousands of results in the database and it's very reasonable to limit queries (thus the
            default value).
        kwargs (dict): Args that will be passed to resultsdb to specify what results to retrieve.
    Returns:
        generator or None: Yields Python objects loaded from ResultsDB's "data" field in its JSON
            response, or None if there was an Exception while performing the query.
    """
    max_queries = max_queries or 0
    url = settings['resultsdb_api_url'] + "/api/v2.0/" + entity
    if kwargs:
        url = url + "?" + urlencode(kwargs)
    data = True
    queries = 0

    try:
        while data and url:
            log.debug("Grabbing %r" % url)
            response = requests.get(url, timeout=60)
            if response.status_code != 200:
                raise IOError("status code was %r" % response.status_code)
            json = response.json()
            for datum in json['data']:
                yield datum

            url = json.get('next')
            queries += 1
            if max_queries and queries >= max_queries and url:
                log.debug('Too many result pages, aborting at: %r' % url)
                break
    except Exception as e:
        log.exception("Problem talking to %r : %r" % (url, str(e)))
Exemplo n.º 12
0
def save_release(request):
    """
    Save a release.

    This entails either creating a new release, or editing an existing one. To
    edit an existing release, the release's original name must be specified in
    the ``edited`` parameter.

    Args:
        request (pyramid.request): The current request.
    Returns:
        bodhi.server.models.Request: The created or edited Request.
    """
    data = request.validated

    edited = data.pop("edited", None)

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token', None)

    try:
        if edited is None:
            log.info("Creating a new release: %s" % data['name'])
            r = Release(**data)

        else:
            log.info("Editing release: %s" % edited)
            r = request.db.query(Release).filter(Release.name == edited).one()
            for k, v in data.items():
                setattr(r, k, v)

    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'release',
                           'Unable to create update: %s' % e)
        return

    request.db.add(r)
    request.db.flush()

    return r
Exemplo n.º 13
0
def set_request(request):
    """
    Set a specific :class:`bodhi.server.models.UpdateRequest` on a given update.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary mapping the key "update" to the update that was modified.
    """
    update = request.validated['update']
    action = request.validated['request']

    if update.locked:
        request.errors.add('body', 'request',
                           "Can't change request on a locked update")
        return

    if update.release.state is ReleaseState.archived:
        request.errors.add('body', 'request',
                           "Can't change request for an archived release")
        return

    if action in (UpdateRequest.stable, UpdateRequest.batched):
        settings = request.registry.settings
        result, reason = update.check_requirements(request.db, settings)
        if not result:
            request.errors.add('body', 'request',
                               'Requirement not met %s' % reason)
            return

    try:
        update.set_request(request.db, action, request.user.name)
    except BodhiException as e:
        log.exception("Failed to set the request")
        request.errors.add('body', 'request', str(e))
    except Exception as e:
        log.exception("Unhandled exception in set_request")
        request.errors.add('body', 'request', str(e))

    return dict(update=update)
Exemplo n.º 14
0
def new_comment(request):
    """
    Add a new comment to an update.

    Args:
        request (pyramid.request): The current request.
    Returns:
        dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes
            an iterable of messages to display to the user.
    """
    data = request.validated

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token')

    update = data.pop('update')
    author = request.user and request.user.name
    if not author:
        # this can happen if we have a stale cached session, a 403
        # response will trigger the client to reauth:
        # https://github.com/fedora-infra/bodhi/issues/3298
        request.errors.add('body', 'email', 'You must provide an author')
        request.errors.status = HTTPForbidden.code
        return

    try:
        comment, caveats = update.comment(session=request.db,
                                          author=author,
                                          **data)
    except ValueError as e:
        request.errors.add('body', 'comment', str(e))
        return
    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'comment', 'Unable to create comment')
        return

    return dict(comment=comment, caveats=caveats)
Exemplo n.º 15
0
def save_override(request):
    """
    Create or edit a buildroot override.

    This entails either creating a new buildroot override, or editing an
    existing one. To edit an existing buildroot override, the buildroot
    override's original id needs to be specified in the ``edited`` parameter.

    Args:
        request (pyramid.request): The current web request.
    Returns:
        dict: The new or edited override.
    """
    data = request.validated

    edited = data.pop("edited")

    caveats = []
    try:
        submitter = User.get(request.user.name)
        if edited is None:
            builds = data['builds']
            overrides = []
            if len(builds) > 1:
                caveats.append({
                    'name': 'nvrs',
                    'description': 'Your override submission was '
                    'split into %i.' % len(builds)
                })
            for build in builds:
                log.info("Creating a new buildroot override: %s" % build.nvr)
                if BuildrootOverride.get(build.id):
                    request.errors.add('body', 'builds',
                                       'Buildroot override for %s already exists' % build.nvr)
                    return
                else:
                    overrides.append(BuildrootOverride.new(
                        request,
                        build=build,
                        submitter=submitter,
                        notes=data['notes'],
                        expiration_date=data['expiration_date'],
                    ))

            if len(builds) > 1:
                result = dict(overrides=overrides)
            else:
                result = overrides[0]
        else:
            log.info("Editing buildroot override: %s" % edited)

            edited = Build.get(edited)

            if edited is None:
                request.errors.add('body', 'edited', 'No such build')
                return

            result = BuildrootOverride.edit(
                request, edited=edited, submitter=submitter,
                notes=data["notes"], expired=data["expired"],
                expiration_date=data["expiration_date"])

            if not result:
                # Some error inside .edit(...)
                return

    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'override',
                           'Unable to save buildroot override: %s' % e)
        return

    if not isinstance(result, dict):
        result = result.__json__()

    result['caveats'] = caveats

    return result
Exemplo n.º 16
0
def new_update(request):
    """
    Save an update.

    This entails either creating a new update, or editing an existing one. To
    edit an existing update, the update's alias must be specified in
    the ``edited`` parameter.

    If the ``from_tag`` parameter is specified and ``builds`` is missing or
    empty, the list of builds will be filled with the latest builds in this
    Koji tag. This is done by validate_from_tag() because the list of builds
    needs to be available in validate_acls().

    If the release is composed by Bodhi (i.e. a branched or stable release
    after the Bodhi activation point), ensure that related tags
    ``from_tag``-pending-signing and ``from_tag``-testing exists and if not
    create them in Koji. If the state of the release is not `pending`, add its
    pending-signing tag and remove it if it's a side tag.

    Args:
        request (pyramid.request): The current request.
    """
    data = request.validated
    log.debug('validated = %s' % data)

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token')

    # Same here, but it can be missing.
    data.pop('builds_from_tag', None)
    data.pop('sidetag_owner', None)

    build_nvrs = data.get('builds', [])
    from_tag = data.get('from_tag')

    caveats = []
    try:

        releases = set()
        builds = []

        # Create the Package and Build entities
        for nvr in build_nvrs:
            name, version, release = request.buildinfo[nvr]['nvr']

            package = Package.get_or_create(request.db, request.buildinfo[nvr])

            # Also figure out the build type and create the build if absent.
            build_class = ContentType.infer_content_class(
                base=Build, build=request.buildinfo[nvr]['info'])
            build = build_class.get(nvr)

            if build is None:
                log.debug("Adding nvr %s, type %r", nvr, build_class)
                build = build_class(nvr=nvr, package=package)
                request.db.add(build)
                request.db.flush()

            build.package = package
            build.release = request.buildinfo[build.nvr]['release']
            builds.append(build)
            releases.add(request.buildinfo[build.nvr]['release'])

        # Disable manual updates for releases not composed by Bodhi
        # see #4058
        if not from_tag:
            for release in releases:
                if not release.composed_by_bodhi:
                    request.errors.add(
                        'body', 'builds',
                        "Cannot manually create updates for a Release which is not "
                        "composed by Bodhi.\nRead the 'Automatic updates' page in "
                        "Bodhi docs about this error.")
                    request.db.rollback()
                    return

        # We want to go ahead and commit the transaction now so that the Builds are in the database.
        # Otherwise, there will be a race condition between robosignatory signing the Builds and the
        # signed handler attempting to mark the builds as signed. When we lose that race, the signed
        # handler doesn't see the Builds in the database and gives up. After that, nothing will mark
        # the builds as signed.
        request.db.commit()

        # After we commit the transaction, we need to get the builds and releases again,
        # since they were tied to the previous session that has now been terminated.
        builds = []
        releases = set()
        for nvr in build_nvrs:
            # At this moment, we are sure the builds are in the database (that is what the commit
            # was for actually).
            build = Build.get(nvr)
            builds.append(build)
            releases.add(build.release)

        if data.get('edited'):

            log.info('Editing update: %s' % data['edited'])

            data['release'] = list(releases)[0]
            data['builds'] = [b.nvr for b in builds]
            data['from_tag'] = from_tag
            result, _caveats = Update.edit(request, data)
            caveats.extend(_caveats)
        else:
            if len(releases) > 1:
                caveats.append({
                    'name':
                    'releases',
                    'description':
                    'Your update is being split '
                    'into %i, one for each release.' % len(releases)
                })
            updates = []
            for release in releases:
                _data = copy.copy(data)  # Copy it because .new(..) mutates it
                _data['builds'] = [b for b in builds if b.release == release]
                _data['release'] = release
                _data['from_tag'] = from_tag

                log.info('Creating new update: %r' % _data['builds'])
                result, _caveats = Update.new(request, _data)
                log.debug('%s update created', result.alias)

                updates.append(result)
                caveats.extend(_caveats)

            if len(releases) > 1:
                result = dict(updates=updates)

            if from_tag:
                for u in updates:
                    builds = [b.nvr for b in u.builds]
                    if not u.release.composed_by_bodhi:
                        # Before the Bodhi activation point of a release, keep builds tagged
                        # with the side-tag and its associate tags.
                        side_tag_signing_pending = u.release.get_pending_signing_side_tag(
                            from_tag)
                        side_tag_testing_pending = u.release.get_pending_testing_side_tag(
                            from_tag)
                        handle_side_and_related_tags_task.delay(
                            builds=builds,
                            pending_signing_tag=side_tag_signing_pending,
                            from_tag=from_tag,
                            pending_testing_tag=side_tag_testing_pending)
                    else:
                        # After the Bodhi activation point of a release, add the pending-signing tag
                        # of the release to funnel the builds back into a normal workflow for a
                        # stable release.
                        pending_signing_tag = u.release.pending_signing_tag
                        candidate_tag = u.release.candidate_tag
                        handle_side_and_related_tags_task.delay(
                            builds=builds,
                            pending_signing_tag=pending_signing_tag,
                            from_tag=from_tag,
                            candidate_tag=candidate_tag)

    except LockedUpdateException as e:
        log.warning(str(e))
        request.errors.add('body', 'builds', "%s" % str(e))
        return
    except Exception as e:
        log.exception('Failed to create update')
        request.errors.add('body', 'builds',
                           'Unable to create update.  %s' % str(e))
        return

    # Obsolete older updates for three different cases...
    # editing an update, submitting a new single update, submitting multiple.

    if isinstance(result, dict):
        updates = result['updates']
    else:
        updates = [result]

    for update in updates:
        try:
            caveats.extend(update.obsolete_older_updates(request.db))
        except Exception as e:
            caveats.append({
                'name':
                'update',
                'description':
                'Problem obsoleting older updates: %s' % str(e),
            })

    if not isinstance(result, dict):
        result = result.__json__()

    result['caveats'] = caveats

    return result
Exemplo n.º 17
0
def save_override(request):
    """
    Create or edit a buildroot override.

    This entails either creating a new buildroot override, or editing an
    existing one. To edit an existing buildroot override, the buildroot
    override's original id needs to be specified in the ``edited`` parameter.

    Args:
        request (pyramid.request): The current web request.
    Returns:
        dict: The new or edited override.
    """
    data = request.validated

    edited = data.pop("edited")

    caveats = []
    try:
        submitter = User.get(request.user.name)
        if edited is None:
            builds = data['builds']
            overrides = []
            if len(builds) > 1:
                caveats.append({
                    'name':
                    'nvrs',
                    'description':
                    'Your override submission was '
                    'split into %i.' % len(builds)
                })
            for build in builds:
                log.info("Creating a new buildroot override: %s" % build.nvr)
                existing_override = BuildrootOverride.get(build.id)
                if existing_override:
                    if not existing_override.expired_date:
                        data['expiration_date'] = max(
                            existing_override.expiration_date,
                            data['expiration_date'])

                    new_notes = f"""{data['notes']}
_____________
_@{existing_override.submitter.name} ({existing_override.submission_date.strftime('%b %d, %Y')})_
{existing_override.notes}"""
                    # Truncate notes at 2000 chars
                    if len(new_notes) > 2000:
                        new_notes = new_notes[:1972] + '(...)\n___Notes truncated___'

                    overrides.append(
                        BuildrootOverride.edit(
                            request,
                            edited=build,
                            submitter=submitter,
                            submission_date=datetime.now(),
                            notes=new_notes,
                            expiration_date=data['expiration_date'],
                            expired=None,
                        ))
                else:
                    overrides.append(
                        BuildrootOverride.new(
                            request,
                            build=build,
                            submitter=submitter,
                            notes=data['notes'],
                            expiration_date=data['expiration_date'],
                        ))

            if len(builds) > 1:
                result = dict(overrides=overrides)
            else:
                result = overrides[0]
        else:
            log.info("Editing buildroot override: %s" % edited)

            edited = Build.get(edited)

            if edited is None:
                request.errors.add('body', 'edited', 'No such build')
                return

            result = BuildrootOverride.edit(
                request,
                edited=edited,
                submitter=submitter,
                notes=data["notes"],
                expired=data["expired"],
                expiration_date=data["expiration_date"])

            if not result:
                # Some error inside .edit(...)
                return

    except Exception as e:
        log.exception(e)
        request.errors.add('body', 'override',
                           'Unable to save buildroot override: %s' % e)
        return

    if not isinstance(result, dict):
        result = result.__json__()

    result['caveats'] = caveats

    return result
Exemplo n.º 18
0
def new_update(request):
    """
    Save an update.

    This entails either creating a new update, or editing an existing one. To
    edit an existing update, the update's alias must be specified in
    the ``edited`` parameter.

    Args:
        request (pyramid.request): The current request.
    """
    data = request.validated
    log.debug('validated = %s' % data)

    # This has already been validated at this point, but we need to ditch
    # it since the models don't care about a csrf argument.
    data.pop('csrf_token')

    caveats = []
    try:

        releases = set()
        builds = []

        # Create the Package and Build entities
        for nvr in data['builds']:
            name, version, release = request.buildinfo[nvr]['nvr']

            package = Package.get_or_create(request.buildinfo[nvr])

            # Also figure out the build type and create the build if absent.
            build_class = ContentType.infer_content_class(
                base=Build, build=request.buildinfo[nvr]['info'])
            build = build_class.get(nvr)

            if build is None:
                log.debug("Adding nvr %s, type %r", nvr, build_class)
                build = build_class(nvr=nvr, package=package)
                request.db.add(build)
                request.db.flush()

            build.package = package
            build.release = request.buildinfo[build.nvr]['release']
            builds.append(build)
            releases.add(request.buildinfo[build.nvr]['release'])

        # We want to go ahead and commit the transaction now so that the Builds are in the database.
        # Otherwise, there will be a race condition between robosignatory signing the Builds and the
        # signed handler attempting to mark the builds as signed. When we lose that race, the signed
        # handler doesn't see the Builds in the database and gives up. After that, nothing will mark
        # the builds as signed.
        request.db.commit()

        # After we commit the transaction, we need to get the builds and releases again, since they
        # were tied to the previous session that has now been terminated.
        builds = []
        releases = set()
        for nvr in data['builds']:
            # At this moment, we are sure the builds are in the database (that is what the commit
            # was for actually).
            build = Build.get(nvr)
            builds.append(build)
            releases.add(build.release)

        if data.get('edited'):

            log.info('Editing update: %s' % data['edited'])

            data['release'] = list(releases)[0]
            data['builds'] = [b.nvr for b in builds]
            result, _caveats = Update.edit(request, data)
            caveats.extend(_caveats)
        else:
            if len(releases) > 1:
                caveats.append({
                    'name':
                    'releases',
                    'description':
                    'Your update is being split '
                    'into %i, one for each release.' % len(releases)
                })
            updates = []
            for release in releases:
                _data = copy.copy(data)  # Copy it because .new(..) mutates it
                _data['builds'] = [b for b in builds if b.release == release]
                _data['release'] = release

                log.info('Creating new update: %r' % _data['builds'])
                result, _caveats = Update.new(request, _data)
                log.debug('%s update created', result.alias)

                updates.append(result)
                caveats.extend(_caveats)

            if len(releases) > 1:
                result = dict(updates=updates)
    except LockedUpdateException as e:
        log.warning(str(e))
        request.errors.add('body', 'builds', "%s" % str(e))
        return
    except Exception as e:
        log.exception('Failed to create update')
        request.errors.add('body', 'builds',
                           'Unable to create update.  %s' % str(e))
        return

    # Obsolete older updates for three different cases...
    # editing an update, submitting a new single update, submitting multiple.

    if isinstance(result, dict):
        updates = result['updates']
    else:
        updates = [result]

    for update in updates:
        try:
            caveats.extend(update.obsolete_older_updates(request.db))
        except Exception as e:
            caveats.append({
                'name':
                'update',
                'description':
                'Problem obsoleting older updates: %s' % str(e),
            })

    if not isinstance(result, dict):
        result = result.__json__()

    result['caveats'] = caveats

    return result