Simplifying the check of the latest package version validation

This patch makes this validation simpler by checking if a package update
is available.

This patch also renames the role's name:
- Old: check-latest-minor-version
- New: check-latest-packages-version

The molecule tests are also included in this commit.

Change-Id: Ia4976b1cb79911d2fdb07eb6536c9a898c35be23
Signed-off-by: Gael Chamoulaud <gchamoul@redhat.com>
This commit is contained in:
Gael Chamoulaud 2019-07-25 16:44:58 +02:00
parent 0204d7665c
commit 4faf07f897
12 changed files with 243 additions and 99 deletions

View File

@ -55,18 +55,17 @@ SUPPORTED_PKG_MGRS = (
PackageDetails = collections.namedtuple('PackageDetails',
['name', 'arch', 'version'])
['name', 'version', 'release', 'arch'])
def get_package_details(line):
# Parses an output line from a package manager's
# `list (available|installed)` command and returns
# a named tuple
parts = line.rstrip().split()
name, arch = parts[0].split('.')
# Version string, excluding release string and epoch
version = parts[1].split('-')[0].split(':')[-1]
return PackageDetails(name, arch, version)
def get_package_details(output):
if output:
return PackageDetails(
output.split('|')[0],
output.split('|')[1],
output.split('|')[2],
output.split('|')[3],
)
def _command(command):
@ -79,37 +78,6 @@ def _command(command):
return process.communicate()
def _get_installed_version_from_output(output, package):
for line in output.split('\n'):
if package in line:
return get_package_details(line)
def _get_latest_available_versions(output, installed):
# Returns the latest available minor and major versions,
# one for each.
latest_minor = None
latest_major = None
# Get all packages with the same architecture
packages = list([get_package_details(line) for line in output.split('\n')
if '{i.name}.{i.arch}'.format(i=installed) in line])
# Get all packages with the *same* major version
minor = sorted((p for p in packages
if p.version[0] == installed.version[0]))
if len(minor) > 0:
latest_minor = minor[-1].version
# Get all packages with a *higher* available major version
major = sorted((p for p in packages
if p.version[0] > installed.version[0]))
if len(major) > 0:
latest_major = major[-1].version
# If the output doesn't contain packages with the same major version
# let's assume the currently installed version as latest minor one.
if latest_minor is None:
latest_minor = installed.version
return latest_minor, latest_major
def check_update(module, package, pkg_mgr):
if pkg_mgr not in SUPPORTED_PKG_MGRS:
module.fail_json(
@ -117,23 +85,47 @@ def check_update(module, package, pkg_mgr):
return
installed_stdout, installed_stderr = _command(
[pkg_mgr, 'list', 'installed', package])
['rpm', '-qa', '--qf',
'%{NAME}|%{VERSION}|%{RELEASE}|%{ARCH}',
package])
# Fail the module if for some reason we can't lookup the current package.
if installed_stderr != '':
module.fail_json(msg=installed_stderr)
return
installed = _get_installed_version_from_output(installed_stdout, package)
elif not installed_stdout:
module.fail_json(
msg='"{}" is not an installed package.'.format(package))
return
installed = get_package_details(installed_stdout)
pkg_mgr_option = 'available'
if pkg_mgr == 'dnf':
pkg_mgr_option = '--available'
available_stdout, available_stderr = _command(
[pkg_mgr, 'list', 'available', installed.name])
latest_minor_version, latest_major_version = \
_get_latest_available_versions(available_stdout, installed)
[pkg_mgr, '-q', 'list', pkg_mgr_option, installed.name])
module.exit_json(changed=False,
name=installed.name,
current_version=installed.version,
latest_minor_version=latest_minor_version,
latest_major_version=latest_major_version)
if available_stdout:
new_pkg_info = available_stdout.split('\n')[1].rstrip().split()[:2]
new_ver, new_rel = new_pkg_info[1].split('-')
module.exit_json(
changed=False,
name=installed.name,
current_version=installed.version,
current_release=installed.release,
new_version=new_ver,
new_release=new_rel)
else:
module.exit_json(
changed=False,
name=installed.name,
current_version=installed.version,
current_release=installed.release,
new_version=None,
new_release=None)
def main():

View File

@ -3,11 +3,11 @@
gather_facts: yes
vars:
metadata:
name: Check if latest minor version is installed
name: Check if latest version of packages is installed
description: >
Makes sure python-tripleoclient is at its latest minor version
Makes sure python-tripleoclient is at its latest version
before starting an upgrade.
groups:
- pre-upgrade
roles:
- check-latest-minor-version
- check-latest-packages-version

View File

@ -1,16 +0,0 @@
---
- name: Get available updates for packages
check_package_update:
package: "{{ item }}"
pkg_mgr: "{{ ansible_pkg_mgr }}"
with_items: "{{ packages }}"
register: updates
- name: Check if current version is latest minor
with_items: "{{ updates.results }}"
assert:
that: "item.latest_minor_version == item.current_version"
msg: >-
"A newer version of the {{ item.name }} package is
available: {{ item.latest_minor_version }} (currently
{{ item.current_version }})."

View File

@ -1,8 +0,0 @@
---
metadata:
name: Check if latest minor version is installed
description: >
Makes sure python-tripleoclient is at its latest minor version
before starting an upgrade.
groups:
- pre-upgrade

View File

