From ae7e20c7aea01d6d63403f867bf42e7c37be0c68 Mon Sep 17 00:00:00 2001 From: Sofer Athlan-Guyot Date: Fri, 24 Jul 2020 20:45:25 +0200 Subject: [PATCH] Add tripleo_ovs_upgrade module. This module replace the code in [1] as we need to replicate this sequence of actions in several places in the templates. It first checks if it's a layered product ovs update which uses rhosp-openvswitch. If so, it makes sure that openvswitch is removed without triggering any actions, like stopping the service. Then we install the rhos-openvswitch package which will take care of updating everything without disturbance. If the os is not using layer product, then we download the openvswitch and update it without triggering the systemctl restart if present in the postun scripts. [1] https://opendev.org/openstack/tripleo-heat-templates/src/branch/master/deployment/tripleo-packages/tripleo-packages-baremetal-puppet.yaml#L234-L370 Change-Id: I793bdb8db1d34011cf64cf9942a811ab5efc00ed (cherry picked from commit 6344bbb43dcf1f462a16572fb3f3b69ce0108862) --- .../modules/tripleo_ovs_upgrade.py | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 tripleo_ansible/ansible_plugins/modules/tripleo_ovs_upgrade.py diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_ovs_upgrade.py b/tripleo_ansible/ansible_plugins/modules/tripleo_ovs_upgrade.py new file mode 100644 index 000000000..318de50c1 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_ovs_upgrade.py @@ -0,0 +1,250 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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. +__metaclass__ = type + +import glob +import re + +from ansible.module_utils.basic import AnsibleModule + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: tripleo_ovs_update +author: + - Sofer Athlan-Guyot +version_added: '2.8' +short_description: Handle special ovs update. +notes: [] +description: + - This module check if ovs need a special treatment during update of the + package. +options: + debug: + description: + - Whether or not debug is enabled. + default: False + required: False + type: bool +""" + +EXAMPLES = """ +- name: Special treatment for ovs upgrade. + tripleo_ovs_upgrade: +""" + +RETURN = """ +msg: + description: Descrption of the action taken. + returned: always + type: str +changed: + description: Was the ovs package update or not. + returned: always + type: bool +""" + + +def pkg_manager(module, downloader=False): + dnf = module.get_bin_path('dnf') + if dnf: + module.debug("Using dnf as package manager") + if not downloader: + return dnf + else: + return ['dnf', 'download'] + if not downloader: + return module.get_bin_path('yum') + else: + return ['yumdownloader'] + + +# Get current OpenvSwitch package name +# return stuff like openvswitch2.11 in original yaml. +def get_current_ovs_pkg_name(module): + cmd = ['rpm', '-qa'] + _, output, _ = module.run_command(cmd, check_rc=True) + ovs_re = re.compile(r""" + ^(openvswitch[0-9]+\.[0-9]+-[0-9]+\.[0-9]+\.[0-9]+ # layered + | # or + openvswitch-2) # non-layered + """, re.X) + for pkg in output.split("\n"): + ovs = re.search(ovs_re, pkg) + if ovs: + return ovs.group(0) + return None + + +# Process rhosp-openvswitch layered package for new version number +# return stuff like ["2.11"] in original module +def get_version(module, pkg, new=True): + if new: + cmd = [pkg_manager(module), 'info', '-q', pkg] + else: + cmd = ['rpm', '-qi', pkg] + # This may fail if the package is not around for non-lp product. + _, output, _ = module.run_command(cmd, check_rc=False) + versions = re.findall(r'Version[^:]*:[^0-9]*([0-9.]+)', output) + found = [] + for version in versions: + if version: + # we are only interested in major/minor number here. + if new: + # We can have several version here + found.append(version.split('.')[:2]) + else: + found = version.split('.')[:2] + return found + + +def flatten_version(versions, join_str=''): + flatten_str = "" + if not isinstance(versions, list): + versions = [versions] + if isinstance(versions[0], list): + for version in versions: + flatten_str += join_str.join(version) + else: + flatten_str += join_str.join(versions) + return flatten_str + + +def get_current_ovs_pkg_names(module, pkg): + cmd = ['rpm', '-qa', pkg] + _, output, _ = module.run_command(cmd, check_rc=True) + # Make sure we remove empty element. + return [pkg for pkg in output.split("\n") if pkg] + + +def remove_package_noaction(module, pkgs, excludes=[]): + cmd = ['rpm', '-e', '--noscripts', '--nopreun', + '--nopostun', '--notriggers', '--nodeps'] + pkgs_to_remove = [] + for pkg in pkgs: + for exclude in excludes: + if not re.match(r'{}'.format(exclude), pkg): + pkgs_to_remove.append(pkg) + _, output, _ = module.run_command(cmd + pkgs_to_remove, check_rc=True) + return output + + +def upgrade_pkg(module, pkg): + cmd = [pkg_manager(module), 'upgrade', '-y', pkg] + _, output, _ = module.run_command(cmd, check_rc=True) + return output + + +def layer_product_upgrade(module, result, ovs_pkg, lp_ovs_current_version): + lp_ovs_coming_versions = get_version(module, 'rhosp-openvswitch') + ovs_current_version = get_version(module, ovs_pkg, new=False) + + pkg_suffix = '' + if int(ovs_current_version[0]) >= 3 or int(ovs_current_version[1]) >= 10: + pkg_suffix = '.'.join(ovs_current_version) + + pkg_base_name = 'openvswitch{}*'.format(pkg_suffix) + + if flatten_version(lp_ovs_coming_versions) \ + != flatten_version(ovs_current_version): + ovs_pkgs = get_current_ovs_pkg_names(module, pkg_base_name) + remove_package_noaction(module, ovs_pkgs, + excludes=['selinux']) + upgraded = upgrade_pkg(module, 'rhosp-openvswitch') + result['msg'] += \ + """ Layer product update workaround applied for {} \ +Upgraded:'{}'""".format(ovs_pkgs, upgraded) + result['changed'] = True + else: + result['msg'] += " No need to upgrade ovs." + + +def pkg_has_restart(module): + cmd = """rpm -q --scripts openvswitch | \\ + awk '/postuninstall/,/*/' | \\ + grep -q 'systemctl.*try-restart'""" + rc, _, _ = module.run_command(cmd, check_rc=False, + use_unsafe_shell=True) + + return rc == 0 + + +def upgrade_non_layered_ovs(module, result): + tmp_dir = '/root/OVS_UPGRADE' + cmds = [ + ['rm', '-rf', tmp_dir], + ['install', '-d', '-o', 'root', '-g', 'root', '-m', '0750', tmp_dir], + [pkg_manager(module), 'makecache'], + pkg_manager(module, downloader=True) + + ['--destdir', tmp_dir, '--resolve', 'openvswitch']] + for cmd in cmds: + module.run_command(cmd, check_rc=True) + + for pkg in glob.glob(tmp_dir + '/*.rpm'): + cmd = ['rpm', '-U', + '--replacepkgs', + '--notriggerun', + '--nopostun', + pkg] + module.run_command(cmd, check_rc=True) + result['msg'] += " {} handled".format(pkg) + result['changed'] = True + + +def non_layered_ovs_upgrade(module, result): + if not pkg_has_restart(module): + result['msg'] += 'Nothing to be done for non layered ovs upgrade, ' \ + "post-script doesn't have restart." + else: + result['msg'] += "OVS upgrade special handling." + upgrade_non_layered_ovs(module, result) + + +def main(): + module = AnsibleModule(argument_spec={}, supports_check_mode=False) + + result = dict( + changed=False, + msg='' + ) + + ovs_current_pkg = get_current_ovs_pkg_name(module) + if ovs_current_pkg: + # We found a ovs package, let's dive in. + ovs_current_version = get_version(module, + 'rhosp-openvswitch', + new=False) + if ovs_current_version: + result['msg'] += "Found a layered product ovs. " + layer_product_upgrade(module, result, + ovs_current_pkg, ovs_current_version) + else: + result['msg'] += "Found ovs. " + non_layered_ovs_upgrade(module, result) + else: + result['msg'] += "No ovs installed, nothing to do." + + module.exit_json(**result) + + +if __name__ == '__main__': + main()