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:
parent
22e40f787a
commit
0475cc8d6b
17
ttm/cli.py
17
ttm/cli.py
@ -15,6 +15,7 @@ import logging
|
|||||||
import ToolBase
|
import ToolBase
|
||||||
import cmdln
|
import cmdln
|
||||||
|
|
||||||
|
from ttm.manager import QAResult
|
||||||
from ttm.releaser import ToTestReleaser
|
from ttm.releaser import ToTestReleaser
|
||||||
from ttm.publisher import ToTestPublisher
|
from ttm.publisher import ToTestPublisher
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
ToolBase.CommandLineInterface.__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):
|
def do_publish(self, subcmd, opts, project):
|
||||||
"""${cmd_name}: check and publish ToTest
|
"""${cmd_name}: check and publish ToTest
|
||||||
|
|
||||||
@ -35,6 +36,16 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
|
|||||||
|
|
||||||
ToTestPublisher(self.tool).publish(project, opts.force)
|
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")
|
@cmdln.option('--force', action='store_true', help="Just release, don't check")
|
||||||
def do_release(self, subcmd, opts, project):
|
def do_release(self, subcmd, opts, project):
|
||||||
"""${cmd_name}: check and release from project to ToTest
|
"""${cmd_name}: check and release from project to ToTest
|
||||||
@ -52,5 +63,7 @@ class CommandLineInterface(ToolBase.CommandLineInterface):
|
|||||||
${cmd_option_list}
|
${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)
|
ToTestReleaser(self.tool).release(project)
|
||||||
|
@ -15,6 +15,7 @@ import ToolBase
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
|
from enum import Enum
|
||||||
from xml.etree import cElementTree as ET
|
from xml.etree import cElementTree as ET
|
||||||
from osclib.stagingapi import StagingAPI
|
from osclib.stagingapi import StagingAPI
|
||||||
try:
|
try:
|
||||||
@ -28,6 +29,19 @@ from ttm.totest import ToTest
|
|||||||
class NotFoundException(Exception):
|
class NotFoundException(Exception):
|
||||||
pass
|
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):
|
class ToTestManager(ToolBase.ToolBase):
|
||||||
|
|
||||||
def __init__(self, tool):
|
def __init__(self, tool):
|
||||||
@ -95,11 +109,16 @@ class ToTestManager(ToolBase.ToolBase):
|
|||||||
return result.group(1)
|
return result.group(1)
|
||||||
raise NotFoundException("can't find %s ftp version" % project)
|
raise NotFoundException("can't find %s ftp version" % project)
|
||||||
|
|
||||||
# we don't lock the access to this attribute as the times these
|
# make sure to update the attribute as atomar as possible - as such
|
||||||
# snapshots are greatly different
|
# 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):
|
def update_status(self, status, snapshot):
|
||||||
status_dict = self.get_status_dict()
|
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
|
status_dict[status] = snapshot
|
||||||
text = yaml.safe_dump(status_dict)
|
text = yaml.safe_dump(status_dict)
|
||||||
self.api.attribute_value_save('ToTestManagerStatus', text)
|
self.api.attribute_value_save('ToTestManagerStatus', text)
|
||||||
@ -111,7 +130,7 @@ class ToTestManager(ToolBase.ToolBase):
|
|||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
def get_status(self, status):
|
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,
|
def release_package(self, project, package, set_release=None, repository=None,
|
||||||
target_project=None, target_repository=None):
|
target_project=None, target_repository=None):
|
||||||
@ -136,3 +155,32 @@ class ToTestManager(ToolBase.ToolBase):
|
|||||||
else:
|
else:
|
||||||
self.api.retried_POST(url)
|
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
|
||||||
|
@ -10,21 +10,18 @@
|
|||||||
# (C) 2019 coolo@suse.de, SUSE
|
# (C) 2019 coolo@suse.de, SUSE
|
||||||
# Distribute under GPLv2 or GPLv3
|
# Distribute under GPLv2 or GPLv3
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
import pika
|
import pika
|
||||||
|
import time
|
||||||
|
|
||||||
import osc
|
import osc
|
||||||
from osc.core import makeurl
|
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
|
from openqa_client.client import OpenQA_Client
|
||||||
|
|
||||||
# QA Results
|
|
||||||
QA_INPROGRESS = 1
|
|
||||||
QA_FAILED = 2
|
|
||||||
QA_PASSED = 3
|
|
||||||
|
|
||||||
class ToTestPublisher(ToTestManager):
|
class ToTestPublisher(ToTestManager):
|
||||||
|
|
||||||
def __init__(self, tool):
|
def __init__(self, tool):
|
||||||
@ -40,7 +37,7 @@ class ToTestPublisher(ToTestManager):
|
|||||||
"""Analyze the openQA jobs of a given snapshot Returns a QAResult"""
|
"""Analyze the openQA jobs of a given snapshot Returns a QAResult"""
|
||||||
|
|
||||||
if snapshot is None:
|
if snapshot is None:
|
||||||
return QA_FAILED
|
return QAResult.failed
|
||||||
|
|
||||||
jobs = self.find_openqa_results(snapshot)
|
jobs = self.find_openqa_results(snapshot)
|
||||||
|
|
||||||
@ -49,7 +46,7 @@ class ToTestPublisher(ToTestManager):
|
|||||||
|
|
||||||
if len(jobs) < self.project.jobs_num: # not yet scheduled
|
if len(jobs) < self.project.jobs_num: # not yet scheduled
|
||||||
self.logger.warning('we have only %s jobs' % len(jobs))
|
self.logger.warning('we have only %s jobs' % len(jobs))
|
||||||
return QA_INPROGRESS
|
return QAResult.inprogress
|
||||||
|
|
||||||
in_progress = False
|
in_progress = False
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
@ -121,12 +118,12 @@ class ToTestPublisher(ToTestManager):
|
|||||||
self.save_issues_to_ignore()
|
self.save_issues_to_ignore()
|
||||||
|
|
||||||
if len(self.failed_relevant_jobs) > 0:
|
if len(self.failed_relevant_jobs) > 0:
|
||||||
return QA_FAILED
|
return QAResult.failed
|
||||||
|
|
||||||
if in_progress:
|
if in_progress:
|
||||||
return QA_INPROGRESS
|
return QAResult.inprogress
|
||||||
|
|
||||||
return QA_PASSED
|
return QAResult.passed
|
||||||
|
|
||||||
def send_amqp_event(self, current_snapshot, current_result):
|
def send_amqp_event(self, current_snapshot, current_result):
|
||||||
amqp_url = osc.conf.config.get('ttm_amqp_url')
|
amqp_url = osc.conf.config.get('ttm_amqp_url')
|
||||||
@ -135,7 +132,7 @@ class ToTestPublisher(ToTestManager):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.logger.debug('Sending AMQP message')
|
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_topic = '%s.ttm.build.%s' % (self.project.base.lower(), inf)
|
||||||
msg_body = json.dumps({
|
msg_body = json.dumps({
|
||||||
'build': current_snapshot,
|
'build': current_snapshot,
|
||||||
@ -173,26 +170,26 @@ class ToTestPublisher(ToTestManager):
|
|||||||
group_id = self.openqa_group_id()
|
group_id = self.openqa_group_id()
|
||||||
|
|
||||||
if self.get_status('publishing') == current_snapshot or self.get_status('published') == current_snapshot:
|
if self.get_status('publishing') == current_snapshot or self.get_status('published') == current_snapshot:
|
||||||
self.logger.info('{} is already published'.format(current_snapshot))
|
self.logger.info('{} is already publishing'.format(current_snapshot))
|
||||||
return
|
return QAResult.inprogress
|
||||||
|
|
||||||
self.update_pinned_descr = False
|
self.update_pinned_descr = False
|
||||||
current_result = self.overall_result(current_snapshot)
|
current_result = self.overall_result(current_snapshot)
|
||||||
current_qa_version = self.current_qa_version()
|
current_qa_version = self.current_qa_version()
|
||||||
|
|
||||||
self.logger.info('current_snapshot %s: %s' %
|
self.logger.info('current_snapshot {}: {}'.format(current_snapshot, str(current_result)))
|
||||||
(current_snapshot, self._result2str(current_result)))
|
self.logger.debug('current_qa_version {}'.format(current_qa_version))
|
||||||
self.logger.debug('current_qa_version %s', current_qa_version)
|
|
||||||
|
|
||||||
self.send_amqp_event(current_snapshot, current_result)
|
self.send_amqp_event(current_snapshot, current_result)
|
||||||
|
|
||||||
if current_result == QA_FAILED:
|
if current_result == QAResult.failed:
|
||||||
self.update_status('failed', current_snapshot)
|
self.update_status('failed', current_snapshot)
|
||||||
|
return QAResult.failed
|
||||||
else:
|
else:
|
||||||
self.update_status('failed', '')
|
self.update_status('failed', '')
|
||||||
|
|
||||||
if current_result != QA_PASSED:
|
if current_result != QAResult.passed:
|
||||||
return
|
return QAResult.inprogress
|
||||||
|
|
||||||
if current_qa_version != current_snapshot:
|
if current_qa_version != current_snapshot:
|
||||||
# We reached a very bad status: openQA testing is 'done', but not of the same version
|
# 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.publish_factory_totest()
|
||||||
self.write_version_to_dashboard('snapshot', current_snapshot)
|
self.write_version_to_dashboard('snapshot', current_snapshot)
|
||||||
self.update_status('publishing', 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)
|
self.update_status('published', current_snapshot)
|
||||||
group_id = self.openqa_group_id()
|
group_id = self.openqa_group_id()
|
||||||
if not group_id:
|
if not group_id:
|
||||||
@ -231,14 +243,6 @@ class ToTestPublisher(ToTestManager):
|
|||||||
jobs.append(job)
|
jobs.append(job)
|
||||||
return jobs
|
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):
|
def add_published_tag(self, group_id, snapshot):
|
||||||
if self.dryrun:
|
if self.dryrun:
|
||||||
return
|
return
|
||||||
@ -301,13 +305,13 @@ class ToTestPublisher(ToTestManager):
|
|||||||
|
|
||||||
def publish_factory_totest(self):
|
def publish_factory_totest(self):
|
||||||
self.logger.info('Publish test project content')
|
self.logger.info('Publish test project content')
|
||||||
|
if self.dryrun or self.project.do_not_release:
|
||||||
|
return
|
||||||
if self.project.container_products:
|
if self.project.container_products:
|
||||||
self.logger.info('Releasing container products from ToTest')
|
self.logger.info('Releasing container products from ToTest')
|
||||||
for container in self.project.container_products:
|
for container in self.project.container_products:
|
||||||
self.release_package(self.project.test_project, container.package,
|
self.release_package(self.project.test_project, container.package,
|
||||||
repository=self.project.totest_container_repo)
|
repository=self.project.totest_container_repo)
|
||||||
if not (self.dryrun or self.project.do_not_release):
|
self.api.switch_flag_in_prj(
|
||||||
self.api.switch_flag_in_prj(
|
self.project.test_project, flag='publish', state='enable',
|
||||||
self.project.test_project, flag='publish', state='enable',
|
repository=self.project.product_repo)
|
||||||
repository=self.project.product_repo)
|
|
||||||
|
|
||||||
|
@ -27,33 +27,25 @@ class ToTestReleaser(ToTestManager):
|
|||||||
def release(self, project, force=False):
|
def release(self, project, force=False):
|
||||||
self.setup(project)
|
self.setup(project)
|
||||||
|
|
||||||
current_snapshot = self.get_status('testing')
|
testing_snapshot = self.get_status('testing')
|
||||||
new_snapshot = self.version_from_project()
|
new_snapshot = self.version_from_project()
|
||||||
|
|
||||||
# not overwriting
|
# not overwriting
|
||||||
if new_snapshot == current_snapshot:
|
if new_snapshot == testing_snapshot:
|
||||||
self.logger.debug('no change in snapshot version')
|
self.logger.debug('no change in snapshot version')
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_snapshot:
|
if testing_snapshot != self.get_status('failed') and testing_snapshot != self.get_status('published'):
|
||||||
testing_snapshot = self.get_status('testing')
|
self.logger.debug('Snapshot {} is still in progress'.format(testing_snapshot))
|
||||||
if testing_snapshot != self.get_status('failed') and testing_snapshot != self.get_status('publishing'):
|
return
|
||||||
self.logger.debug('Snapshot {} is still in progress'.format(testing_snapshot))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info('current_snapshot %s', current_snapshot)
|
self.logger.info('testing snapshot %s', testing_snapshot)
|
||||||
self.logger.debug('new_snapshot %s', new_snapshot)
|
self.logger.debug('new snapshot %s', new_snapshot)
|
||||||
|
|
||||||
if not self.is_snapshotable():
|
if not self.is_snapshotable():
|
||||||
self.logger.debug('not snapshotable')
|
self.logger.debug('not snapshotable')
|
||||||
return
|
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_totest(new_snapshot)
|
||||||
self.update_status('testing', new_snapshot)
|
self.update_status('testing', new_snapshot)
|
||||||
self.update_status('failed', '')
|
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,
|
return self.iso_build_version(self.project.name, self.project.image_products[0].package,
|
||||||
arch=self.project.image_products[0].archs[0])
|
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):
|
def maxsize_for_package(self, package):
|
||||||
if re.match(r'.*-mini-.*', package):
|
if re.match(r'.*-mini-.*', package):
|
||||||
return 737280000 # a CD needs to match
|
return 737280000 # a CD needs to match
|
||||||
@ -267,4 +229,3 @@ class ToTestReleaser(ToTestManager):
|
|||||||
repository=self.project.product_repo)
|
repository=self.project.product_repo)
|
||||||
|
|
||||||
self._release(set_release=release)
|
self._release(set_release=release)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user