#!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright: (c) 2020, Daniel Anglin # The MIT License (see LICENSE or https://mit-license.org) ANSIBLE_METADATA = { 'metadata_version': '1.1', 'supported_by': 'community', 'status': ['preview'] } DOCUMENTATION = ''' --- module: compare_semantic_versions short_description: Compares two semantic version values. description: - This module compares two semantic versions and outputs whether the change is an upgrade, a downgrade or no change. - The values must use valid semantic versioning in order for a successful comparison. version_added: "2.9.5" author: Daniel Anglin (@dananglin) options: new_version: description: - The semantic version of the new software. required: true type: string old_version: description: - The semantic version of the old software. required: true type: string ''' EXAMPLES = ''' - name: Simple comparison between two semantic versions. compare_semantic_versions: old_version: "1.7.10" new_version: "1.8.0" register: output1 - debug: msg: "{{ output1.result }}" - name: Semantic versions with the 'v' prefix are supported. compare_semantic_versions: old_version: "1.6.7" new_version: "v3.5.9" register: output2 - debug: msg: "{{ output2.result }}" - name: Semantic versions without minor and/or patch versions are supported. compare_semantic_versions: old_version: "v1.100.3" new_version: "v2" register: output3 - debug: msg: "{{ output3.result }}" ''' RETURN = ''' result: description: - The result of the comparison between the two semantic versions. - The value "upgrade" is returned if the new version is higher than the old version. - The value "downgrade" is returned if the new version is lower than the old version. - The value "noVersionChange" is returned if the new version equals the old version. returned: On success type: string sample: upgrade ''' from ansible.module_utils.basic import AnsibleModule UPGRADE = "upgrade" DOWNGRADE = "downgrade" NO_VERSION_CHANGE = "noVersionChange" MAJOR = "major" MINOR = "minor" PATCH = "patch" MAX_VERSION_COMPONENTS = 3 def process(semVer): if semVer.startswith('v'): semVer = semVer[1:] semVerArr = semVer.split(".") if len(semVerArr) > MAX_VERSION_COMPONENTS: raise ValueError("invalid number of version components in semantic version '{}'.".format(semVer)) if len(semVerArr) < MAX_VERSION_COMPONENTS: diff = MAX_VERSION_COMPONENTS - len(semVerArr) for i in range(diff): semVerArr.append('0') try: semVerMap = { MAJOR: int(semVerArr[0]), MINOR: int(semVerArr[1]), PATCH: int(semVerArr[2]) } except ValueError as e: raise ValueError("unable to parse an int for semantic version '{}' due to invalid format. ({})".format(semVer, str(e))) return semVerMap def compare_versions(old, new): try: oldSemVerMap = process(old) newSemVerMap = process(new) except: raise versions = [MAJOR, MINOR, PATCH] for v in range(len(versions)): if newSemVerMap[versions[v]] > oldSemVerMap[versions[v]]: return UPGRADE elif newSemVerMap[versions[v]] < oldSemVerMap[versions[v]]: return DOWNGRADE return NO_VERSION_CHANGE def run_module(): module_args = dict( new_version = dict(type='str', required=True), old_version = dict(type='str', required=True) ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) output = dict( changed=False, result='' ) try: result = compare_versions(module.params['old_version'], module.params['new_version']) except ValueError as e: module.fail_json(msg="Unable to compare semantic versions: {}".format(str(e)), **output) output['result'] = result module.exit_json(**output) def main(): run_module() if __name__ == '__main__': main()