@ -0,0 +1,37 @@
# Molecule managed
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash {{ item.pkg_extras | default('') }} && dnf clean all; \
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl python-setuptools bash {{ item.pkg_extras | default('') }} && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml {{ item.pkg_extras | default('') }} && zypper clean -a; \
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates {{ item.pkg_extras | default('') }}; \
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates {{ item.pkg_extras | default('') }} && xbps-remove -O; fi
{% for pkg in item.easy_install | default([]) %}
# install pip for centos where there is no python-pip rpm in default repos
RUN easy_install {{ pkg }}
{% endfor %}
CMD ["sh", "-c", "while true; do sleep 10000; done"]

View File

@ -0,0 +1,47 @@
---
driver:
name: docker
log: true
platforms:
- name: centos7
hostname: centos7
image: centos:7
pkg_extras: python-setuptools
easy_install:
- pip
environment: &env
http_proxy: "{{ lookup('env', 'http_proxy') }}"
https_proxy: "{{ lookup('env', 'https_proxy') }}"
- name: fedora28
hostname: fedora28
image: fedora:28
pkg_extras: python*-setuptools
environment:
<<: *env
provisioner:
name: ansible
log: true
env:
ANSIBLE_STDOUT_CALLBACK: yaml
ANSIBLE_LIBRARY: "../../../../library"
scenario:
test_sequence:
- destroy
- create
- prepare
- converge
- verify
- destroy
lint:
enabled: false
verifier:
name: testinfra
lint:
name: flake8

View File

@ -0,0 +1,51 @@
---
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
- name: Converge
hosts: all
tasks:
- name: Validate No Available Update for patch rpm
include_role:
name: check-latest-packages-version
vars:
packages:
- patch
- name: Working Detection of Update for Pam package
block:
- include_role:
name: check-latest-packages-version
vars:
packages:
- pam
rescue:
- name: Clear host errors
meta: clear_host_errors
- debug:
msg: The validation works! End the playbook run
- name: End play
meta: end_play
- name: Fail the test
fail:
msg: |
The check-latest-packages-version role should have detected
that packages have available updates.

View File

@ -0,0 +1,25 @@
---
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
- name: Prepare
hosts: all
gather_facts: no
tasks:
- name: install patch rpm
package:
name: patch

View File

@ -0,0 +1,16 @@
---
- name: Get available updates for packages
check_package_update:
package: "{{ item }}"
pkg_mgr: "{{ ansible_pkg_mgr }}"
with_items: "{{ packages }}"
register: updates
- name: Check if current version is the latest one
fail:
msg: >-
A newer version of the {{ item.name }} package is
available: {{ item.new_version }}-{{ item.new_release }}
(currently {{ item.current_version }}-{{ item.current_release }})
with_items: "{{ updates.results }}"
when: item.new_version

View File

@ -0,0 +1,8 @@
---
metadata:
name: Check if latest version of packages is installed
description: >
Makes sure python-tripleoclient is at its latest version
before starting an upgrade.
groups:
- pre-upgrade

View File

@ -20,32 +20,18 @@ from library.check_package_update import get_package_details
from tripleo_validations.tests import base
PKG_INSTALLED = """\
Last metadata expiration check: 1 day, 3:05:37 ago on Mon Jun 5 11:55:16 2017.
Installed Packages
foo-package.x86_64 2:6.1.5-1 @spideroak-one-stable
"""
PKG_INSTALLED = "foo-package|6.1.5|1|x86_64"
# This stretches the boundaries of a realistic yum list output a bit
# but it's more explicit for testing.
PKG_AVAILABLE = """\
Last metadata expiration check: 1 day, 3:06:30 ago on Mon Jun 5 11:55:16 2017.
Available Packages
foo-package.i386 2:9.1.0-1 foo-stable
foo-package.i386 2:6.2.3-1 foo-stable
foo-package.x86_64 2:8.0.0-1 foo-stable
foo-package.x86_64 2:7.0.0-1 foo-stable
foo-package.x86_64 2:6.2.0-1 foo-stable
foo-package.x86_64 2:6.1.6-1 foo-stable
foo-package.x86_64 8.0.0-1 foo-stable
"""
class TestGetPackageDetails(base.TestCase):
def setUp(self):
super(TestGetPackageDetails, self).setUp()
self.entry = get_package_details("""\
foo-package.x86_64 2:6.2.0-1 spideroak-one-stable
""")
self.entry = get_package_details("foo-package|6.2.0|1|x86_64")
def test_name(self):
self.assertEqual(self.entry.name, 'foo-package')
@ -56,6 +42,9 @@ foo-package.x86_64 2:6.2.0-1 spideroak-one-stable
def test_version(self):
self.assertEqual(self.entry.version, '6.2.0')
def test_release(self):
self.assertEqual(self.entry.release, '1')
class TestCheckUpdate(base.TestCase):
def setUp(self):
@ -82,12 +71,14 @@ class TestCheckUpdate(base.TestCase):
[PKG_INSTALLED, ''],
[PKG_AVAILABLE, ''],
]
check_update(self.module, 'foo-package', 'yum')
self.module.exit_json.assert_called_with(changed=False,
name='foo-package',
current_version='6.1.5',
latest_minor_version='6.2.0',
latest_major_version='8.0.0')
current_release='1',
new_version='8.0.0',
new_release='1')
@patch('library.check_package_update._command')
def test_returns_current_version_if_no_updates(self, mock_command):
@ -99,5 +90,6 @@ class TestCheckUpdate(base.TestCase):
self.module.exit_json.assert_called_with(changed=False,
name='foo-package',
current_version='6.1.5',
latest_minor_version='6.1.5',
latest_major_version=None)
current_release='1',
new_version=None,
new_release=None)