Merge "Add a validation that checks for available update"

This commit is contained in:
Zuul 2018-04-13 14:14:37 +00:00 committed by Gerrit Code Review
commit 008bf28bfa
4 changed files with 282 additions and 0 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
New validation to check for latest minor version of python-tripleoclient
- |
New module to check for new minor and major versions of a package

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# 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.
from mock import MagicMock
from mock import patch
from tripleo_validations.tests import base
from validations.library.check_package_update import check_update
from validations.library.check_package_update import get_package_details
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
"""
# 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
"""
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
""")
def test_name(self):
self.assertEqual(self.entry.name, 'foo-package')
def test_arch(self):
self.assertEqual(self.entry.arch, 'x86_64')
def test_version(self):
self.assertEqual(self.entry.version, '6.2.0')
class TestCheckUpdate(base.TestCase):
def setUp(self):
super(TestCheckUpdate, self).setUp()
self.module = MagicMock()
def test_unsupported_pkg_mgr_fails(self):
check_update(self.module, 'foo-package', 'apt')
self.module.fail_json.assert_called_with(
msg='Package manager "apt" is not supported.')
@patch('validations.library.check_package_update._command')
def test_fails_if_installed_package_not_found(self, mock_command):
mock_command.side_effect = [
['', 'No package found.'],
]
check_update(self.module, 'foo-package', 'yum')
self.module.fail_json.assert_called_with(
msg='No package found.')
@patch('validations.library.check_package_update._command')
def test_returns_current_and_available_versions(self, mock_command):
mock_command.side_effect = [
[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')
@patch('validations.library.check_package_update._command')
def test_returns_current_version_if_no_updates(self, mock_command):
mock_command.side_effect = [
[PKG_INSTALLED, ''],
['', 'No packages found'],
]
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.1.5',
latest_major_version=None)

View File

@ -0,0 +1,23 @@
---
- hosts: undercloud
vars:
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
packages:
- python-tripleoclient
tasks:
- 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

@ -0,0 +1,150 @@
#!/usr/bin/env python
# Copyright 2017 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.
""" Check for available updates for a given package."""
import collections
import subprocess
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = '''
---
module: check_package_update
short_description: Check for available updates for a given package
options:
package:
required: true
description:
- The name of the package you want to check
type: str
pkg_mgr:
required: true
description:
- Supported Package Manager, DNF or YUM
type: str
author: "Florian Fuchs"
'''
EXAMPLES = '''
- hosts: webservers
tasks:
- name: Get available updates for packages
check_package_update:
package: python-tripleoclient
pkg_mgr: {{ ansible_pkg_mgr}}
'''
SUPPORTED_PKG_MGRS = (
'yum',
'dnf',
)
PackageDetails = collections.namedtuple('PackageDetails',
['name', 'arch', 'version'])
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 _command(command):
# Return the result of a subprocess call
# as [stdout, stderr]
process = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,)
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(
msg='Package manager "{}" is not supported.'.format(pkg_mgr))
return
installed_stdout, installed_stderr = _command(
[pkg_mgr, 'list', 'installed', 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)
available_stdout, available_stderr = _command(
[pkg_mgr, 'list', 'available', installed.name])
latest_minor_version, latest_major_version = \
_get_latest_available_versions(available_stdout, installed)
module.exit_json(changed=False,
name=installed.name,
current_version=installed.version,
latest_minor_version=latest_minor_version,
latest_major_version=latest_major_version)
def main():
module = AnsibleModule(argument_spec=dict(
package=dict(required=True, type='str'),
pkg_mgr=dict(required=True, type='str')
))
check_update(module,
module.params.get('package'),
module.params.get('pkg_mgr'))
if __name__ == '__main__':
main()