2019-05-16 06:39:08 +02:00
#!/usr/bin/python3
2016-12-29 00:34:56 -06:00
import os
2018-08-13 22:47:01 -05:00
import re
2016-12-29 00:34:56 -06:00
import shutil
import subprocess
import sys
try :
from xml . etree import cElementTree as ET
except ImportError :
import cElementTree as ET
2017-10-11 22:19:24 -05:00
import osc . conf
2016-12-29 00:34:56 -06:00
import osc . core
2019-05-16 13:13:25 +02:00
from osc . util . helper import decode_list
2018-08-16 23:35:10 -05:00
from osclib . conf import Config
2018-01-17 18:08:40 -06:00
from osclib . core import devel_project_get
2018-01-17 20:36:03 -06:00
from osclib . core import devel_project_fallback
2018-11-06 16:15:20 -06:00
from osclib . core import group_members
2019-05-16 06:39:08 +02:00
from urllib . error import HTTPError
2018-11-16 08:32:25 +01:00
2016-12-29 00:34:56 -06:00
import ReviewBot
2018-03-09 13:37:23 +01:00
from osclib . conf import str2bool
2016-12-29 00:34:56 -06:00
class CheckSource ( ReviewBot . ReviewBot ) :
SCRIPT_PATH = os . path . dirname ( os . path . realpath ( __file__ ) )
def __init__ ( self , * args , * * kwargs ) :
ReviewBot . ReviewBot . __init__ ( self , * args , * * kwargs )
2017-06-20 16:29:19 -05:00
# ReviewBot options.
2018-03-16 14:21:41 -05:00
self . request_default_return = True
2017-06-20 16:29:19 -05:00
2016-12-29 00:34:56 -06:00
self . skip_add_reviews = False
2017-10-11 22:19:24 -05:00
def target_project_config ( self , project ) :
# Load project config and allow for remote entries.
2018-08-16 23:35:10 -05:00
config = Config . get ( self . apiurl , project )
2017-10-11 22:19:24 -05:00
2018-06-28 12:13:26 -05:00
self . single_action_require = str2bool ( config . get ( ' check-source-single-action-require ' , ' False ' ) )
2018-03-09 13:37:23 +01:00
self . ignore_devel = not str2bool ( config . get ( ' devel-project-enforce ' , ' False ' ) )
2018-03-16 14:09:02 -05:00
self . in_air_rename_allow = str2bool ( config . get ( ' check-source-in-air-rename-allow ' , ' False ' ) )
2018-03-09 13:45:14 +01:00
self . add_review_team = str2bool ( config . get ( ' check-source-add-review-team ' , ' True ' ) )
2017-10-11 22:19:24 -05:00
self . review_team = config . get ( ' review-team ' )
2018-08-16 23:29:25 -05:00
self . staging_group = config . get ( ' staging-group ' )
2017-10-11 22:19:24 -05:00
self . repo_checker = config . get ( ' repo-checker ' )
self . devel_whitelist = config . get ( ' devel-whitelist ' , ' ' ) . split ( )
2018-09-12 11:48:23 +02:00
self . skip_add_reviews = False
2017-10-11 22:19:24 -05:00
2018-06-28 12:11:24 -05:00
if self . action . type == ' maintenance_incident ' :
# The workflow effectively enforces the names to match and the
# parent code sets target_package from source_package so this check
# becomes useless and awkward to perform.
self . in_air_rename_allow = True
2018-06-28 12:13:26 -05:00
# The target project will be set to product and thus inherit
# settings, but override since real target is not product.
self . single_action_require = False
2018-06-28 12:25:24 -05:00
# It might make sense to supersede maintbot, but for now.
self . skip_add_reviews = True
2016-12-29 00:34:56 -06:00
def check_source_submission ( self , source_project , source_package , source_revision , target_project , target_package ) :
super ( CheckSource , self ) . check_source_submission ( source_project , source_package , source_revision , target_project , target_package )
2017-10-11 22:19:24 -05:00
self . target_project_config ( target_project )
2016-12-29 00:34:56 -06:00
2018-06-28 12:13:26 -05:00
if self . single_action_require and len ( self . request . actions ) != 1 :
self . review_messages [ ' declined ' ] = ' Only one action per request allowed '
return False
2018-09-13 20:49:56 +08:00
if target_package . startswith ( ' 00 ' ) or target_package . startswith ( ' _ ' ) :
self . review_messages [ ' accepted ' ] = ' Skipping all checks for product related packages '
2018-03-15 15:43:37 +01:00
return True
2018-06-28 12:10:19 -05:00
inair_renamed = target_package != source_package
2016-12-29 00:34:56 -06:00
if not self . ignore_devel :
2017-10-11 22:14:42 -05:00
self . logger . info ( ' checking if target package exists and has devel project ' )
2018-01-17 18:08:40 -06:00
devel_project , devel_package = devel_project_get ( self . apiurl , target_project , target_package )
2016-12-29 00:34:56 -06:00
if devel_project :
2017-02-10 11:19:25 -06:00
if ( source_project != devel_project or source_package != devel_package ) and \
not ( source_project == target_project and source_package == target_package ) :
# Not from proper devel project/package and not self-submission.
2016-12-29 00:34:56 -06:00
self . review_messages [ ' declined ' ] = ' Expected submission from devel package %s / %s ' % ( devel_project , devel_package )
return False
else :
# Check to see if other packages exist with the same source project
# which indicates that the project has already been used as devel.
if not self . is_devel_project ( source_project , target_project ) :
2018-01-02 13:27:10 +01:00
self . review_messages [ ' declined ' ] = (
' %s is not a devel project of %s , submit the package to a devel project first. '
' See https://en.opensuse.org/openSUSE:How_to_contribute_to_Factory#How_to_request_a_new_devel_project for details. '
) % ( source_project , target_project )
2016-12-29 00:34:56 -06:00
return False
2018-06-28 12:10:19 -05:00
else :
if source_project . endswith ( ' :Update ' ) :
# Allow for submission like:
# - source: openSUSE:Leap:15.0:Update/google-compute-engine.8258
# - target: openSUSE:Leap:15.1/google-compute-engine
# Note: home:jberry:Update would also be allowed via this condition,
# but that should be handled by leaper and human review.
2018-08-13 22:47:01 -05:00
# Ignore a dot in package name (ex. tpm2.0-abrmd) and instead
# only look for ending in dot number.
match = re . match ( r ' (.*) \ . \ d+$ ' , source_package )
if match :
inair_renamed = target_package != match . group ( 1 )
2018-06-28 12:10:19 -05:00
if not self . in_air_rename_allow and inair_renamed :
self . review_messages [ ' declined ' ] = ' Source and target package names must match '
return False
2016-12-29 00:34:56 -06:00
# Checkout and see if renaming package screws up version parsing.
dir = os . path . expanduser ( ' ~/co/ %s ' % self . request . reqid )
if os . path . exists ( dir ) :
2019-05-11 14:25:02 +02:00
self . logger . warning ( ' directory %s already exists ' % dir )
2016-12-29 00:34:56 -06:00
shutil . rmtree ( dir )
os . makedirs ( dir )
os . chdir ( dir )
old_info = { ' version ' : None }
try :
CheckSource . checkout_package ( self . apiurl , target_project , target_package , pathname = dir ,
server_service_files = True , expand_link = True )
shutil . rmtree ( os . path . join ( target_package , ' .osc ' ) )
os . rename ( target_package , ' _old ' )
2017-04-19 09:21:45 -05:00
old_info = self . package_source_parse ( target_project , target_package )
2018-11-16 08:32:25 +01:00
except HTTPError :
2016-12-29 00:34:56 -06:00
self . logger . error ( ' failed to checkout %s / %s ' % ( target_project , target_package ) )
CheckSource . checkout_package ( self . apiurl , source_project , source_package , revision = source_revision ,
pathname = dir , server_service_files = True , expand_link = True )
os . rename ( source_package , target_package )
shutil . rmtree ( os . path . join ( target_package , ' .osc ' ) )
2017-04-19 09:21:45 -05:00
new_info = self . package_source_parse ( source_project , source_package , source_revision )
2016-12-29 00:34:56 -06:00
if new_info [ ' name ' ] != target_package :
shutil . rmtree ( dir )
self . review_messages [ ' declined ' ] = " A package submitted as %s has to build as ' Name: %s ' - found Name ' %s ' " % ( target_package , target_package , new_info [ ' name ' ] )
return False
2017-08-18 15:18:04 -05:00
# Run check_source.pl script and interpret output.
source_checker = os . path . join ( CheckSource . SCRIPT_PATH , ' check_source.pl ' )
2016-12-29 00:34:56 -06:00
civs = ' '
new_version = None
if old_info [ ' version ' ] and old_info [ ' version ' ] != new_info [ ' version ' ] :
new_version = new_info [ ' version ' ]
civs + = " NEW_VERSION= ' {} ' " . format ( new_version )
civs + = ' LC_ALL=C perl %s _old %s 2>&1 ' % ( source_checker , target_package )
p = subprocess . Popen ( civs , shell = True , stdout = subprocess . PIPE , close_fds = True )
ret = os . waitpid ( p . pid , 0 ) [ 1 ]
2019-05-16 13:13:25 +02:00
checked = decode_list ( p . stdout . readlines ( ) )
2016-12-29 00:34:56 -06:00
2019-05-16 13:13:25 +02:00
output = ' ' . join ( checked ) . replace ( ' \033 ' , ' ' )
2016-12-29 00:34:56 -06:00
os . chdir ( ' /tmp ' )
2017-11-22 17:29:14 +08:00
# ret = 0 : Good
# ret = 1 : Bad
# ret = 2 : Bad but can be non-fatal in some cases
if ret > 1 and target_project . startswith ( ' openSUSE:Leap: ' ) and ( source_project . startswith ( ' SUSE:SLE-15: ' ) or source_project . startswith ( ' openSUSE:Factory ' ) ) :
pass
elif ret != 0 :
2016-12-29 00:34:56 -06:00
shutil . rmtree ( dir )
self . review_messages [ ' declined ' ] = " Output of check script: \n " + output
return False
shutil . rmtree ( dir )
self . review_messages [ ' accepted ' ] = ' Check script succeeded '
if len ( checked ) :
self . review_messages [ ' accepted ' ] + = " \n \n Output of check script (non-fatal): \n " + output
if not self . skip_add_reviews :
2018-03-09 13:45:14 +01:00
if self . add_review_team and self . review_team is not None :
2017-08-18 15:15:26 -05:00
self . add_review ( self . request , by_group = self . review_team , msg = ' Please review sources ' )
2016-12-29 00:34:56 -06:00
2017-04-19 09:30:57 -05:00
if self . only_changes ( ) :
self . logger . debug ( ' only .changes modifications ' )
2018-11-06 16:15:20 -06:00
if self . staging_group and self . review_user in group_members ( self . apiurl , self . staging_group ) :
if not self . dryrun :
osc . core . change_review_state ( self . apiurl , str ( self . request . reqid ) , ' accepted ' ,
by_group = self . staging_group ,
message = ' skipping the staging process since only .changes modifications ' )
else :
self . logger . debug ( ' unable to skip staging review since not a member of staging group ' )
2017-04-19 09:30:57 -05:00
elif self . repo_checker is not None :
2016-12-29 00:34:56 -06:00
self . add_review ( self . request , by_user = self . repo_checker , msg = ' Please review build success ' )
return True
def is_devel_project ( self , source_project , target_project ) :
2017-10-11 22:19:24 -05:00
if source_project in self . devel_whitelist :
2016-12-29 00:34:56 -06:00
return True
2017-07-18 17:08:02 -05:00
# Allow any projects already used as devel projects for other packages.
2016-12-29 00:34:56 -06:00
search = {
' package ' : " @project= ' %s ' and devel/@project= ' %s ' " % ( target_project , source_project ) ,
}
result = osc . core . search ( self . apiurl , * * search )
return result [ ' package ' ] . attrib [ ' matches ' ] != ' 0 '
@staticmethod
def checkout_package ( * args , * * kwargs ) :
_stdout = sys . stdout
2019-05-16 13:13:25 +02:00
sys . stdout = open ( os . devnull , ' w ' )
2016-12-29 00:34:56 -06:00
try :
result = osc . core . checkout_package ( * args , * * kwargs )
finally :
sys . stdout = _stdout
return result
2017-04-19 09:21:45 -05:00
def package_source_parse ( self , project , package , revision = None ) :
2016-12-29 00:34:56 -06:00
query = { ' view ' : ' info ' , ' parse ' : 1 }
if revision :
query [ ' rev ' ] = revision
url = osc . core . makeurl ( self . apiurl , [ ' source ' , project , package ] , query )
ret = { ' name ' : None , ' version ' : None }
try :
xml = ET . parse ( osc . core . http_GET ( url ) ) . getroot ( )
2018-11-16 08:32:25 +01:00
except HTTPError as e :
2016-12-29 00:34:56 -06:00
self . logger . error ( ' ERROR in URL %s [ %s ] ' % ( url , e ) )
return ret
# ET boolean check fails.
if xml . find ( ' name ' ) is not None :
ret [ ' name ' ] = xml . find ( ' name ' ) . text
if xml . find ( ' version ' ) is not None :
ret [ ' version ' ] = xml . find ( ' version ' ) . text
return ret
2017-04-19 09:30:57 -05:00
def only_changes ( self ) :
u = osc . core . makeurl ( self . apiurl , [ ' request ' , self . request . reqid ] ,
{ ' cmd ' : ' diff ' , ' view ' : ' xml ' } )
try :
diff = ET . parse ( osc . core . http_POST ( u ) ) . getroot ( )
for f in diff . findall ( ' action/sourcediff/files/file/*[@name] ' ) :
if not f . get ( ' name ' ) . endswith ( ' .changes ' ) :
return False
return True
except :
pass
return False
2016-12-29 00:34:56 -06:00
def check_action_add_role ( self , request , action ) :
# Decline add_role request (assumed the bot acting on requests to Factory or similar).
message = ' Roles to packages are granted in the devel project, not in %s . ' % action . tgt_project
if action . tgt_package is not None :
2018-01-17 20:36:03 -06:00
project , package = devel_project_fallback ( self . apiurl , action . tgt_project , action . tgt_package )
2018-01-17 18:08:40 -06:00
message + = ' Send this request to {} / {} . ' . format ( project , package )
2016-12-29 00:34:56 -06:00
self . review_messages [ ' declined ' ] = message
return False
2018-09-17 17:11:33 -05:00
def check_action_delete_package ( self , request , action ) :
2018-01-03 20:01:12 -06:00
self . target_project_config ( action . tgt_project )
2016-12-29 00:34:56 -06:00
try :
result = osc . core . show_project_sourceinfo ( self . apiurl , action . tgt_project , True , ( action . tgt_package ) )
root = ET . fromstring ( result )
2018-11-16 08:32:25 +01:00
except HTTPError :
2016-12-29 00:34:56 -06:00
return None
2017-10-12 19:11:01 +08:00
# Decline the delete request if there is another delete/submit request against the same package
query = " match=state/@name= ' new ' +and+(action/target/@project= ' {} ' +and+action/target/@package= ' {} ' ) " \
" +and+(action/@type= ' delete ' +or+action/@type= ' submit ' ) " . format ( action . tgt_project , action . tgt_package )
url = osc . core . makeurl ( self . apiurl , [ ' search ' , ' request ' ] , query )
matches = ET . parse ( osc . core . http_GET ( url ) ) . getroot ( )
if int ( matches . attrib [ ' matches ' ] ) > 1 :
ids = [ rq . attrib [ ' id ' ] for rq in matches . findall ( ' request ' ) ]
self . review_messages [ ' declined ' ] = " There is a pending request %s to %s / %s in process. " % ( ' , ' . join ( ids ) , action . tgt_project , action . tgt_package )
return False
2019-01-09 12:58:54 +01:00
# Decline delete requests against linked flavor package
linked = root . find ( ' sourceinfo/linked ' )
if not ( linked is None or self . check_linked_package ( action , linked ) ) :
2016-12-29 00:34:56 -06:00
return False
2019-01-09 12:58:54 +01:00
if not self . ignore_devel :
self . devel_project_review_ensure ( request , action . tgt_project , action . tgt_package )
if not self . skip_add_reviews and self . repo_checker is not None :
self . add_review ( self . request , by_user = self . repo_checker , msg = ' Is this delete request safe? ' )
return True
def check_linked_package ( self , action , linked ) :
if linked . get ( ' project ' , action . tgt_project ) != action . tgt_project :
return True
linked_package = linked . get ( ' package ' )
self . review_messages [ ' declined ' ] = " Delete the package %s instead " % ( linked_package )
return False
2018-09-17 17:06:18 -05:00
def check_action_delete_project ( self , request , action ) :
# Presumably if the request is valid the bot should be disabled or
# overridden, but seems like no valid case for allowing this (see #1696).
self . review_messages [ ' declined ' ] = ' Deleting the {} project is not allowed. ' . format ( action . tgt_project )
return False
2018-09-17 17:10:01 -05:00
def check_action_delete_repository ( self , request , action ) :
if action . tgt_project . startswith ( ' openSUSE: ' ) :
self . review_messages [ ' declined ' ] = ' The repositories in the openSUSE:* namespace ' \
' are managed by the Release Managers. For suggesting changes, send a mail ' \
' to opensuse-releaseteam@opensuse.org with an explanation of why the change ' \
' makes sense. '
return False
self . review_messages [ ' accepted ' ] = ' unhandled: removing repository '
return True
2016-12-29 00:34:56 -06:00
class CommandLineInterface ( ReviewBot . CommandLineInterface ) :
def __init__ ( self , * args , * * kwargs ) :
ReviewBot . CommandLineInterface . __init__ ( self , args , kwargs )
self . clazz = CheckSource
def get_optparser ( self ) :
parser = ReviewBot . CommandLineInterface . get_optparser ( self )
2017-10-04 15:13:32 -05:00
parser . add_option ( ' --skip-add-reviews ' , action = ' store_true ' , default = False , help = ' skip adding review after completing checks ' )
2016-12-29 00:34:56 -06:00
return parser
def setup_checker ( self ) :
bot = ReviewBot . CommandLineInterface . setup_checker ( self )
bot . skip_add_reviews = self . options . skip_add_reviews
return bot
if __name__ == " __main__ " :
app = CommandLineInterface ( )
sys . exit ( app . main ( ) )