TTM: Don't set 'published' snapshot before it's done

Publish stage of the TTM will wait for the ToTest project to be done
before setting 'published' - and that's what the Release stage waits
for.
This commit is contained in:
Stephan Kulow 2019-04-08 09:07:27 +02:00
parent 22e40f787a
commit 0475cc8d6b
4 changed files with 112 additions and 86 deletions

View File

@ -15,6 +15,7 @@ import logging
import ToolBase
import cmdln
from ttm.manager import QAResult
from ttm.releaser import ToTestReleaser
from ttm.publisher import ToTestPublisher
@ -25,7 +26,7 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
def __init__(self, *args, **kwargs):
ToolBase.CommandLineInterface.__init__(self, args, kwargs)
@cmdln.option('--force', action='store_true', help="Just release, don't check")
@cmdln.option('--force', action='store_true', help="Just publish, don't check")
def do_publish(self, subcmd, opts, project):
"""${cmd_name}: check and publish ToTest
@ -35,6 +36,16 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
ToTestPublisher(self.tool).publish(project, opts.force)
@cmdln.option('--force', action='store_true', help="Just update status")
def do_wait_for_published(self, subcmd, opts, project):
"""${cmd_name}: wait for ToTest to contain publishing status and publisher finished
${cmd_usage}
${cmd_option_list}
"""
ToTestPublisher(self.tool).wait_for_published(project, opts.force)
@cmdln.option('--force', action='store_true', help="Just release, don't check")
def do_release(self, subcmd, opts, project):
"""${cmd_name}: check and release from project to ToTest
@ -52,5 +63,7 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
${cmd_option_list}
"""
ToTestPublisher(self.tool).publish(project)
if ToTestPublisher(self.tool).publish(project) == QAResult.passed:
ToTestPublisher(self.tool).wait_for_published(project)
ToTestReleaser(self.tool).release(project)

View File

@ -15,6 +15,7 @@ import ToolBase
import logging
import re
import yaml
from enum import Enum
from xml.etree import cElementTree as ET
from osclib.stagingapi import StagingAPI
try:
@ -28,6 +29,19 @@ from ttm.totest import ToTest
class NotFoundException(Exception):
pass
class QAResult(Enum):
inprogress = 1
failed = 2
passed = 3
def __str__(self):
if self.value == QAResult.inprogress:
return 'inprogress'
elif self.value == QAResult.failed:
return 'failed'
else:
return 'passed'
class ToTestManager(ToolBase.ToolBase):
def __init__(self, tool):
@ -95,11 +109,16 @@ class ToTestManager(ToolBase.ToolBase):
return result.group(1)
raise NotFoundException("can't find %s ftp version" % project)
# we don't lock the access to this attribute as the times these
# snapshots are greatly different
# make sure to update the attribute as atomar as possible - as such
# only update the snapshot and don't erase anything else. The snapshots
# have very different update times within the pipeline, so there is
# normally no chance that releaser and publisher overwrite states
def update_status(self, status, snapshot):
status_dict = self.get_status_dict()
if not self.dryrun and status_dict.get(status, '') != snapshot:
if self.dryrun:
self.logger.info('setting {} snapshot to {}'.format(status, snapshot))
return
if status_dict.get(status, '') != snapshot:
status_dict[status] = snapshot
text = yaml.safe_dump(status_dict)
self.api.attribute_value_save('ToTestManagerStatus', text)
@ -111,7 +130,7 @@ class ToTestManager(ToolBase.ToolBase):
return dict()
def get_status(self, status):
return self.get_status_dict().get(status, '')
return self.get_status_dict().get(status)
def release_package(self, project, package, set_release=None, repository=None,
target_project=None, target_repository=None):
@ -136,3 +155,32 @@ class ToTestManager(ToolBase.ToolBase):
else:
self.api.retried_POST(url)
def all_repos_done(self, project, codes=None):
"""Check the build result of the project and only return True if all
repos of that project are either published or unpublished
"""
# coolo's experience says that 'finished' won't be
# sufficient here, so don't try to add it :-)
codes = ['published', 'unpublished'] if not codes else codes
url = self.api.makeurl(
['build', project, '_result'], {'code': 'failed'})
f = self.api.retried_GET(url)
root = ET.parse(f).getroot()
ready = True
for repo in root.findall('result'):
# ignore ports. 'factory' is used by arm for repos that are not
# meant to use the totest manager.
if repo.get('repository') in ('ports', 'factory', 'images_staging'):
continue
if repo.get('dirty', '') == 'true':
self.logger.info('%s %s %s -> %s' % (repo.get('project'),
repo.get('repository'), repo.get('arch'), 'dirty'))
ready = False
if repo.get('code') not in codes:
self.logger.info('%s %s %s -> %s' % (repo.get('project'),
repo.get('repository'), repo.get('arch'), repo.get('code')))
ready = False
return ready

View File

@ -10,21 +10,18 @@
# (C) 2019 coolo@suse.de, SUSE
# Distribute under GPLv2 or GPLv3
import json
import re
import yaml
import pika
import time
import osc
from osc.core import makeurl
from ttm.manager import ToTestManager, NotFoundException
from ttm.manager import ToTestManager, NotFoundException, QAResult
from openqa_client.client import OpenQA_Client
# QA Results
QA_INPROGRESS = 1
QA_FAILED = 2
QA_PASSED = 3
class ToTestPublisher(ToTestManager):
def __init__(self, tool):
@ -40,7 +37,7 @@ class ToTestPublisher(ToTestManager):
"""Analyze the openQA jobs of a given snapshot Returns a QAResult"""
if snapshot is None:
return QA_FAILED
return QAResult.failed
jobs = self.find_openqa_results(snapshot)
@ -49,7 +46,7 @@ class ToTestPublisher(ToTestManager):
if len(jobs) < self.project.jobs_num: # not yet scheduled
self.logger.warning('we have only %s jobs' % len(jobs))
return QA_INPROGRESS
return QAResult.inprogress
in_progress = False
for job in jobs:
@ -121,12 +118,12 @@ class ToTestPublisher(ToTestManager):
self.save_issues_to_ignore()
if len(self.failed_relevant_jobs) > 0:
return QA_FAILED
return QAResult.failed
if in_progress:
return QA_INPROGRESS
return QAResult.inprogress
return QA_PASSED
return QAResult.passed
def send_amqp_event(self, current_snapshot, current_result):
amqp_url = osc.conf.config.get('ttm_amqp_url')
@ -135,7 +132,7 @@ class ToTestPublisher(ToTestManager):
return
self.logger.debug('Sending AMQP message')
inf = re.sub(r'ed$', '', self._result2str(current_result))
inf = re.sub(r'ed$', '', str(current_result))
msg_topic = '%s.ttm.build.%s' % (self.project.base.lower(), inf)
msg_body = json.dumps({
'build': current_snapshot,
@ -173,26 +170,26 @@ class ToTestPublisher(ToTestManager):
group_id = self.openqa_group_id()
if self.get_status('publishing') == current_snapshot or self.get_status('published') == current_snapshot:
self.logger.info('{} is already published'.format(current_snapshot))
return
self.logger.info('{} is already publishing'.format(current_snapshot))
return QAResult.inprogress
self.update_pinned_descr = False
current_result = self.overall_result(current_snapshot)
current_qa_version = self.current_qa_version()
self.logger.info('current_snapshot %s: %s' %
(current_snapshot, self._result2str(current_result)))
self.logger.debug('current_qa_version %s', current_qa_version)
self.logger.info('current_snapshot {}: {}'.format(current_snapshot, str(current_result)))
self.logger.debug('current_qa_version {}'.format(current_qa_version))
self.send_amqp_event(current_snapshot, current_result)
if current_result == QA_FAILED:
if current_result == QAResult.failed:
self.update_status('failed', current_snapshot)
return QAResult.failed
else:
self.update_status('failed', '')
if current_result != QA_PASSED:
return
if current_result != QAResult.passed:
return QAResult.inprogress
if current_qa_version != current_snapshot:
# We reached a very bad status: openQA testing is 'done', but not of the same version
@ -204,7 +201,22 @@ class ToTestPublisher(ToTestManager):
self.publish_factory_totest()
self.write_version_to_dashboard('snapshot', current_snapshot)
self.update_status('publishing', current_snapshot)
# for now we don't wait
return QAResult.passed
def wait_for_published(self, project, force=False):
self.setup(project)
if not force:
wait_time = 20
while not self.all_repos_done(self.project.test_project):
self.logger.info('{} is still not published, waiting {} seconds'.format(self.project.test_project, wait_time))
time.sleep(wait_time)
current_snapshot = self.get_status('publishing')
if self.dryrun:
self.logger.info('Publisher finished, updating published snpashot to {}'.format(current_snapshot))
return
self.update_status('published', current_snapshot)
group_id = self.openqa_group_id()
if not group_id:
@ -231,14 +243,6 @@ class ToTestPublisher(ToTestManager):
jobs.append(job)
return jobs
def _result2str(self, result):
if result == QA_INPROGRESS:
return 'inprogress'
elif result == QA_FAILED:
return 'failed'
else:
return 'passed'
def add_published_tag(self, group_id, snapshot):
if self.dryrun:
return
@ -301,13 +305,13 @@ class ToTestPublisher(ToTestManager):
def publish_factory_totest(self):
self.logger.info('Publish test project content')
if self.dryrun or self.project.do_not_release:
return
if self.project.container_products:
self.logger.info('Releasing container products from ToTest')
for container in self.project.container_products:
self.release_package(self.project.test_project, container.package,
repository=self.project.totest_container_repo)
if not (self.dryrun or self.project.do_not_release):
self.api.switch_flag_in_prj(
self.project.test_project, flag='publish', state='enable',
repository=self.project.product_repo)
self.api.switch_flag_in_prj(
self.project.test_project, flag='publish', state='enable',
repository=self.project.product_repo)

View File

@ -27,33 +27,25 @@ class ToTestReleaser(ToTestManager):
def release(self, project, force=False):
self.setup(project)
current_snapshot = self.get_status('testing')
testing_snapshot = self.get_status('testing')
new_snapshot = self.version_from_project()
# not overwriting
if new_snapshot == current_snapshot:
if new_snapshot == testing_snapshot:
self.logger.debug('no change in snapshot version')
return
if current_snapshot:
testing_snapshot = self.get_status('testing')
if testing_snapshot != self.get_status('failed') and testing_snapshot != self.get_status('publishing'):
self.logger.debug('Snapshot {} is still in progress'.format(testing_snapshot))
return
if testing_snapshot != self.get_status('failed') and testing_snapshot != self.get_status('published'):
self.logger.debug('Snapshot {} is still in progress'.format(testing_snapshot))
return
self.logger.info('current_snapshot %s', current_snapshot)
self.logger.debug('new_snapshot %s', new_snapshot)
self.logger.info('testing snapshot %s', testing_snapshot)
self.logger.debug('new snapshot %s', new_snapshot)
if not self.is_snapshotable():
self.logger.debug('not snapshotable')
return
if not force and not self.all_repos_done(self.project.test_project):
self.logger.debug("not all repos done, can't release")
# the repos have to be done, otherwise we better not touch them
# with a new release
return
self.update_totest(new_snapshot)
self.update_status('testing', new_snapshot)
self.update_status('failed', '')
@ -83,36 +75,6 @@ class ToTestReleaser(ToTestManager):
return self.iso_build_version(self.project.name, self.project.image_products[0].package,
arch=self.project.image_products[0].archs[0])
def all_repos_done(self, project, codes=None):
"""Check the build result of the project and only return True if all
repos of that project are either published or unpublished
"""
# coolo's experience says that 'finished' won't be
# sufficient here, so don't try to add it :-)
codes = ['published', 'unpublished'] if not codes else codes
url = self.api.makeurl(
['build', project, '_result'], {'code': 'failed'})
f = self.api.retried_GET(url)
root = ET.parse(f).getroot()
ready = True
for repo in root.findall('result'):
# ignore ports. 'factory' is used by arm for repos that are not
# meant to use the totest manager.
if repo.get('repository') in ('ports', 'factory', 'images_staging'):
continue
if repo.get('dirty', '') == 'true':
self.logger.info('%s %s %s -> %s' % (repo.get('project'),
repo.get('repository'), repo.get('arch'), 'dirty'))
ready = False
if repo.get('code') not in codes:
self.logger.info('%s %s %s -> %s' % (repo.get('project'),
repo.get('repository'), repo.get('arch'), repo.get('code')))
ready = False
return ready
def maxsize_for_package(self, package):
if re.match(r'.*-mini-.*', package):
return 737280000 # a CD needs to match
@ -267,4 +229,3 @@ class ToTestReleaser(ToTestManager):
repository=self.project.product_repo)
self._release(set_release=release)