Files
fm-orchestrator/module_build_service/utils/reuse.py

336 lines
14 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2018 Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Written by Ralph Bean <rbean@redhat.com>
# Matt Prahl <mprahl@redhat.com>
# Jan Kaluza <jkaluza@redhat.com>
import kobo.rpmlib
import module_build_service.messaging
from module_build_service import log, models, conf
def reuse_component(component, previous_component_build,
change_state_now=False):
"""
Reuses component build `previous_component_build` instead of building
component `component`
Returns the list of BaseMessage instances to be handled later by the
scheduler.
"""
import koji
log.info(
'Reusing component "{0}" from a previous module '
'build with the nvr "{1}"'.format(
component.package, previous_component_build.nvr))
component.reused_component_id = previous_component_build.id
component.task_id = previous_component_build.task_id
if change_state_now:
component.state = previous_component_build.state
else:
# Use BUILDING state here, because we want the state to change to
# COMPLETE by the fake KojiBuildChange message we are generating
# few lines below. If we would set it to the right state right
# here, we would miss the code path handling the KojiBuildChange
# which works only when switching from BUILDING to COMPLETE.
component.state = koji.BUILD_STATES['BUILDING']
component.state_reason = \
'Reused component from previous module build'
component.nvr = previous_component_build.nvr
nvr_dict = kobo.rpmlib.parse_nvr(component.nvr)
# Add this message to further_work so that the reused
# component will be tagged properly
return [
module_build_service.messaging.KojiBuildChange(
msg_id='reuse_component: fake msg',
build_id=None,
task_id=component.task_id,
build_new_state=previous_component_build.state,
build_name=component.package,
build_version=nvr_dict['version'],
build_release=nvr_dict['release'],
module_build_id=component.module_id,
state_reason=component.state_reason
)
]
def _get_reusable_module(session, module):
"""
Returns previous module build of the module `module` in case it can be
used as a source module to get the components to reuse from.
In case there is no such module, returns None.
:param session: SQLAlchemy database session
:param module: the ModuleBuild object of module being built.
:return: ModuleBuild object which can be used for component reuse.
"""
mmd = module.mmd()
# Find the latest module that is in the done or ready state
previous_module_build = session.query(models.ModuleBuild)\
.filter_by(name=mmd.get_name())\
.filter_by(stream=mmd.get_stream())\
.filter(models.ModuleBuild.state.in_([3, 5]))\
.filter(models.ModuleBuild.scmurl.isnot(None))\
.order_by(models.ModuleBuild.time_completed.desc())
# If we are rebuilding with the "changed-and-after" option, then we can't reuse
# components from modules that were built more liberally
if module.rebuild_strategy == 'changed-and-after':
previous_module_build = previous_module_build.filter(
models.ModuleBuild.rebuild_strategy.in_(['all', 'changed-and-after']))
previous_module_build = previous_module_build.filter_by(
build_context=module.build_context)
previous_module_build = previous_module_build.first()
# The component can't be reused if there isn't a previous build in the done
# or ready state
if not previous_module_build:
log.info("Cannot re-use. %r is the first module build." % module)
return None
return previous_module_build
def attempt_to_reuse_all_components(builder, session, module):
"""
Tries to reuse all the components in a build. The components are also
tagged to the tags using the `builder`.
Returns True if all components could be reused, otherwise False. When
False is returned, no component has been reused.
"""
previous_module_build = _get_reusable_module(session, module)
if not previous_module_build:
return False
mmd = module.mmd()
old_mmd = previous_module_build.mmd()
# [(component, component_to_reuse), ...]
component_pairs = []
# Find out if we can reuse all components and cache component and
# component to reuse pairs.
for c in module.component_builds:
if c.package == "module-build-macros":
continue
component_to_reuse = get_reusable_component(
session, module, c.package,
previous_module_build=previous_module_build, mmd=mmd,
old_mmd=old_mmd)
if not component_to_reuse:
return False
component_pairs.append((c, component_to_reuse))
# Stores components we will tag to buildroot and final tag.
components_to_tag = []
# Reuse all components.
for c, component_to_reuse in component_pairs:
# Set the module.batch to the last batch we have.
if c.batch > module.batch:
module.batch = c.batch
# Reuse the component
reuse_component(c, component_to_reuse, True)
components_to_tag.append(c.nvr)
# Tag them
builder.buildroot_add_artifacts(components_to_tag, install=False)
builder.tag_artifacts(components_to_tag, dest_tag=True)
return True
def get_reusable_components(session, module, component_names):
"""
Returns the list of ComponentBuild instances belonging to previous module
build which can be reused in the build of module `module`.
The ComponentBuild instances in returned list are in the same order as
their names in the component_names input list.
In case some component cannot be reused, None is used instead of a
ComponentBuild instance in the returned list.
:param session: SQLAlchemy database session
:param module: the ModuleBuild object of module being built.
:param component_names: List of component names to be reused.
:return: List of ComponentBuild instances to reuse in the same
order as `component_names`
"""
# We support components reusing only for koji and test backend.
if conf.system not in ['koji', 'test']:
return [None] * len(component_names)
previous_module_build = _get_reusable_module(session, module)
if not previous_module_build:
return [None] * len(component_names)
mmd = module.mmd()
old_mmd = previous_module_build.mmd()
ret = []
for component_name in component_names:
ret.append(get_reusable_component(
session, module, component_name, previous_module_build, mmd,
old_mmd))
return ret
def get_reusable_component(session, module, component_name,
previous_module_build=None, mmd=None, old_mmd=None):
"""
Returns the component (RPM) build of a module that can be reused
instead of needing to rebuild it
:param session: SQLAlchemy database session
:param module: the ModuleBuild object of module being built with a formatted
mmd
:param component_name: the name of the component (RPM) that you'd like to
reuse a previous build of
:param previous_module_build: the ModuleBuild instances of a module build
which contains the components to reuse. If not passed, _get_reusable_module
is called to get the ModuleBuild instance. Consider passing the ModuleBuild
instance in case you plan to call get_reusable_component repeatedly for the
same module to make this method faster.
:param mmd: ModuleMd.Module of `module`. If not passed, it is taken from
module.mmd(). Consider passing this arg in case you plan to call
get_reusable_component repeatedly for the same module to make this method faster.
:param old_mmd: ModuleMd.Module of `previous_module_build`. If not passed,
it is taken from previous_module_build.mmd(). Consider passing this arg in
case you plan to call get_reusable_component repeatedly for the same
module to make this method faster.
:return: the component (RPM) build SQLAlchemy object, if one is not found,
None is returned
"""
# We support component reusing only for koji and test backend.
if conf.system not in ['koji', 'test']:
return None
# If the rebuild strategy is "all", that means that nothing can be reused
if module.rebuild_strategy == 'all':
log.info('Cannot re-use the component because the rebuild strategy is "all".')
return None
if not previous_module_build:
previous_module_build = _get_reusable_module(session, module)
if not previous_module_build:
return None
if not mmd:
mmd = module.mmd()
if not old_mmd:
old_mmd = previous_module_build.mmd()
# If the chosen component for some reason was not found in the database,
# or the ref is missing, something has gone wrong and the component cannot
# be reused
new_module_build_component = models.ComponentBuild.from_component_name(
session, component_name, module.id)
if not new_module_build_component or not new_module_build_component.batch \
or not new_module_build_component.ref:
log.info('Cannot re-use. New component not found in the db.')
return None
prev_module_build_component = models.ComponentBuild.from_component_name(
session, component_name, previous_module_build.id)
# If the component to reuse for some reason was not found in the database,
# or the ref is missing, something has gone wrong and the component cannot
# be reused
if not prev_module_build_component or not prev_module_build_component.batch\
or not prev_module_build_component.ref:
log.info('Cannot re-use. Previous component not found in the db.')
return None
# Make sure the ref for the component that is trying to be reused
# hasn't changed since the last build
if prev_module_build_component.ref != new_module_build_component.ref:
log.info('Cannot re-use. Component commit hashes do not match.')
return None
# At this point we've determined that both module builds contain the component
# and the components share the same commit hash
if module.rebuild_strategy == 'changed-and-after':
# Make sure the batch number for the component that is trying to be reused
# hasn't changed since the last build
if prev_module_build_component.batch != new_module_build_component.batch:
log.info('Cannot re-use. Batch numbers do not match.')
return None
# If the mmd.buildopts.macros.rpms changed, we cannot reuse
if mmd.get_rpm_buildopts().get('macros') != old_mmd.get_rpm_buildopts().get('macros'):
log.info('Cannot re-use. Old modulemd macros do not match the new.')
return None
# At this point we've determined that both module builds contain the component
# with the same commit hash and they are in the same batch. We've also determined
# that both module builds depend(ed) on the same exact module builds. Now it's time
# to determine if the components before it have changed.
#
# Convert the component_builds to a list and sort them by batch
new_component_builds = list(module.component_builds)
new_component_builds.sort(key=lambda x: x.batch)
prev_component_builds = list(previous_module_build.component_builds)
prev_component_builds.sort(key=lambda x: x.batch)
new_module_build_components = []
previous_module_build_components = []
# Create separate lists for the new and previous module build. These lists
# will have an entry for every build batch *before* the component's
# batch except for 1, which is reserved for the module-build-macros RPM.
# Each batch entry will contain a set of "(name, ref)" with the name and
# ref (commit) of the component.
for i in range(new_module_build_component.batch - 1):
# This is the first batch which we want to skip since it will always
# contain only the module-build-macros RPM and it gets built every time
if i == 0:
continue
new_module_build_components.append(set([
(value.package, value.ref) for value in
new_component_builds if value.batch == i + 1
]))
previous_module_build_components.append(set([
(value.package, value.ref) for value in
prev_component_builds if value.batch == i + 1
]))
# If the previous batches don't have the same ordering and hashes, then the
# component can't be reused
if previous_module_build_components != new_module_build_components:
log.info('Cannot re-use. Ordering or commit hashes of '
'previous batches differ.')
return None
reusable_component = models.ComponentBuild.query.filter_by(
package=component_name, module_id=previous_module_build.id).one()
log.debug('Found reusable component!')
return reusable_component