From c9aad8202705829c77b31e7d1fe510e87558b58d Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Fri, 6 Mar 2020 12:04:48 +0000 Subject: [PATCH] fix: implement an upgrade procedure for Pleroma. This commit implements different installation paths when running the pleroma-main role, depending on whether Pleroma needs to be installed for the first time or upgraded. For first time installations the playbook will run through the normal download and installation process and also executes the database migration. If Pleroma is already installed then, by default, the playbook will not re-install Pleroma or re-run the database migration. If the user wants to update Pleroma to a newer version then they can re-run the playbook with the command-line argument '--extra-vars enable_pleroma_upgrade=True'. This commit also introduces a custom module used to compare the installed and downloaded semantic versions of Pleroma. The playbook uses this to see whether the version change is an upgrade, a downgrade or no version change. If it's an upgrade the playbook will proceed with the re-installation of Pleroma. If there is no change then the playbook will skip installation. Finally if it detects that the user is trying to downgrade Pleroma then it will fail. This commit resolves dananglin/pleroma-ansible-playbook#9 and also resolves dananglin/pleroma-ansible-playbook#5 --- .gitignore | 3 + .gitlab-ci.yml | 13 ++ Makefile | 3 + library/compare_semantic_versions.py | 162 ++++++++++++++++++++++ library/test_compare_semantic_versions.py | 33 +++++ roles/pleroma-main/tasks/main.yml | 93 ++++++++++++- 6 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 library/compare_semantic_versions.py create mode 100644 library/test_compare_semantic_versions.py diff --git a/.gitignore b/.gitignore index 35eb97f..79fdf95 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ inventories/* !inventories/.gitkeep site.yml vapid-private-key.pem + +library/__pycache__/ +library/*.pyc diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..86c1774 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,13 @@ +--- +image: python:3.7.6-slim-buster + +stages: +- test + +test: + stage: test + before_script: + - apt-get update && apt-get install make + - pip install ansible==2.9.6 + script: + - make test_modules_unit diff --git a/Makefile b/Makefile index 73a27b1..b0abff3 100644 --- a/Makefile +++ b/Makefile @@ -24,3 +24,6 @@ vapid_private_key: $(VAPID_PRIVATE_KEY_FILE) vapid_public_key: $(VAPID_PRIVATE_KEY_FILE) @echo -e "\n\nVapid public key:" @openssl ec -in $(VAPID_PRIVATE_KEY_FILE) -pubout -outform DER 2> /dev/null | tail -c 65 | base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n' + +test_modules_unit: + @find ./library -mindepth 1 -maxdepth 1 -type f -name test_*.py | xargs python3 diff --git a/library/compare_semantic_versions.py b/library/compare_semantic_versions.py new file mode 100644 index 0000000..39a4413 --- /dev/null +++ b/library/compare_semantic_versions.py @@ -0,0 +1,162 @@ +#!/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() diff --git a/library/test_compare_semantic_versions.py b/library/test_compare_semantic_versions.py new file mode 100644 index 0000000..8fda93a --- /dev/null +++ b/library/test_compare_semantic_versions.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Daniel Anglin +# The MIT License (see LICENSE or https://mit-license.org) + +import unittest as u +import compare_semantic_versions as c + +class TestCompareSemanticVersions(u.TestCase): + def test_compare_versions_results(self): + self.assertEqual(c.compare_versions("2.3.0", "3.0.0"), c.UPGRADE) + self.assertEqual(c.compare_versions("2.4.0", "2.3.0"), c.DOWNGRADE) + self.assertEqual(c.compare_versions("1.2.2", "1.2.3"), c.UPGRADE) + self.assertEqual(c.compare_versions("v2", "2.0.0"), c.NO_VERSION_CHANGE) + self.assertEqual(c.compare_versions("1.4.0", "v1.4"), c.NO_VERSION_CHANGE) + self.assertEqual(c.compare_versions("12.24.1", "v12.25.6"), c.UPGRADE) + + def test_compare_versions_exceptions(self): + with self.assertRaises(ValueError) as ctx: + c.compare_versions("1.2.3.4", "1.2.3") + self.assertEqual("invalid number of version components in semantic version '1.2.3.4'.", str(ctx.exception)) + + with self.assertRaises(ValueError) as ctx: + c.compare_versions("10.3.1", "11..3") + self.assertRegex(str(ctx.exception), r'^unable to parse an int for semantic version \'11..3\' due to invalid format\..*$') + + with self.assertRaises(ValueError) as ctx: + c.compare_versions("10.FOUR.8", "10.4.8") + self.assertRegex(str(ctx.exception), r'^unable to parse an int for semantic version \'10.FOUR.8\' due to invalid format\..*$') + +if __name__ == '__main__': + u.main() diff --git a/roles/pleroma-main/tasks/main.yml b/roles/pleroma-main/tasks/main.yml index ac13200..ce27373 100644 --- a/roles/pleroma-main/tasks/main.yml +++ b/roles/pleroma-main/tasks/main.yml @@ -37,10 +37,37 @@ - "{{ pleroma_static_dir }}/static" - "{{ pleroma_static_dir }}/static/themes" -- name: Ensuring that the release build of pleroma is downloaded. +- name: Checking if Pleroma is already installed. + stat: + path: "{{ pleroma_user.home }}/bin/pleroma" + register: pleroma_bin + +- debug: + msg: "Pleroma is currently installed." + verbosity: 0 + when: pleroma_bin.stat.isreg is defined + +- debug: + msg: "Pleroma does not appear to be installed. It will be downloaded and installed." + verbosity: 0 + when: pleroma_bin.stat.isreg is not defined + +- name: Registering the 'enable_pleroma_download' flag. + set_fact: + enable_pleroma_download: true + when: (pleroma_bin.stat.isreg is not defined) or + (enable_pleroma_upgrade | default(False)) + +- name: Registering the 'enable_pleroma_installation' flag. + set_fact: + enable_pleroma_installation: true + when: pleroma_bin.stat.isreg is not defined + +- name: Ensuring that the stable release build of pleroma is downloaded. get_url: url: "{{ pleroma_download_url }}" dest: "{{ pleroma_download_dest }}" + when: enable_pleroma_download | default(False) - name: Unzipping the release build of pleroma. unarchive: @@ -49,12 +76,71 @@ dest: /tmp owner: "{{ pleroma_user.name }}" group: "{{ pleroma_user.group }}" + when: enable_pleroma_download | default(False) + +- name: Registering the installed version of Pleroma. + shell: "{{ pleroma_user.home }}/bin/pleroma version | awk '{print $2}'" + register: pleroma_installed_version + when: enable_pleroma_upgrade | default(False) + +- debug: + msg: "Version {{ pleroma_installed_version.stdout }} is installed." + verbosity: 0 + when: enable_pleroma_upgrade | default(False) + +- name: Registering the downloaded version of Pleroma. + shell: /tmp/release/bin/pleroma version | awk '{print $2}' + register: pleroma_downloaded_version + when: enable_pleroma_upgrade | default(False) + +- debug: + msg: "Version {{ pleroma_downloaded_version.stdout }} is downloaded." + verbosity: 0 + when: enable_pleroma_upgrade | default(False) + +- name: Comparing the installed and downloaded versions of Pleroma. + compare_semantic_versions: + old_version: "{{ pleroma_installed_version.stdout }}" + new_version: "{{ pleroma_downloaded_version.stdout }}" + register: comparison + when: enable_pleroma_upgrade | default(False) + +- fail: + msg: "This playbook does not currently support downgrading Pleroma." + when: comparison.result is defined and comparison.result == "downgrade" + +- debug: + msg: "Pleroma is already installed at the target version {{ pleroma_downloaded_version.stdout }}." + verbosity: 0 + when: comparison.result is defined and comparison.result == "noVersionChange" + +- debug: + msg: "Pleroma will be upgraded to version {{ pleroma_downloaded_version.stdout }}." + verbosity: 0 + when: comparison.result is defined and comparison.result == "upgrade" + +- name: Registering the 'enable_pleroma_installation' flag for the upgrade. + set_fact: + enable_pleroma_installation: true + when: comparison.result is defined and comparison.result == "upgrade" + +- name: Ensuring that the Pleroma service is stopped. + service: + name: pleroma + state: stopped + when: comparison.result is defined and comparison.result == "upgrade" + +- name: Ensuring that the previous version of Pleroma is uninstalled. + shell: | + find {{ pleroma_user.home }} -mindepth 1 -maxdepth 1 | xargs -I dir rm -rf dir + when: comparison.result is defined and comparison.result == "upgrade" - name: Ensuring that Pleroma is installed. shell: | find /tmp/release/ -mindepth 1 -maxdepth 1 | xargs -I dir mv dir {{ pleroma_user.home }} args: creates: "{{ pleroma_user.home }}/bin/pleroma" + when: enable_pleroma_installation is defined - name: Ensuring the configuration file is set. template: @@ -71,6 +157,7 @@ - migrate environment: PATH: "{{ ansible_env.PATH }}:/opt/pleroma/bin" + when: enable_pleroma_installation is defined - name: Ensuring that folder permissions are set properly in /opt/pleroma. shell: | @@ -103,7 +190,7 @@ group: "{{ pleroma_user.group }}" mode: '0400' -- name: Setting up the Pleroma service. +- name: Ensuring that the pleroma init file is installed. copy: src: "{{ pleroma_user.home }}/installation/init.d/pleroma" dest: /etc/init.d/pleroma @@ -116,7 +203,7 @@ service: name: pleroma enabled: yes - state: restarted + state: started - name: Cleaning up file: