rpm-packaging-tools/tools/rpm-packaging-status.py
Dirk Mueller a20149e1ae update master branch name to xena
Change-Id: I95f40cf2ff531e6eb99bde59cd1d60ad9ab9e30f
2021-07-17 16:39:11 +02:00

348 lines
13 KiB
Python
Executable File

#!/usr/bin/python3
# Copyright (c) 2016 SUSE Linux GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
from collections import namedtuple
import os
from packaging import version
from packaging.requirements import Requirement
import re
import requests
import sys
import yaml
import json
# the current 'in development' release
CURRENT_MASTER = 'xena'
# the host where to query for open reviews
GERRIT_HOST = 'https://review.opendev.org'
V = namedtuple('V', ['release', 'upper_constraints', 'rpm_packaging_pkg',
'reviews', 'obs_published'])
def _process_status(args=None):
projects = {}
upper_constraints = read_upper_constraints(
os.path.join(args['requirements-git-dir'], 'upper-constraints.txt'))
# open reviews for the given release
open_reviews = _gerrit_open_reviews_per_file(args['release'])
# directory which contains all yaml files from the openstack/release
# git dir
releases_yaml_dir = os.path.join(args['releases-git-dir'], 'deliverables',
args['release'])
releases_indep_yaml_dir = os.path.join(args['releases-git-dir'],
'deliverables', '_independent')
yaml_files = [os.path.join(releases_indep_yaml_dir, f)
for f in os.listdir(releases_indep_yaml_dir)]
yaml_files += [os.path.join(releases_yaml_dir, f)
for f in os.listdir(releases_yaml_dir)]
for yaml_file in yaml_files:
project_name = re.sub(r'\.ya?ml$', '', os.path.basename(yaml_file))
# skip projects if include list is given
if len(args['include_projects']) and \
project_name not in args['include_projects']:
continue
with open(yaml_file) as f:
data = yaml.load(f.read())
if 'releases' not in data or not data['releases']:
# there might be yaml files without any releases
continue
v_release = find_highest_release_version(data['releases'])
# use tarball-base name if available
project_name_pkg = v_release['projects'][0].get('tarball-base',
project_name)
# get version from upper-constraints.txt
if project_name in upper_constraints:
v_upper_constraints = upper_constraints[project_name]
else:
v_upper_constraints = '-'
# path to the corresponding .spec.j2 file
rpm_packaging_pkg_project_spec = os.path.join(
args['rpm-packaging-git-dir'],
'openstack', project_name_pkg,
'%s.spec.j2' % project_name_pkg)
v_rpm_packaging_pkg = find_rpm_packaging_pkg_version(
rpm_packaging_pkg_project_spec)
# version from build service published file
v_obs_published = find_openbuildservice_pkg_version(
args['obs_published_xml'], project_name)
# reviews for the given project
if project_name in open_reviews:
project_reviews = open_reviews[project_name]
else:
project_reviews = []
# add both versions to the project dict
projects[project_name] = V(version.parse(v_release['version']),
v_upper_constraints,
v_rpm_packaging_pkg,
project_reviews,
v_obs_published)
include_obs = args['obs_published_xml']
if args['format'] == 'text':
output_text(args['release'], projects, include_obs)
elif args['format'] == 'html':
output_html(args['release'], projects, include_obs)
def process_args():
parser = argparse.ArgumentParser(
description='Compare rpm-packaging with OpenStack releases')
subparsers = parser.add_subparsers(help='sub-command help')
# subparsers - status
parser_status = subparsers.add_parser('status', help='status help')
parser_status.add_argument('releases-git-dir',
help='Base directory of the openstack/releases '
'git repo', default='releases')
parser_status.add_argument('rpm-packaging-git-dir',
help='Base directory of the '
'openstack/rpm-packaging git repo',
default='rpm-packaging')
parser_status.add_argument('requirements-git-dir',
help='Base directory of the '
'openstack/requirements git repo',
default='requirements')
parser_status.add_argument('--obs-published-xml',
help='path to a published xml file from the '
'openbuildservice')
parser_status.add_argument('release',
help='name of the release. I.e. "mitaka"',
default='mitaka')
parser_status.add_argument('--include-projects', nargs='*',
metavar='project-name', default=[],
help='If non-empty, only the given '
'projects will be checked. '
'default: %(default)s')
parser_status.add_argument('--format',
help='output format', choices=('text', 'html'),
default='text')
parser_status.set_defaults(func=_process_status)
args = parser.parse_args()
args.func(vars(args))
def find_highest_release_version(releases):
"""get a list of dicts with a version key and find the highest version
using PEP440 to compare the different versions"""
return max(releases, key=lambda x: version.parse(str(x['version'])))
def _rpm_split_filename(filename):
"""Taken from yum's rpmUtils.miscutils.py file
Pass in a standard style rpm fullname
Return a name, version, release, epoch, arch, e.g.::
foo-1.0-1.i386.rpm returns foo, 1.0, 1, i386
1:bar-9-123a.ia64.rpm returns bar, 9, 123a, 1, ia64
"""
if filename[-4:] == '.rpm':
filename = filename[:-4]
archIndex = filename.rfind('.')
arch = filename[archIndex+1:]
relIndex = filename[:archIndex].rfind('-')
rel = filename[relIndex+1:archIndex]
verIndex = filename[:relIndex].rfind('-')
ver = filename[verIndex+1:relIndex]
epochIndex = filename.find(':')
if epochIndex == -1:
epoch = ''
else:
epoch = filename[:epochIndex]
name = filename[epochIndex + 1:verIndex]
return name, ver, rel, epoch, arch
def find_openbuildservice_pkg_version(published_xml, pkg_name):
"""find the version in the openbuildservice published xml for the given
pkg name"""
import pymod2pkg
import xml.etree.ElementTree as ET
if published_xml and os.path.exists(published_xml):
with open(published_xml) as f:
tree = ET.fromstring(f.read())
distro_pkg_name = pymod2pkg.module2package(pkg_name, 'suse')
for child in tree:
if not child.attrib['name'].startswith('_') and \
child.attrib['name'].endswith('.rpm') and not \
child.attrib['name'].endswith('.src.rpm'):
(name, ver, release, epoch, arch) = _rpm_split_filename(
child.attrib['name'])
if name == distro_pkg_name:
return version.parse(ver)
return version.parse('0')
def find_rpm_packaging_pkg_version(pkg_project_spec):
"""get a spec.j2 template and get the version"""
if os.path.exists(pkg_project_spec):
with open(pkg_project_spec) as f:
for line in f:
# if the template variable 'upstream_version' is set, use that
m = re.search(
r"{%\s*set upstream_version\s*=\s*(?:upstream_version\()?"
r"'(?P<version>.*)'(?:\))?\s*%}$", line)
if m:
return version.parse(m.group('version'))
# check the Version field
m = re.search(r'^Version:\s*(?P<version>.*)\s*$', line)
if m:
if m.group('version') == '{{ py2rpmversion() }}':
return 'version unset'
return version.parse(m.group('version'))
# no version in spec found
print('ERROR: no version in %s found' % pkg_project_spec)
return version.parse('0')
return version.parse('0')
def _pretty_table(release, projects, include_obs):
from prettytable import PrettyTable
tb = PrettyTable()
fn = ['name',
'release (%s)' % release,
'u-c (%s)' % release,
'rpm packaging (%s)' % release,
'reviews']
if include_obs:
fn += ['obs']
fn += ['comment']
tb.field_names = fn
for p_name, x in projects.items():
if x.rpm_packaging_pkg == 'version unset':
comment = 'ok'
elif x.rpm_packaging_pkg == version.parse('0'):
comment = 'unpackaged'
elif x.rpm_packaging_pkg < x.release:
comment = 'needs upgrade'
elif x.rpm_packaging_pkg == x.release:
if x.upper_constraints != '-' and \
x.release > version.parse(x.upper_constraints):
comment = 'needs downgrade (u-c)'
comment = 'ok'
elif x.rpm_packaging_pkg > x.release:
comment = 'needs downgrade'
else:
comment = ''
row = [p_name, x.release, x.upper_constraints, x.rpm_packaging_pkg,
x.reviews]
if include_obs:
row += [x.obs_published]
row += [comment]
tb.add_row(row)
return tb
def output_text(release, projects, include_obs):
tb = _pretty_table(release, projects, include_obs)
print(tb.get_string(sortby='comment'))
def output_html(release, projects, include_obs):
"""adjust the comment color a big with an ugly hack"""
from lxml import html
tb = _pretty_table(release, projects, include_obs)
s = tb.get_html_string(sortby='comment')
tree = html.document_fromstring(s)
tab = tree.cssselect('table')
tab[0].attrib['style'] = 'border-collapse: collapse;'
trs = tree.cssselect('tr')
for t in trs:
t.attrib['style'] = 'border-bottom:1pt solid black;'
tds = tree.cssselect('td')
for t in tds:
if t.text_content() == 'unpackaged':
t.attrib['style'] = 'background-color:yellow'
elif t.text_content() == 'needs upgrade':
t.attrib['style'] = 'background-color:LightYellow'
elif t.text_content() == ('needs downgrade' or 'needs downgrade (uc)'):
t.attrib['style'] = 'background-color:red'
elif t.text_content() == 'ok':
t.attrib['style'] = 'background-color:green'
print(html.tostring(tree))
def read_upper_constraints(filename):
uc = dict()
with open(filename) as f:
for line in f.readlines():
# ignore markers for now
line = line.split(';')[0]
r = Requirement(line)
for s in r.specifier:
uc[r.name] = s.version
# there is only a single version in upper constraints
break
return uc
def _gerrit_open_reviews_per_file(release):
"""Returns a dict with filename as key and a list of review numbers
where this file is modified as value"""
# NOTE: gerrit has a strange first line in the returned data
gerrit_strip = ')]}\'\n'
data = dict()
if release == CURRENT_MASTER:
branch = 'master'
else:
branch = 'stable/%s' % release
url_reviews = GERRIT_HOST + '/changes/?q=status:open+project:openstack/' \
'rpm-packaging+branch:%s' % branch
res_reviews = requests.get(url_reviews)
if res_reviews.status_code == 200:
data_reviews = json.loads(res_reviews.text.lstrip(gerrit_strip))
for review in data_reviews:
url_files = GERRIT_HOST + '/changes/%s/revisions/current/files/' \
% review['change_id']
res_files = requests.get(url_files)
if res_files.status_code == 200:
data_files = json.loads(res_files.text.lstrip(gerrit_strip))
for f in data_files.keys():
# extract project name
if f.startswith('openstack/') and f.endswith('spec.j2'):
f = f.split('/')[1]
data.setdefault(f, []).append(review['_number'])
return data
def main():
process_args()
return 0
if __name__ == '__main__':
sys.exit(main())