openSUSE-release-tools/osc-staging.py

428 lines
16 KiB
Python
Raw Normal View History

2013-09-04 15:11:27 +02:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
2013-09-04 15:11:27 +02:00
#
2014-02-10 10:19:37 +01:00
# (C) 2014 mhrusecky@suse.cz, openSUSE.org
# (C) 2014 tchvatal@suse.cz, openSUSE.org
2013-09-04 15:11:27 +02:00
# Distribute under GPLv2 or GPLv3
import logging
import os.path
import re
import sys
from urllib2 import HTTPError
from xml.etree import cElementTree as ET
from osc import cmdln, oscerr
from osc.core import delete_package
from osc.core import delete_project
from osc.core import get_request
from osc.core import make_meta_url
from osc.core import makeurl
from osc.core import meta_get_packagelist
from osc.core import metafile
from osc.core import http_GET
from osc.core import http_POST
from osc.core import http_PUT
from osc.core import link_pac
from osc.core import server_diff
# Expand sys.path to search modules inside the pluging directory
_plugin_dir = os.path.expanduser('~/.osc-plugins')
sys.path.append(_plugin_dir)
2014-02-12 17:58:19 +01:00
from osclib.stagingapi import StagingAPI
2013-09-04 15:11:27 +02:00
OSC_STAGING_VERSION='0.0.1'
def _print_version(self):
""" Print version information about this extension. """
print '%s'%(self.OSC_STAGING_VERSION)
2013-09-04 15:11:27 +02:00
quit(0)
2014-02-10 14:05:18 +01:00
def _get_parent(apiurl, project, repo = "standard"):
"""
Finds what is the parent project of the staging project
2014-02-10 10:48:29 +01:00
:param apiurl: url to the OBS api
:param project: staging project to check
:param repo: which repository to follow
2014-02-10 10:48:29 +01:00
:return name of the parent project
"""
url = make_meta_url("prj", project, apiurl)
data = http_GET(url).readlines()
root = ET.fromstring(''.join(data))
p_path = root.find("repository[@name='%s']/path"%(repo))
if not p_path:
logging.error("Project '%s' has no repository named '%s'"%(project, repo))
return None
return p_path['project']
# Get last build results (optionally only for specified repo/arch)
# Works even when rebuild is triggered
def _get_build_res(opts, prj, repo=None, arch=None):
2013-12-13 13:22:55 +01:00
query = {}
query['lastbuild'] = 1
if repo is not None:
2013-12-13 13:22:55 +01:00
query['repository'] = repo
if arch is not None:
2013-12-13 13:22:55 +01:00
query['arch'] = arch
u = makeurl(opts.apiurl, ['build', prj, '_result'], query=query)
2013-12-13 13:22:55 +01:00
f = http_GET(u)
return f.readlines()
def _get_changed(opts, project, everything):
ret = []
# Check for local changes
for pkg in meta_get_packagelist(opts.apiurl, project):
if len(ret) != 0 and not everything:
2013-09-04 15:11:27 +02:00
break
f = http_GET(makeurl(opts.apiurl, ['source', project, pkg]))
2013-09-04 15:11:27 +02:00
linkinfo = ET.parse(f).getroot().find('linkinfo')
if linkinfo is None:
ret.append({'pkg': pkg, 'code': 'NOT_LINK', 'msg': 'Not a source link'})
2013-09-04 15:11:27 +02:00
continue
if linkinfo.get('error'):
ret.append({'pkg': pkg, 'code': 'BROKEN', 'msg': 'Broken source link'})
2013-09-04 15:11:27 +02:00
continue
t = linkinfo.get('project')
p = linkinfo.get('package')
r = linkinfo.get('revision')
if len(server_diff(opts.apiurl, t, p, r, project, pkg, None, True)) > 0:
ret.append({'pkg': pkg, 'code': 'MODIFIED', 'msg': 'Has local modifications', 'pprj': t, 'ppkg': p})
2013-09-04 15:11:27 +02:00
continue
return ret
# Checks the state of staging repo (local modifications, regressions, ...)
def _staging_check(self, project, check_everything, opts):
"""
Checks whether project does not contain local changes
and whether it contains only links
:param project: staging project to check
:param everything: do not stop on first verification failure
:param opts: pointer to options
"""
ret = 0
2014-02-11 11:08:02 +01:00
chng = _get_changed(opts, project, check_everything)
if len(chng) > 0:
for pair in chng:
print >>sys.stderr, 'Error: Package "%s": %s'%(pair['pkg'],pair['msg'])
print >>sys.stderr, "Error: Check for local changes failed"
ret = 1
else:
print "Check for local changes passed"
# Check for regressions
root = None
if ret == 0 or check_everything:
print "Getting build status, this may take a while"
# Get staging project results
2014-02-11 11:44:59 +01:00
f = _get_build_res(opts, project)
root = ET.fromstring(''.join(f))
# Get parent project
m_url = make_meta_url("prj", project, opts.apiurl)
m_data = http_GET(m_url).readlines()
m_root = ET.fromstring(''.join(m_data))
print "Comparing build statuses, this may take a while"
# Iterate through all repos/archs
if root is not None and root.find('result') is not None:
for results in root.findall('result'):
if ret != 0 and not check_everything:
break
if results.get("state") not in [ "published", "unpublished" ]:
print >>sys.stderr, "Warning: Building not finished yet for %s/%s (%s)!"%(results.get("repository"),results.get("arch"),results.get("state"))
ret |= 2
# Get parent project results for this repo/arch
p_project = m_root.find("repository[@name='%s']/path"%(results.get("repository")))
if p_project == None:
print >>sys.stderr, "Error: Can't get path for '%s'!"%results.get("repository")
ret |= 4
continue
2014-02-11 11:44:59 +01:00
f = _get_build_res(opts, p_project.get("project"), repo=results.get("repository"), arch=results.get("arch"))
p_root = ET.fromstring(''.join(f))
# Find corresponding set of results in parent project
p_results = p_root.find("result[@repository='%s'][@arch='%s']"%(results.get("repository"),results.get("arch")))
if p_results == None:
print >>sys.stderr, "Error: Inconsistent setup!"
ret |= 4
else:
# Iterate through packages
for node in results:
if ret != 0 and not check_everything:
break
result = node.get("code")
# Skip not rebuilt
2013-12-13 13:22:55 +01:00
if result in [ "blocked", "building", "disabled" "excluded", "finished", "unknown", "unpublished", "published" ]:
continue
# Get status of package in parent project
p_node = p_results.find("status[@package='%s']"%(node.get("package")))
if p_node == None:
p_result = None
else:
p_result = p_node.get("code")
# Skip packages not built in parent project
if p_result in [ None, "disabled", "excluded", "unknown", "unresolvable" ]:
continue
# Find regressions
if result in [ "broken", "failed", "unresolvable" ] and p_result not in [ "blocked", "broken", "failed" ]:
print >>sys.stderr, "Error: Regression (%s -> %s) in package '%s' in %s/%s!"%(p_result, result, node.get("package"),results.get("repository"),results.get("arch"))
ret |= 8
# Find fixed builds
if result in [ "succeeded" ] and result != p_result:
print "Package '%s' fixed (%s -> %s) in staging for %s/%s."%(node.get("package"), p_result, result, results.get("repository"),results.get("arch"))
if ret != 0:
print "Staging check failed!"
else:
print "Staging check succeeded!"
2013-09-04 15:11:27 +02:00
return ret
def _staging_remove(self, project, opts):
"""
Remove staging project.
:param project: staging project to delete
:param opts: pointer to options
"""
chng = _get_changed(opts, project, True)
if len(chng) > 0:
print('Staging project "%s" is not clean:'%(project))
print('')
for pair in chng:
print(' * %s : %s'%(pair['pkg'],pair['msg']))
print('')
print('Really delete? (N/y)')
answer = sys.stdin.readline()
if not re.search("^\s*[Yy]", answer):
print('Aborting...')
exit(1)
delete_project(opts.apiurl, project, force=True, msg=None)
2013-09-04 15:11:27 +02:00
print("Deleted.")
return
2013-09-04 15:11:27 +02:00
def _staging_submit_devel(self, project, opts):
"""
Generate new review requests for devel-projects based on our staging changes.
:param project: staging project to submit into devel projects
"""
chng = _get_changed(opts, project, True)
msg = "Fixes from staging project %s" % project
if opts.message is not None:
msg = opts.message
if len(chng) > 0:
for pair in chng:
if pair['code'] != 'MODIFIED':
print >>sys.stderr, 'Error: Package "%s": %s'%(pair['pkg'],pair['msg'])
else:
print('Sending changes back %s/%s -> %s/%s'%(project,pair['pkg'],pair['pprj'],pair['ppkg']))
action_xml = '<request>';
action_xml += ' <action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" />' % (project, pair['pkg'], pair['pprj'], pair['ppkg'])
action_xml += ' </action>'
action_xml += ' <state name="new"/> <description>%s</description>' % msg
action_xml += '</request>'
u = makeurl(opts.apiurl, ['request'], query='cmd=create&addrevision=1')
f = http_POST(u, data=action_xml)
root = ET.parse(f).getroot()
print("Created request %s" % (root.get('id')))
else:
print("No changes to submit")
2013-09-04 15:11:27 +02:00
return
def _staging_change_review_state(self, opts, id, newstate, by_group='', by_user='', message='', supersed=None):
""" taken from osc/osc/core.py, improved:
- verbose option added,
- empty by_user=& removed.
- numeric id can be int().
"""
query = {'cmd': 'changereviewstate', 'newstate': newstate }
if by_group: query['by_group'] = by_group
if by_user: query['by_user'] = by_user
if supersed: query['superseded_by'] = supersed
# if message: query['comment'] = message
u = makeurl(opts.apiurl, ['request', str(id)], query=query)
f = http_POST(u, data=message)
root = ET.parse(f).getroot()
return root.attrib['code']
def _staging_get_rings(self, opts):
ret = dict()
for prj in ['openSUSE:Factory:Rings:0-Bootstrap', 'openSUSE:Factory:Rings:1-MinimalX']:
u = makeurl(opts.apiurl, ['source', prj])
f = http_GET(u)
for entry in ET.parse(f).getroot().findall('entry'):
ret[entry.attrib['name']] = prj
return ret
def _staging_one_request(self, rq, opts):
if (opts.verbose):
ET.dump(rq)
print(opts)
id = int(rq.get('id'))
act_id = 0
actions = rq.findall('action')
act = actions[0]
2014-02-10 10:19:37 +01:00
tprj = act.find('target').get('project')
tpkg = act.find('target').get('package')
e = []
if not tpkg:
e.append('no target/package in request %d, action %d; ' % (id, act_id))
if not tprj:
e.append('no target/project in request %d, action %d; ' % (id, act_id))
# it is no error, if the target package dies not exist
ring = self.rings.get(tpkg, None)
if ring is None:
msg = "ok"
else:
stage_info = self.packages_staged.get(tpkg, ('', 0))
if stage_info[0] == self.letter_to_accept and int(stage_info[1]) == id:
# TODO make api for that
stprj = 'openSUSE:Factory:Staging:%s' % self.letter_to_accept
msg = 'ok, tested in %s' % stprj
delete_package(opts.apiurl, stprj, tpkg, msg='done')
elif stage_info[1] != 0 and int(stage_info[1]) != id:
2014-02-10 14:05:18 +01:00
print stage_info
print "osc staging select %s %s" % (stage_info[0], id)
2014-02-10 14:05:18 +01:00
return
elif stage_info[1] != 0: # keep silent about those already asigned
return
else:
print "Request(%d): %s -> %s" % (id, tpkg, ring)
return
self._staging_change_review_state(opts, id, 'accepted', by_group='factory-staging', message=msg)
2014-02-11 11:45:53 +01:00
@cmdln.option('-e', '--everything', action='store_true',
2013-09-04 15:11:27 +02:00
help='during check do not stop on first first issue and show them all')
@cmdln.option('-p', '--parent', metavar='TARGETPROJECT',
help='manually specify different parent project during creation of staging')
@cmdln.option('-m', '--message', metavar='TEXT',
help='manually specify different parent project during creation of staging')
2013-09-04 15:11:27 +02:00
@cmdln.option('-v', '--version', action='store_true',
help='show version of the plugin')
def do_staging(self, subcmd, opts, *args):
"""${cmd_name}: Commands to work with staging projects
"check" will check if all packages are links without changes
"remove" (or "r") will delete the staging project into submit requests for openSUSE:Factory
"submit-devel" (or "s") will create review requests for changed packages in staging project
into their respective devel projects to obtain approval from maitnainers for pushing the
changes to openSUSE:Factory
"freeze" will freeze the sources of the project's links (not affecting the packages actually in)
2014-02-10 10:19:37 +01:00
"accept" will accept all requests openSUSE:Factory:Staging:<LETTER>
"list" will pick the requests not in rings
"select" will add requests to the project
2013-09-04 15:11:27 +02:00
Usage:
osc staging check [--everything] REPO
osc staging remove REPO
osc staging submit-devel [-m message] REPO
osc staging freeze PROJECT
osc staging list
osc staging select LETTER REQUEST...
osc staging accept LETTER
osc staging cleanup_rings
2013-09-04 15:11:27 +02:00
"""
if opts.version:
self._print_version()
# verify the argument counts match the commands
2014-02-10 19:59:45 +01:00
if len(args) == 0:
raise oscerr.WrongArgs('No command given, see "osc help staging"!')
2013-09-04 15:11:27 +02:00
cmd = args[0]
if cmd in ['submit-devel', 's', 'remove', 'r', 'accept', 'freeze']:
2013-09-04 15:11:27 +02:00
min_args, max_args = 1, 1
elif cmd in ['check']:
min_args, max_args = 1, 2
elif cmd in ['select']:
min_args, max_args = 2, None
elif cmd in ['list', 'cleanup_rings']:
min_args, max_args = 0, 0
2013-09-04 15:11:27 +02:00
else:
2014-02-10 19:59:45 +01:00
raise oscerr.WrongArgs('Unknown command: %s'%(cmd))
2013-09-04 15:11:27 +02:00
if len(args) - 1 < min_args:
raise oscerr.WrongArgs('Too few arguments.')
if not max_args is None and len(args) - 1 > max_args:
raise oscerr.WrongArgs('Too many arguments.')
# init the obs access
opts.apiurl = self.get_api_url()
opts.verbose = False
self.rings = self._staging_get_rings(opts)
2014-02-12 17:58:19 +01:00
api = StagingAPI(opts.apiurl)
2013-09-04 15:11:27 +02:00
# call the respective command and parse args by need
if cmd in ['push', 'p']:
project = args[1]
self._staging_push(project, opts)
elif cmd in ['check']:
project = args[1]
2014-02-14 19:18:08 +01:00
return api.check_project_status(project)
2013-09-04 15:11:27 +02:00
elif cmd in ['remove', 'r']:
project = args[1]
self._staging_remove(project, opts)
elif cmd in ['submit-devel', 's']:
project = args[1]
self._staging_submit_devel(project, opts)
elif cmd in ['freeze']:
import osclib.freeze_command
osclib.freeze_command.FreezeCommand(opts.apiurl).perform(args[1])
elif cmd in ['select']:
# TODO: have an api call for that
stprj = 'openSUSE:Factory:Staging:%s' % args[1]
for i in range(2, len(args)):
api.rq_to_prj(args[i], stprj)
elif cmd in ['cleanup_rings']:
import osclib.cleanup_rings
osclib.cleanup_rings.CleanupRings(opts.apiurl).perform()
elif cmd in ['accept', 'list']:
self.letter_to_accept = None
if cmd == 'accept':
self.letter_to_accept = args[1]
self.packages_staged = dict()
for prj in api.get_staging_projects():
meta = api.get_prj_pseudometa(prj)
for req in meta['requests']:
self.packages_staged[req['package']] = (prj[-1], req['id'])
# xpath query, using the -m, -r, -s options
where = "@by_group='factory-staging'+and+@state='new'"
url = makeurl(opts.apiurl, ['search','request'], "match=state/@name='review'+and+review["+where+"]")
f = http_GET(url)
root = ET.parse(f).getroot()
for rq in root.findall('request'):
tprj = rq.find('action/target').get('project')
self._staging_one_request(rq, opts)
if self.letter_to_accept:
url = makeurl(opts.apiurl, ['source', 'openSUSE:Factory:Staging:%s' % self.letter_to_accept])
f = http_GET(url)
root = ET.parse(f).getroot()
print ET.tostring(root)