From 61a3881f1c7cceeefd5aff913fa6766392d1f5c9 Mon Sep 17 00:00:00 2001 From: Ludovic <54670129+lbr38@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:28:07 +0100 Subject: [PATCH] 3.0.0 (python version) --- .github/workflows/build-and-test-deb.yml | 542 ++++++ .github/workflows/build-and-test-rpm.yml | 312 ++++ .github/workflows/build-deb.yml | 148 -- .github/workflows/build-rpm.yml | 98 -- .github/workflows/packaging/deb/control | 4 +- .github/workflows/packaging/deb/postinst | 49 +- .github/workflows/packaging/deb/preinst | 9 +- .github/workflows/packaging/rpm/spec | 75 +- .github/workflows/release.yml | 78 +- .gitignore | 3 +- README.md | 102 +- linupdate | 410 ----- linupdate.py | 133 ++ mods-available/agent/reposerver.agent | 278 --- mods-available/configurations/reposerver.conf | 17 - mods-available/reposerver.mod | 1490 ----------------- service.py | 21 + service/linupdate-agent | 19 - service/linupdate.systemd.template | 5 +- src/00_check-system | 126 -- src/00_exclude | 27 - src/00_generateConf | 21 - src/00_get-conf | 35 - src/00_get-modules | 16 - src/00_help | 38 - src/00_list-modules | 37 - src/00_space-left | 20 - src/01_exec-pre-modules | 26 - src/01_load-modules | 22 - src/02_check-packages-before-update | 365 ---- src/03_check-yum-lock | 14 - src/04_update | 86 - src/09_exec-post-modules | 24 - src/09_service-restart | 30 - src/10_check-reboot-needed | 20 - src/10_send-mail | 29 - src/200_apt-history-parser | 353 ---- src/200_yum-history-parser | 187 --- src/96_start-agent | 7 - src/97_stop-agent | 7 - src/98_restart-agent | 7 - src/99_clean-exit | 23 - src/99_disable-agent | 7 - src/99_enable-agent | 7 - src/__init__.py | 0 src/controllers/App/App.py | 155 ++ src/controllers/App/Config.py | 657 ++++++++ src/controllers/App/Service.py | 185 ++ src/controllers/App/Utils.py | 20 + src/controllers/App/__init__.py | 0 src/controllers/Args.py | 784 +++++++++ src/controllers/ArgsException.py | 9 + src/controllers/Exit.py | 61 + src/controllers/HttpRequest.py | 112 ++ src/controllers/Log.py | 34 + src/controllers/Mail.py | 58 + src/controllers/Module/Module.py | 231 +++ src/controllers/Module/Reposerver/Agent.py | 331 ++++ src/controllers/Module/Reposerver/Args.py | 394 +++++ src/controllers/Module/Reposerver/Config.py | 668 ++++++++ .../Module/Reposerver/Reposerver.py | 75 + src/controllers/Module/Reposerver/Status.py | 237 +++ src/controllers/Module/Reposerver/__init__.py | 0 src/controllers/Package/Apt.py | 553 ++++++ src/controllers/Package/Dnf.py | 637 +++++++ src/controllers/Package/Package.py | 308 ++++ src/controllers/Service/Service.py | 77 + src/controllers/Service/__init__.py | 0 src/controllers/System.py | 123 ++ src/controllers/Yaml.py | 27 + src/controllers/__init__.py | 0 templates/linupdate.template.yml | 8 + templates/mail/mail.template.html | 182 ++ templates/modules/reposerver.template.yml | 15 + templates/update.template.yml | 10 + version | 2 +- 76 files changed, 7165 insertions(+), 4115 deletions(-) create mode 100644 .github/workflows/build-and-test-deb.yml create mode 100644 .github/workflows/build-and-test-rpm.yml delete mode 100644 .github/workflows/build-deb.yml delete mode 100644 .github/workflows/build-rpm.yml delete mode 100755 linupdate create mode 100755 linupdate.py delete mode 100644 mods-available/agent/reposerver.agent delete mode 100644 mods-available/configurations/reposerver.conf delete mode 100644 mods-available/reposerver.mod create mode 100755 service.py delete mode 100755 service/linupdate-agent delete mode 100644 src/00_check-system delete mode 100644 src/00_exclude delete mode 100644 src/00_generateConf delete mode 100644 src/00_get-conf delete mode 100644 src/00_get-modules delete mode 100644 src/00_help delete mode 100644 src/00_list-modules delete mode 100644 src/00_space-left delete mode 100644 src/01_exec-pre-modules delete mode 100644 src/01_load-modules delete mode 100644 src/02_check-packages-before-update delete mode 100644 src/03_check-yum-lock delete mode 100644 src/04_update delete mode 100644 src/09_exec-post-modules delete mode 100644 src/09_service-restart delete mode 100644 src/10_check-reboot-needed delete mode 100644 src/10_send-mail delete mode 100644 src/200_apt-history-parser delete mode 100644 src/200_yum-history-parser delete mode 100644 src/96_start-agent delete mode 100644 src/97_stop-agent delete mode 100644 src/98_restart-agent delete mode 100644 src/99_clean-exit delete mode 100644 src/99_disable-agent delete mode 100644 src/99_enable-agent create mode 100644 src/__init__.py create mode 100644 src/controllers/App/App.py create mode 100644 src/controllers/App/Config.py create mode 100644 src/controllers/App/Service.py create mode 100644 src/controllers/App/Utils.py create mode 100644 src/controllers/App/__init__.py create mode 100644 src/controllers/Args.py create mode 100644 src/controllers/ArgsException.py create mode 100644 src/controllers/Exit.py create mode 100644 src/controllers/HttpRequest.py create mode 100644 src/controllers/Log.py create mode 100644 src/controllers/Mail.py create mode 100644 src/controllers/Module/Module.py create mode 100644 src/controllers/Module/Reposerver/Agent.py create mode 100644 src/controllers/Module/Reposerver/Args.py create mode 100644 src/controllers/Module/Reposerver/Config.py create mode 100644 src/controllers/Module/Reposerver/Reposerver.py create mode 100644 src/controllers/Module/Reposerver/Status.py create mode 100644 src/controllers/Module/Reposerver/__init__.py create mode 100644 src/controllers/Package/Apt.py create mode 100644 src/controllers/Package/Dnf.py create mode 100644 src/controllers/Package/Package.py create mode 100644 src/controllers/Service/Service.py create mode 100644 src/controllers/Service/__init__.py create mode 100644 src/controllers/System.py create mode 100644 src/controllers/Yaml.py create mode 100644 src/controllers/__init__.py create mode 100644 templates/linupdate.template.yml create mode 100644 templates/mail/mail.template.html create mode 100644 templates/modules/reposerver.template.yml create mode 100644 templates/update.template.yml diff --git a/.github/workflows/build-and-test-deb.yml b/.github/workflows/build-and-test-deb.yml new file mode 100644 index 0000000..c14063c --- /dev/null +++ b/.github/workflows/build-and-test-deb.yml @@ -0,0 +1,542 @@ +name: Build and test deb package for linupdate + +on: + push: + branches: [ python ] + pull_request: + push: + branches: [ main ] + +jobs: + build-deb: + name: Build deb package + runs-on: ubuntu-latest + container: + image: debian:latest + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + - name: Install dependencies packages + run: apt-get update && apt-get install build-essential binutils lintian debhelper dh-make devscripts -y + + - name: Create build environment + run: | + mkdir -p /tmp/linupdate-build/DEBIAN + mkdir -p /tmp/linupdate-build/etc/linupdate/modules + mkdir -p /tmp/linupdate-build/opt/linupdate + mkdir -p /tmp/linupdate-build/opt/linupdate/src/ + mkdir -p /tmp/linupdate-build/opt/linupdate/templates/ + mkdir -p /tmp/linupdate-build/lib/systemd/system/ + + - name: Copy files to include in the build + run: | + cp -r ${GITHUB_WORKSPACE}/src/* /tmp/linupdate-build/opt/linupdate/src/ + cp -r ${GITHUB_WORKSPACE}/templates/* /tmp/linupdate-build/opt/linupdate/templates/ + cp ${GITHUB_WORKSPACE}/linupdate.py /tmp/linupdate-build/opt/linupdate/linupdate.py + cp ${GITHUB_WORKSPACE}/service.py /tmp/linupdate-build/opt/linupdate/service.py + cp ${GITHUB_WORKSPACE}/version /tmp/linupdate-build/opt/linupdate/version + cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /tmp/linupdate-build/lib/systemd/system/linupdate.service + + - name: Copy control file + run: | + cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/control /tmp/linupdate-build/DEBIAN/control + sed -i "s/__VERSION__/${{ env.VERSION }}/g" /tmp/linupdate-build/DEBIAN/control + + - name: Copy preinst and postinst script + run: | + cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/preinst /tmp/linupdate-build/DEBIAN/preinst + cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/postinst /tmp/linupdate-build/DEBIAN/postinst + chmod 755 /tmp/linupdate-build/DEBIAN/preinst /tmp/linupdate-build/DEBIAN/postinst + + - name: Build package + run: | + cd /tmp + dpkg-deb --build linupdate-build + mv /tmp/linupdate-build.deb /tmp/linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + path: /tmp/linupdate-test-build_${{ env.VERSION }}_all.deb + retention-days: 1 + + # Linupdate 3 is not working on Debian 10 + # Try to install package on Debian 10 + # install-debian-10: + # name: Install on Debian 10 + # needs: + # build-deb + # runs-on: ubuntu-latest + # container: + # image: debian:10 + # options: --user root + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + + # - name: Get linupdate version + # run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # # Download builded deb package artifact + # - name: Download artifact + # uses: actions/download-artifact@v4 + # with: + # name: linupdate-test-build_${{ env.VERSION }}_all.deb + + # - name: Install package + # run: | + # apt-get update -y + # apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # - name: Launch linupdate + # run: python3 /opt/linupdate/linupdate.py --check-updates + + # Try to install package on Debian 11 + install-debian-11: + name: Install (Debian 11) + needs: + build-deb + runs-on: ubuntu-latest + container: + image: debian:11 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # Download builded deb package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Install package + run: | + apt-get update -y + apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # Tests some params + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister + + # Try to install package on Debian 12 + install-debian-12: + name: Install (Debian 12) + needs: + build-deb + runs-on: ubuntu-latest + container: + image: debian:12 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # Download builded deb package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Install package + run: | + apt-get update -y + apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # Tests some params + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister + + # Try to install package on Ubuntu 22.04 + install-ubuntu-2204: + name: Install (Ubuntu 22.04) + needs: + build-deb + runs-on: ubuntu-latest + container: + image: ubuntu:22.04 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # Download builded deb package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Install package + run: | + apt-get update -y + apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # Tests some params + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister + + # Try to install package on Ubuntu (latest) + install-ubuntu-latest: + name: Install (latest Ubuntu) + needs: + build-deb + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # Download builded deb package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Install package + run: | + sudo apt-get update -y + sudo apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # Tests some params + - name: "Run test: print help" + run: sudo python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: sudo python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: sudo python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: sudo python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: sudo python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: sudo python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: sudo python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: sudo python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: sudo python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: sudo python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: sudo python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: sudo python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: sudo python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: sudo python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: sudo python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: sudo python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: sudo python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: sudo python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: sudo python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: sudo python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister + + + # Try to migrate from old linupdate (bash) to new linupdate (python) package on Debian 12 + migrate-debian-12: + name: Migrate bash linupdate to python linupdate (Debian 12) + needs: + build-deb + runs-on: ubuntu-latest + container: + image: debian:12 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + # Install linupdate bash version + - name: Install linupdate bash version + run: | + apt-get update -y + apt-get install -y curl gpg + curl -sS https://packages.bespin.ovh/repo/gpgkeys/packages.bespin.ovh.pub | gpg --dearmor > /etc/apt/trusted.gpg.d/packages.bespin.ovh.gpg + echo "deb https://packages.bespin.ovh/repo/linupdate/bookworm/main_prod bookworm main" > /etc/apt/sources.list.d/linupdate.list + apt-get update -y + apt-get install -y linupdate + + # Set up some params + - name: Set up linupdate bash version + run: | + linupdate --profile test-migration + linupdate --env test-migration + linupdate --exclude package1,package2 + linupdate --exclude-major package3,package4 + sed -i 's/MAIL_RECIPIENT=.*/MAIL_RECIPIENT="repomanager@protonmail.com"/g' /etc/linupdate/linupdate.conf + sed -i 's/SERVICE_RESTART=.*/SERVICE_RESTART="service1,service2"/g' /etc/linupdate/linupdate.conf + linupdate --mod-enable reposerver + linupdate --mod-configure reposerver --url https://packages.bespin.ovh --fail-level 3 + linupdate --mod-configure reposerver --get-packages-conf-from-reposerver no + linupdate --mod-configure reposerver --get-repos-from-reposerver no + linupdate --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + linupdate --mod-configure reposerver --unregister + + # Download builded deb package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build_${{ env.VERSION }}_all.deb + + - name: Install package + run: | + apt-get update -y + apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb + + # Tests some params + - name: Print config files content + run: | + cat /etc/linupdate/linupdate.yml /etc/linupdate/update.yml + + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: print profile" + run: python3 /opt/linupdate/linupdate.py --profile + + - name: "Run test: print environment" + run: python3 /opt/linupdate/linupdate.py --env + + - name: "Run test: get mail recipient" + run: python3 /opt/linupdate/linupdate.py --get-mail-recipient + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: print raw configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print config files" + run: ls -l /etc/linupdate/ /etc/linupdate/modules/ \ No newline at end of file diff --git a/.github/workflows/build-and-test-rpm.yml b/.github/workflows/build-and-test-rpm.yml new file mode 100644 index 0000000..70d2e64 --- /dev/null +++ b/.github/workflows/build-and-test-rpm.yml @@ -0,0 +1,312 @@ +name: Build and test rpm package for linupdate + +on: + push: + branches: [ python ] + pull_request: + push: + branches: [ main ] + +jobs: + build-rpm: + name: Build rpm package + runs-on: ubuntu-latest + container: + image: centos:8 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + - name: Install CentOS8 archive repositories + run: | + rm /etc/yum.repos.d/* -f + + echo "[os] + name=os repo + baseurl=https://vault.centos.org/8-stream/BaseOS/x86_64/os/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/os.repo + + echo "[appstream] + name=updates repo + baseurl=https://vault.centos.org/8-stream/AppStream/x86_64/os/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/appstream.repo + + echo "[extras] + name=extras repo + baseurl=https://vault.centos.org/8-stream/extras/x86_64/extras-common/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/extras.repo + + dnf clean all + + - name: Install dependencies packages + run: dnf install rpmdevtools rpmlint -y + + - name: Create build environment + run: | + mkdir -p $HOME/rpmbuild/BUILD + mkdir -p $HOME/rpmbuild/BUILDROOT + mkdir -p $HOME/rpmbuild/RPMS + mkdir -p $HOME/rpmbuild/SOURCES + mkdir -p $HOME/rpmbuild/SPECS + mkdir -p /etc/linupdate/modules + mkdir -p /opt/linupdate + mkdir -p /opt/linupdate/src/ + mkdir -p /opt/linupdate/templates/ + mkdir -p /lib/systemd/system/ + + - name: Copy files to include in the build + run: | + cp -r ${GITHUB_WORKSPACE}/src/* /opt/linupdate/src/ + cp -r ${GITHUB_WORKSPACE}/templates/* /opt/linupdate/templates/ + cp ${GITHUB_WORKSPACE}/linupdate.py /opt/linupdate/linupdate.py + cp ${GITHUB_WORKSPACE}/service.py /opt/linupdate/service.py + cp ${GITHUB_WORKSPACE}/version /opt/linupdate/version + cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /lib/systemd/system/linupdate.service + + - name: Copy spec file + run: | + cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/rpm/spec $HOME/rpmbuild/SPECS/linupdate.spec + sed -i "s/__VERSION__/${{ env.VERSION }}/g" $HOME/rpmbuild/SPECS/linupdate.spec + + - name: Build package + run: | + cd $HOME/rpmbuild/SPECS + rpmbuild --target noarch -bb linupdate.spec + mv $HOME/rpmbuild/RPMS/noarch/linupdate-${{ env.VERSION }}-stable.noarch.rpm /tmp/linupdate-test-build-${{ env.VERSION }}.noarch.rpm + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: linupdate-test-build-${{ env.VERSION }}.noarch.rpm + path: /tmp/linupdate-test-build-${{ env.VERSION }}.noarch.rpm + retention-days: 1 + + # Try to install package on CentOS8 + install-centos8: + name: Install on CentOS8 + needs: + build-rpm + runs-on: ubuntu-latest + container: + image: centos:8 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + - name: Install CentOS8 archive repositories + run: | + cat > /etc/yum.repos.d/CentOS-Linux-AppStream.repo << EOF + [appstream] + name=CentOS Linux \$releasever - AppStream + baseurl=http://vault.centos.org/\$contentdir/\$releasever/AppStream/\$basearch/os/ + gpgcheck=1 + enabled=1 + gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial + EOF + + cat > /etc/yum.repos.d/CentOS-Linux-BaseOS.repo << EOF + [baseos] + name=CentOS Linux \$releasever - BaseOS + baseurl=http://vault.centos.org/\$contentdir/\$releasever/BaseOS/\$basearch/os/ + gpgcheck=1 + enabled=1 + gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial + EOF + + cat > /etc/yum.repos.d/CentOS-Linux-Extras.repo << EOF + [extras] + name=CentOS Linux \$releasever - Extras + baseurl=http://vault.centos.org/\$contentdir/\$releasever/extras/\$basearch/os/ + gpgcheck=1 + enabled=1 + gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial + EOF + + dnf install epel-release -y + + dnf clean all + + - name: Update system + run: dnf update -y --exclude=centos-release + + # Download builded rpm package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build-${{ env.VERSION }}.noarch.rpm + + - name: Install package + run: dnf --nogpgcheck localinstall -y ./linupdate-test-build-${{ env.VERSION }}.noarch.rpm + + - name: Launch linupdate + run: python3 /opt/linupdate/linupdate.py --check-updates + + # Tests some params + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister + + # Try to install package on Rocky Linux 9 + install-rockylinux9: + name: Install on Rocky Linux 9 + needs: + build-rpm + runs-on: ubuntu-latest + container: + image: rockylinux/rockylinux:9 + options: --user root + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get linupdate version + run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + + - name: Install EPEL repositories + run: dnf install epel-release -y + + - name: Update system + run: dnf update -y + + # Download builded rpm package artifact + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: linupdate-test-build-${{ env.VERSION }}.noarch.rpm + + - name: Install package + run: dnf --nogpgcheck localinstall -y ./linupdate-test-build-${{ env.VERSION }}.noarch.rpm + + - name: Launch linupdate + run: python3 /opt/linupdate/linupdate.py --check-updates + + # Tests some params + - name: "Run test: print help" + run: python3 /opt/linupdate/linupdate.py --help + + - name: "Run test: print configuration" + run: python3 /opt/linupdate/linupdate.py --show-config + + - name: "Run test: print version" + run: python3 /opt/linupdate/linupdate.py --version + + - name: "Run test: switch profile" + run: python3 /opt/linupdate/linupdate.py --profile container + + - name: "Run test: switch environment" + run: python3 /opt/linupdate/linupdate.py --env test + + - name: "Run test: disable mail" + run: python3 /opt/linupdate/linupdate.py --mail-enable false + + - name: "Run test: set mail recipient" + run: python3 /opt/linupdate/linupdate.py --set-mail-recipient test@mail.com,test2@mail.com + + - name: "Run test: set package exclusions" + run: python3 /opt/linupdate/linupdate.py --exclude "kernel.*" + + - name: "Run test: get package exclusions" + run: python3 /opt/linupdate/linupdate.py --get-exclude + + - name: "Run test: set package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + + - name: "Run test: get package exclusions on major update" + run: python3 /opt/linupdate/linupdate.py --get-exclude-major + + - name: "Run test: set services to restart after update" + run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + + - name: "Run test: get services to restart after update" + run: python3 /opt/linupdate/linupdate.py --get-service-restart + + - name: "Run test: check updates" + run: python3 /opt/linupdate/linupdate.py --check-updates + + - name: "Run test: list available modules" + run: python3 /opt/linupdate/linupdate.py --mod-list + + - name: "Run test: enable reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-enable reposerver + + - name: "Run test: configure reposerver module" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --url https://packages.bespin.ovh + + - name: "Run test: register to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --api-key ${{ secrets.REPOSITORY_TOKEN }} --register + + - name: "Run test: send all informations to reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --send-all-info + + - name: "Run test: unregister from reposerver" + run: python3 /opt/linupdate/linupdate.py --mod-configure reposerver --unregister diff --git a/.github/workflows/build-deb.yml b/.github/workflows/build-deb.yml deleted file mode 100644 index 4e03e33..0000000 --- a/.github/workflows/build-deb.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Build and test deb package for linupdate - -on: - push: - branches: [ devel ] - pull_request: - push: - branches: [ main ] - -jobs: - build-deb: - name: Build deb package - runs-on: ubuntu-latest - container: - image: debian:latest - options: --user root - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - - name: Install dependencies packages - run: apt-get update && apt-get install build-essential binutils lintian debhelper dh-make devscripts -y - - - name: Create build environment - run: | - mkdir -p /tmp/linupdate-build/DEBIAN - mkdir -p /tmp/linupdate-build/etc/linupdate/modules - mkdir -p /tmp/linupdate-build/opt/linupdate - mkdir -p /tmp/linupdate-build/opt/linupdate/.src/ - mkdir -p /tmp/linupdate-build/opt/linupdate/mods-available/ - mkdir -p /tmp/linupdate-build/opt/linupdate/mods-enabled/ - mkdir -p /tmp/linupdate-build/opt/linupdate/agents-available/ - mkdir -p /tmp/linupdate-build/opt/linupdate/service/ - mkdir -p /tmp/linupdate-build/lib/systemd/system/ - - - name: Copy files to include in the build - run: | - cp -r ${GITHUB_WORKSPACE}/src/* /tmp/linupdate-build/opt/linupdate/.src/ - cp -r ${GITHUB_WORKSPACE}/mods-available/* /tmp/linupdate-build/opt/linupdate/mods-available/ - cp -r ${GITHUB_WORKSPACE}/service/* /tmp/linupdate-build/opt/linupdate/service/ - cp ${GITHUB_WORKSPACE}/linupdate /tmp/linupdate-build/opt/linupdate/linupdate - cp ${GITHUB_WORKSPACE}/version /tmp/linupdate-build/opt/linupdate/version - cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /tmp/linupdate-build/lib/systemd/system/linupdate.service - - - name: Copy control file - run: | - cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/control /tmp/linupdate-build/DEBIAN/control - sed -i "s/__VERSION__/${{ env.VERSION }}/g" /tmp/linupdate-build/DEBIAN/control - - - name: Copy preinst and postinst script - run: | - cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/preinst /tmp/linupdate-build/DEBIAN/preinst - cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/deb/postinst /tmp/linupdate-build/DEBIAN/postinst - chmod 755 /tmp/linupdate-build/DEBIAN/preinst /tmp/linupdate-build/DEBIAN/postinst - - - name: Build package - run: | - cd /tmp - dpkg-deb --build linupdate-build - mv /tmp/linupdate-build.deb /tmp/linupdate-test-build_${{ env.VERSION }}_all.deb - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: linupdate-test-build_${{ env.VERSION }}_all.deb - path: /tmp/linupdate-test-build_${{ env.VERSION }}_all.deb - retention-days: 1 - - # Try to install package on Debian 10 - install-debian-10: - name: Install on Debian 10 - needs: - build-deb - runs-on: ubuntu-latest - container: - image: debian:10 - options: --user root - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - # Download builded deb package artifact - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: linupdate-test-build_${{ env.VERSION }}_all.deb - - - name: Install package - run: | - apt-get update -y - apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb - - # Try to install package on Debian 11 - install-debian-11: - name: Install on Debian 11 - needs: - build-deb - runs-on: ubuntu-latest - container: - image: debian:11 - options: --user root - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - # Download builded deb package artifact - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: linupdate-test-build_${{ env.VERSION }}_all.deb - - - name: Install package - run: | - apt-get update -y - apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb - - # Try to install package on Ubuntu (latest) - install-ubuntu: - name: Install on Ubuntu (latest) - needs: - build-deb - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - # Download builded deb package artifact - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: linupdate-test-build_${{ env.VERSION }}_all.deb - - - name: Install package - run: | - sudo apt-get update -y - sudo apt-get install -y ./linupdate-test-build_${{ env.VERSION }}_all.deb \ No newline at end of file diff --git a/.github/workflows/build-rpm.yml b/.github/workflows/build-rpm.yml deleted file mode 100644 index 8df4f33..0000000 --- a/.github/workflows/build-rpm.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Build and test rpm package for linupdate - -on: - push: - branches: [ devel ] - pull_request: - push: - branches: [ main ] - -jobs: - build-rpm: - name: Build rpm package - runs-on: ubuntu-latest - container: - image: centos:7 - options: --user root - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - - name: Install dependencies packages - run: yum install rpmdevtools rpmlint -y - - - name: Create build environment - run: | - mkdir -p $HOME/rpmbuild/BUILD - mkdir -p $HOME/rpmbuild/BUILDROOT - mkdir -p $HOME/rpmbuild/RPMS - mkdir -p $HOME/rpmbuild/SOURCES - mkdir -p $HOME/rpmbuild/SPECS - mkdir -p /etc/linupdate/modules - mkdir -p /opt/linupdate - mkdir -p /opt/linupdate/.src/ - mkdir -p /opt/linupdate/mods-available/ - mkdir -p /opt/linupdate/mods-enabled/ - mkdir -p /opt/linupdate/agents-available/ - mkdir -p /opt/linupdate/agents-enabled/ - mkdir -p /opt/linupdate/service/ - mkdir -p /lib/systemd/system/ - - - name: Copy files to include in the build - run: | - cp -r ${GITHUB_WORKSPACE}/src/* /opt/linupdate/.src/ - cp -r ${GITHUB_WORKSPACE}/mods-available/* /opt/linupdate/mods-available/ - cp -r ${GITHUB_WORKSPACE}/service/* /opt/linupdate/service/ - cp ${GITHUB_WORKSPACE}/linupdate /opt/linupdate/linupdate - cp ${GITHUB_WORKSPACE}/version /opt/linupdate/version - cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /lib/systemd/system/linupdate.service - - - name: Copy spec file - run: | - cp ${GITHUB_WORKSPACE}/.github/workflows/packaging/rpm/spec $HOME/rpmbuild/SPECS/linupdate.spec - sed -i "s/__VERSION__/${{ env.VERSION }}/g" $HOME/rpmbuild/SPECS/linupdate.spec - - - name: Build package - run: | - cd $HOME/rpmbuild/SPECS - rpmbuild --target noarch -bb --quiet linupdate.spec - mv $HOME/rpmbuild/RPMS/noarch/linupdate-${{ env.VERSION }}-stable.noarch.rpm /tmp/linupdate-test-build-${{ env.VERSION }}.noarch.rpm - - - name: Upload artifact - uses: actions/upload-artifact@v3 - with: - name: linupdate-test-build-${{ env.VERSION }}.noarch.rpm - path: /tmp/linupdate-test-build-${{ env.VERSION }}.noarch.rpm - retention-days: 1 - - # Try to install package on Fedora 37 - install-fedora: - name: Install on Fedora 37 - needs: - build-rpm - runs-on: ubuntu-latest - container: - image: fedora:37 - options: --user root - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Get linupdate version - run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV - - # Download builded deb package artifact - - name: Download artifact - uses: actions/download-artifact@v3 - with: - name: linupdate-test-build-${{ env.VERSION }}.noarch.rpm - - - name: Install package - run: | - dnf update -y - yum clean all - yum --nogpgcheck localinstall -y ./linupdate-test-build-${{ env.VERSION }}.noarch.rpm - \ No newline at end of file diff --git a/.github/workflows/packaging/deb/control b/.github/workflows/packaging/deb/control index 5606d29..8e05b07 100644 --- a/.github/workflows/packaging/deb/control +++ b/.github/workflows/packaging/deb/control @@ -3,7 +3,7 @@ Version: __VERSION__ Section: main Priority: optional Architecture: all -Depends: curl, git, apt-transport-https, aptitude, mutt, locales, ngrep, inotify-tools, jq, virt-what, net-tools, dnsutils, locales-all +Depends: apt-transport-https, locales, virt-what, net-tools, dnsutils, locales-all, python3, python3-tabulate, python3-colorama, python3-dateutil, python3-yaml, python3-dateutil, python3-simplejson, python3-distro, python3-apt, python3-requests, python3-pyinotify, python3-websocket Maintainer: Ludovic -Description: Linupdate package updater - Repomanager client side agent +Description: Linupdate 3 (python version) - Packages updater and Repomanager client side agent Homepage: https://github.com/lbr38/linupdate diff --git a/.github/workflows/packaging/deb/postinst b/.github/workflows/packaging/deb/postinst index cb17ecb..b59781f 100644 --- a/.github/workflows/packaging/deb/postinst +++ b/.github/workflows/packaging/deb/postinst @@ -1,21 +1,27 @@ #!/bin/bash -DATA_DIR="/opt/linupdate" -SERVICE="$DATA_DIR/service/linupdate-agent" +SERVICE="/opt/linupdate/service.py" -# Restore configuration file if exists -if [ -f "/tmp/linupdate.conf.debsave" ];then - rm -f /etc/linupdate/linupdate.conf - mv /tmp/linupdate.conf.debsave /etc/linupdate/linupdate.conf +# Restore configuration files if exists +if [ -f "/tmp/linupdate.yml.debsave" ];then + rm -f /etc/linupdate/linupdate.yml + mv /tmp/linupdate.yml.debsave /etc/linupdate/linupdate.yml +fi +if [ -f "/tmp/update.yml.debsave" ];then + rm -f /etc/linupdate/update.yml + mv /tmp/update.yml.debsave /etc/linupdate/update.yml fi -# Create a symlink to main script -ln -sf /opt/linupdate/linupdate /usr/bin/linupdate - -# Delete old 'functions' directory if exists -if [ -d "$DATA_DIR/functions" ];then - rm -rf "$DATA_DIR/functions" +# If no configuration files exists, copy default +if [ ! -f "/etc/linupdate/linupdate.yml" ];then + cp /opt/linupdate/templates/linupdate.template.yml /etc/linupdate/linupdate.yml fi +if [ ! -f "/etc/linupdate/update.yml" ];then + cp /opt/linupdate/templates/update.template.yml /etc/linupdate/update.yml +fi + +# Create a symlink to main script +ln -sf /opt/linupdate/linupdate.py /usr/bin/linupdate # Install en_US.UTF-8 locale if not present if ! locale -a | grep -q "en_US.UTF-8";then @@ -28,12 +34,27 @@ chmod 750 /opt/linupdate # Only if systemd is installed (not the case on github runners) if [ -f "/usr/bin/systemctl" ];then + # Copy systemd unit file if not exists + if [ ! -f "/lib/systemd/system/linupdate.service" ];then + cp /opt/linupdate/service/linupdate.systemd.template /etc/systemd/system/linupdate.service + fi + + # Enable service script by creating a symlink if not exists + if [ ! -L "/etc/systemd/system/linupdate.service" ];then + # Delete file in case it is a file and not a symlink + rm -f /etc/systemd/system/linupdate.service + ln -sf /lib/systemd/system/linupdate.service /etc/systemd/system/linupdate.service + fi - # Enable service script by creating a symlink - ln -sf /lib/systemd/system/linupdate.service /etc/systemd/system/linupdate.service chmod 550 "$SERVICE" chown root:root "$SERVICE" + # Clean old directories + rm /opt/linupdate/agents-enabled -rf + rm /opt/linupdate/mods-enabled -rf + rm /opt/linupdate/mods-available -rf + + /usr/bin/systemctl enable linupdate /usr/bin/systemctl --quiet daemon-reload # Start service diff --git a/.github/workflows/packaging/deb/preinst b/.github/workflows/packaging/deb/preinst index ad6e378..889b4d1 100644 --- a/.github/workflows/packaging/deb/preinst +++ b/.github/workflows/packaging/deb/preinst @@ -1,8 +1,11 @@ #!/bin/bash -# Save current configuration file if exists -if [ -f "/etc/linupdate/linupdate.conf" ];then - cp /etc/linupdate/linupdate.conf /tmp/linupdate.conf.debsave +# Save current configuration files if exists +if [ -f "/etc/linupdate/linupdate.yml" ];then + cp /etc/linupdate/linupdate.yml /tmp/linupdate.yml.debsave +fi +if [ -f "/etc/linupdate/update.yml" ];then + cp /etc/linupdate/update.yml /tmp/update.yml.debsave fi # Only if systemd is installed (not the case on github runners) diff --git a/.github/workflows/packaging/rpm/spec b/.github/workflows/packaging/rpm/spec index 1098ea7..495242a 100644 --- a/.github/workflows/packaging/rpm/spec +++ b/.github/workflows/packaging/rpm/spec @@ -1,30 +1,37 @@ Name: linupdate Version: __VERSION__ Release: stable -Summary: Linupdate package updater - Repomanager client side agent +Summary: Linupdate 3 (python version) - Packages updater and Repomanager client side agent BuildArch: noarch License: GPL-3.0 URL: https://github.com/lbr38/linupdate -Requires: curl -Requires: git -Requires: mutt -Requires: ngrep -Requires: inotify-tools -Requires: jq Requires: virt-what Requires: net-tools Requires: bind-utils -Requires: yum-utils +Requires: python3 +Requires: python3-tabulate +Requires: python3-colorama +Requires: python3-dateutil +Requires: python3-yaml +Requires: python3-dateutil +Requires: python3-simplejson +Requires: python3-distro +Requires: python3-requests +Requires: python3-inotify +Requires: python3-websocket-client %description -Linupdate package updater - Repomanager client side agent +Linupdate 3 (python version) - Packages updater and Repomanager client side agent %prep -# Save current configuration file if exists -if [ -f "/etc/linupdate/linupdate.conf" ];then - cp /etc/linupdate/linupdate.conf /tmp/linupdate.conf.rpmsave +# Save current configuration files if exists +if [ -f "/etc/linupdate/linupdate.yml" ];then + cp /etc/linupdate/linupdate.yml /tmp/linupdate.yml.rpmsave +fi +if [ -f "/etc/linupdate/update.yml" ];then + cp /etc/linupdate/update.yml /tmp/update.yml.rpmsave fi # Only if systemd is installed (not the case on github runners) @@ -45,22 +52,28 @@ cp -r /etc/linupdate $RPM_BUILD_ROOT/etc/ cp /lib/systemd/system/linupdate.service $RPM_BUILD_ROOT/lib/systemd/system/linupdate.service %post -DATA_DIR="/opt/linupdate" -SERVICE="$DATA_DIR/service/linupdate-agent" +SERVICE="/opt/linupdate/service.py" -# Restore configuration file if exists -if [ -f "/tmp/linupdate.conf.rpmsave" ];then - rm -f /etc/linupdate/linupdate.conf - mv /tmp/linupdate.conf.rpmsave /etc/linupdate/linupdate.conf +# Restore configuration files if exists +if [ -f "/tmp/linupdate.yml.rpmsave" ];then + rm -f /etc/linupdate/linupdate.yml + mv /tmp/linupdate.yml.rpmsave /etc/linupdate/linupdate.yml +fi +if [ -f "/tmp/update.yml.rpmsave" ];then + rm -f /etc/linupdate/update.yml + mv /tmp/update.yml.rpmsave /etc/linupdate/update.yml fi -# Delete old 'functions' directory if exists -if [ -d "$DATA_DIR/functions" ];then - rm -rf "$DATA_DIR/functions" +# If no configuration files exists, copy default +if [ ! -f "/etc/linupdate/linupdate.yml" ];then + cp /opt/linupdate/templates/linupdate.template.yml /etc/linupdate/linupdate.yml +fi +if [ ! -f "/etc/linupdate/update.yml" ];then + cp /opt/linupdate/templates/update.template.yml /etc/linupdate/update.yml fi # Create a symlink to main script -ln -sf /opt/linupdate/linupdate /usr/bin/linupdate +ln -sf /opt/linupdate/linupdate.py /usr/bin/linupdate # Set permissions chmod 750 /etc/linupdate @@ -68,11 +81,27 @@ chmod 750 /opt/linupdate # Only if systemd is installed (not the case on github runners) if [ -f "/usr/bin/systemctl" ];then + # Copy systemd unit file if not exists + if [ ! -f "/lib/systemd/system/linupdate.service" ];then + cp /opt/linupdate/service/linupdate.systemd.template /etc/systemd/system/linupdate.service + fi + # Enable service script by creating a symlink - ln -sf /lib/systemd/system/linupdate.service /etc/systemd/system/linupdate.service + if [ ! -L "/etc/systemd/system/linupdate.service" ];then + # Delete file in case it is a file and not a symlink + rm -f /etc/systemd/system/linupdate.service + ln -sf /lib/systemd/system/linupdate.service /etc/systemd/system/linupdate.service + fi + chmod 550 "$SERVICE" chown root:root "$SERVICE" + # Clean old directories + rm /opt/linupdate/agents-enabled -rf + rm /opt/linupdate/mods-enabled -rf + rm /opt/linupdate/mods-available -rf + + /usr/bin/systemctl enable linupdate /usr/bin/systemctl --quiet daemon-reload # Start service diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df24594..9dad897 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: options: --user root steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get linupdate version run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV @@ -26,19 +26,16 @@ jobs: mkdir -p /tmp/linupdate-build/DEBIAN mkdir -p /tmp/linupdate-build/etc/linupdate/modules mkdir -p /tmp/linupdate-build/opt/linupdate - mkdir -p /tmp/linupdate-build/opt/linupdate/.src/ - mkdir -p /tmp/linupdate-build/opt/linupdate/mods-available/ - mkdir -p /tmp/linupdate-build/opt/linupdate/mods-enabled/ - mkdir -p /tmp/linupdate-build/opt/linupdate/agents-available/ - mkdir -p /tmp/linupdate-build/opt/linupdate/service/ + mkdir -p /tmp/linupdate-build/opt/linupdate/src/ + mkdir -p /tmp/linupdate-build/opt/linupdate/templates/ mkdir -p /tmp/linupdate-build/lib/systemd/system/ - name: Copy files to include in the build run: | - cp -r ${GITHUB_WORKSPACE}/src/* /tmp/linupdate-build/opt/linupdate/.src/ - cp -r ${GITHUB_WORKSPACE}/mods-available/* /tmp/linupdate-build/opt/linupdate/mods-available/ - cp -r ${GITHUB_WORKSPACE}/service/* /tmp/linupdate-build/opt/linupdate/service/ - cp ${GITHUB_WORKSPACE}/linupdate /tmp/linupdate-build/opt/linupdate/linupdate + cp -r ${GITHUB_WORKSPACE}/src/* /tmp/linupdate-build/opt/linupdate/src/ + cp -r ${GITHUB_WORKSPACE}/templates/* /tmp/linupdate-build/opt/linupdate/templates/ + cp ${GITHUB_WORKSPACE}/linupdate.py /tmp/linupdate-build/opt/linupdate/linupdate.py + cp ${GITHUB_WORKSPACE}/service.py /tmp/linupdate-build/opt/linupdate/service.py cp ${GITHUB_WORKSPACE}/version /tmp/linupdate-build/opt/linupdate/version cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /tmp/linupdate-build/lib/systemd/system/linupdate.service @@ -60,7 +57,7 @@ jobs: mv /tmp/linupdate-build.deb /tmp/linupdate_${{ env.VERSION }}_all.deb - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linupdate_${{ env.VERSION }}_all.deb path: /tmp/linupdate_${{ env.VERSION }}_all.deb @@ -70,15 +67,42 @@ jobs: name: Build rpm package runs-on: ubuntu-latest container: - image: centos:7 + image: centos:8 options: --user root steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get linupdate version run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV + - name: Install CentOS8 archive repositories + run: | + rm /etc/yum.repos.d/* -f + + echo "[os] + name=os repo + baseurl=https://vault.centos.org/8-stream/BaseOS/x86_64/os/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/os.repo + + echo "[appstream] + name=updates repo + baseurl=https://vault.centos.org/8-stream/AppStream/x86_64/os/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/appstream.repo + + echo "[extras] + name=extras repo + baseurl=https://vault.centos.org/8-stream/extras/x86_64/extras-common/ + enabled=1 + gpgkey=https://www.centos.org/keys/RPM-GPG-KEY-CentOS-Official-SHA256 + gpgcheck=1" > /etc/yum.repos.d/extras.repo + + dnf clean all + - name: Install dependencies packages run: yum install rpmdevtools rpmlint -y @@ -91,20 +115,16 @@ jobs: mkdir -p $HOME/rpmbuild/SPECS mkdir -p /etc/linupdate/modules mkdir -p /opt/linupdate - mkdir -p /opt/linupdate/.src/ - mkdir -p /opt/linupdate/mods-available/ - mkdir -p /opt/linupdate/mods-enabled/ - mkdir -p /opt/linupdate/agents-available/ - mkdir -p /opt/linupdate/agents-enabled/ - mkdir -p /opt/linupdate/service/ + mkdir -p /opt/linupdate/src/ + mkdir -p /opt/linupdate/templates/ mkdir -p /lib/systemd/system/ - name: Copy files to include in the build run: | - cp -r ${GITHUB_WORKSPACE}/src/* /opt/linupdate/.src/ - cp -r ${GITHUB_WORKSPACE}/mods-available/* /opt/linupdate/mods-available/ - cp -r ${GITHUB_WORKSPACE}/service/* /opt/linupdate/service/ - cp ${GITHUB_WORKSPACE}/linupdate /opt/linupdate/linupdate + cp -r ${GITHUB_WORKSPACE}/src/* /opt/linupdate/src/ + cp -r ${GITHUB_WORKSPACE}/templates/* /opt/linupdate/templates/ + cp ${GITHUB_WORKSPACE}/linupdate.py /opt/linupdate/linupdate.py + cp ${GITHUB_WORKSPACE}/service.py /opt/linupdate/service.py cp ${GITHUB_WORKSPACE}/version /opt/linupdate/version cp -r ${GITHUB_WORKSPACE}/service/linupdate.systemd.template /lib/systemd/system/linupdate.service @@ -120,7 +140,7 @@ jobs: mv $HOME/rpmbuild/RPMS/noarch/linupdate-${{ env.VERSION }}-stable.noarch.rpm /tmp/linupdate-${{ env.VERSION }}.noarch.rpm - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linupdate-${{ env.VERSION }}.noarch.rpm path: /tmp/linupdate-${{ env.VERSION }}.noarch.rpm @@ -134,21 +154,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get linupdate version run: echo "VERSION=$(cat ${GITHUB_WORKSPACE}/version)" >> $GITHUB_ENV # Download builded deb package artifact - name: Download deb artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linupdate_${{ env.VERSION }}_all.deb path: ~/assets # Download builded rpm package artifact - name: Download rpm artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linupdate-${{ env.VERSION }}.noarch.rpm path: ~/assets @@ -163,9 +183,9 @@ jobs: tag_name: ${{ env.VERSION }} release_name: ${{ env.VERSION }} body: | - **Fixes**: + **Changes:** - - **reposerver** module: fixed missing reposerver IP address in the configuration file + - Linupdate python version - initial release draft: false prerelease: false diff --git a/.gitignore b/.gitignore index 78434b7..4044886 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,7 @@ *.swp *.bak TODO -linuxautoupdate.conf -# ignoré car pas fini : +__pycache__ # Compiled source # diff --git a/README.md b/README.md index 1d3b07e..d954d92 100644 --- a/README.md +++ b/README.md @@ -3,34 +3,28 @@ **linupdate** is a package updater tool for Debian and Redhat based OS. -Using apt and yum, it provides basic and avanced update features especially when being managed by a Repomanager reposerver: +Using ``apt`` and ``dnf``, it provides basic and avanced update features especially when being managed by a Repomanager reposerver: - update packages - exclude packages from update - execute pre or post update actions (e.g: restart services) -- receive mail update reports +- send mail update reports - register to a **Repomanager** reposerver and get configuration from that server linupdate is a modular tool. New modules could be added in the future to improve the update experience. -![alt text](https://raw.githubusercontent.com/lbr38/resources/main/screenshots/linupdate/linupdate-1.png) +![alt text](https://github.com/user-attachments/assets/a028db13-d7ef-4b1c-9d01-3fd40b0e538e)

Requirements

-

OS

+**linupdate** ``3`` is regulary tested and should run fine on following systems with python3 installed: -Linupdate **has been tested and runs fine** on following systems: -- Debian 10, 11 -- CentOS 7 - -It may run on most recent Debian/RHEL systems but haven't been tested yet / maybe needs some code update. +- Debian 11, 12 and derivatives (Ubuntu, Mint, ...) +- CentOS 8, 9 and derivatives (Rocky, Alma, ...) -

Installation

+Older OS can still run **linupdate** ``2.x.x`` (bash version) but will not be supported anymore: -``` -git clone https://github.com/lbr38/linupdate.git /tmp/linupdate -cd /tmp/linupdate -./linupdate -``` +- Debian 10 +- CentOS 7

Installation and documentation

@@ -42,34 +36,52 @@ It should help you **installing** and starting using linupdate.

Parameters

- Main:
-  --vv|-vv                                     → Enable verbose mode
-  --version|-v                                 → Print current version
-  --profile|--type|--print-profile PROFILE     → Configure host profile (leave empty to print actual)
-  --environment|--env ENV                      → Configure host environment (leave empty to print actual)
-
- Package update configuration
-  --exclude-major|-em PACKAGE                  → Configure packages to exclude on major release update, separated by a comma. Specify 'none' to clean.
-  --exclude|-e PACKAGE                         → Configure packages to exclude, separated by a comma. Specify 'none' to clean.
-
- Package update execution
-  --check-updates|-cu                          → Check packages to be updated and quit
-  --assume-yes|--force                         → Enable 'assume yes' (answer 'yes' to every confirm prompt)
-  --dist-upgrade|-du                           → Enable 'dist-upgrade' for apt (Debian only)
-  --keep-oldconf|-ko                           → Keep actual configuration file when attempting to be overwrited by apt during package update (Debian only)
-  --ignore-exclude|-ie                         → Ignore all packages minor or major release update exclusions
-
- Modules
-  --list-modules|--list-mod|-m                 → List available modules
-  --mod-enable|-mod-enable|-me MODULE          → Enable specified module
-  --mod-disable|-mod-disable|-md MODULE        → Disable specified module
-  --mod-configure|-mc|--mod-exec MODULE        → Configure specified module (using module commands, see module help or documentation)
-  --mod-configure MODULE --help                → Print module help
-
- Agent
-  --agent-start                                → Start linupdate agent
-  --agent-stop                                 → Stop linupdate agent
-  --agent-restart                              → Restart linupdate agent
-  --agent-enable                               → Enable linupdate agent start on boot
-  --agent-disable                              → Disable linupdate agent start on boot
+ Available options:
+
+    Name                                         Description
+--  -------------------------------------------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+    --help, -h                                   Show help
+    --show-config, -sc                           Show raw configuration
+    --version, -v                                Show version
+    
+    Global configuration options
+    
+    --profile, -p [PROFILE]                      Print current profile or set profile
+    --env, -e [ENVIRONMENT]                      Print current environment or set environment
+    --mail-enable [true|false]                   Enable or disable mail reports
+    --get-mail-recipient                         Get current mail recipient(s)
+    --set-mail-recipient [EMAIL]                 Set mail recipient(s) (separated by commas)
+    
+    Update options
+    
+    --dist-upgrade, -du                          Enable distribution upgrade when updating packages (Debian based OS only)
+    --keep-oldconf                               Keep old configuration files when updating packages (Debian based OS only)
+    --assume-yes, -y                             Answer yes to all questions
+    --check-updates, -cu                         Only check for updates and exit
+    --ignore-exclude, -ie                        Ignore all package exclusions
+    --get-update-method                          Get current update method
+    --set-update-method [one_by_one|global]      Set update method: one_by_one (update packages one by one, one apt command executed for each package) or global (update all packages at once, one single apt command executed for all packages)
+    --exit-on-package-update-error [true|false]  When update method is one_by_one, immediately exit if an error occurs during package update and do not update the remaining packages
+    
+    Packages exclusion and services restart
+    
+    --get-exclude                                Get the current list of packages to exclude from update
+    --get-exclude-major                          Get the current list of packages to exclude from update (if package has a major version update)
+    --get-service-restart                        Get the current list of services to restart after package update
+    --exclude [PACKAGE]                          Set packages to exclude from update (separated by commas)
+    --exclude-major [PACKAGE]                    Set packages to exclude from update (if package has a major version update) (separated by commas)
+    --service-restart [SERVICE]                  Set services to restart after package update (separated by commas)
+    
+    Modules
+    
+    --mod-list                                   List available modules
+    --mod-enable [MODULE]                        Enable a module
+    --mod-disable [MODULE]                       Disable a module
+
+ Usage: linupdate [OPTIONS]
 
+ +

Contact

+ +- For bug reports, issues or features requests, please open a new issue in the Github ``Issues`` section +- A Discord channel is available here for any questions or quick help/debugging (English or French spoken) diff --git a/linupdate b/linupdate deleted file mode 100755 index 258458b..0000000 --- a/linupdate +++ /dev/null @@ -1,410 +0,0 @@ -#!/usr/bin/env bash - -export LC_ALL="en_US.UTF-8" -export TERM="xterm-256color" - - -## ↓ VARIABLES ↓ ## -BASE_DIR="/opt/linupdate" -DATE_DMY=$(date +%d-%m-%Y) # Today date in 'DD-MM-YYYY' format -DATE_YMD=$(date +%Y-%m-%d) # Today date in 'YYYY-MM-DD' format -DATE_FULL=$(date +%d-%m-%Y_%Hh%M) # Today date in 'DD-MM-YYYY_hh-mm' format -TIME=$(date +%H:%M) # Time in '00:00' format -TIME_FULL=$(date +%Hh%Mm%Ss) # Time in '00h00m00s' format -LINUPDATE="${BASE_DIR}/linupdate" # Main program -SRC_DIR="${BASE_DIR}/.src" # Source code directory -LOGS_DIR="/var/log/linupdate" # Logs directory -ETC_DIR="/etc/linupdate" # Configuration files directory -CONF="${ETC_DIR}/linupdate.conf" # Main configuration file -MODULES_DIR="${BASE_DIR}/mods-available" # Available modules directory -MODULES_ENABLED_DIR="${BASE_DIR}/mods-enabled" # Enabled modules directory -AGENTS_ENABLED_DIR="${BASE_DIR}/agents-enabled" # Enabled agents directory -MODULES_CONF_DIR="${ETC_DIR}/modules" # Modules configuration files directory -SERVICE_DIR="${BASE_DIR}/service" # Main agent/service directory -LINUPDATE_PARAMS="$@" -OS_NAME="" -OS_ID="" -OS_VERSION="" -OS_FAMILY="" -KERNEL=$(uname -r) -ARCH=$(uname -m) -VIRT_TYPE="" -PKG_MANAGER="" -PKG_TYPE="" -PROFILE="" -ENV="" -FAILLEVEL="" -MAIL_ENABLED="" -MAIL_RECIPIENT="" -PACKAGES_EXCLUDE_MAJOR="" -PACKAGES_EXCLUDE="" -SERVICE_RESTART="" -HISTORIQUE="${BASE_DIR}/linupdate.history" # Packages updates history file -REPORT="${DATE_YMD}_${TIME_FULL}_linupdate_${HOSTNAME}.log" # Name of the log/report file -LOG="${LOGS_DIR}/${REPORT}" # Location of the log/report file -LOG_REPORT_MAIL="/tmp/${REPORT}" # Same file but this one will be formated to be sent by mail (without ANSI characters) -APT_UPGRADE="upgrade" # upgrade or dist-upgrade (default: upgrade) -APT_OPTIONS="" -YUM_OPTIONS="" -KEEP_OLDCONF="0" -UPDATE_ERROR="0" -SOMETHING_TO_UPDATE="true" -ONLY_CHECK_UPDATE="false" -IGNORE_EXCLUDE="0" -UPDATE_EXCLUDE="" -SERVICE_TO_BE_RESTARTED="" -READ_PACKAGES_TO_EXCLUDE="" -READ_PACKAGES_TO_EXCLUDE_MAJOR="" -ERROR_STATUS="0" -ASSUME_YES="0" -ONLY_UPDATE="0" -PROCID=$(echo "$RANDOM") -VERBOSE="0" -FROM_AGENT="0" -REBOOT_REQUIRED="false" - -# Mods variables -MOD_ERROR="0" -LOADED_MODULES="" - -# Terminal printing -# Colors: -WHITE=$(tput setaf 7) -GRAY=$(tput setaf 8) -GREEN=$(tput setaf 2) -RED=$(tput setaf 1) -YELLOW=$(tput setaf 3) -CYAN=$(tput setaf 6) -RESET=$(tput sgr0) -# Bold version: -WHITEB=$(tput bold;tput setaf 7) -GRAYB=$(tput bold;tput setaf 8) -GREENB=$(tput bold;tput setaf 2) -REDB=$(tput bold;tput setaf 1) -YELLOWB=$(tput bold;tput setaf 3) -CYANB=$(tput bold;tput setaf 6) -# Separator -SEP=$(printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' '=') - -# Retrieve actual version -if [ -f "${BASE_DIR}/version" ];then - VERSION=$(cat ${BASE_DIR}/version) -else - VERSION="" -fi - -# Detecting user -if [ "$(id -u)" -ne "0" ];then - echo -e "\n${YELLOW}Must be executed with sudo ${RESET}\n" - exit -fi - -# Create lock file -if [ ! -f "/tmp/linupdate.lock" ];then - touch "/tmp/linupdate.lock" -fi - -# Create some base directories -mkdir -p "$ETC_DIR" -mkdir -p "$MODULES_CONF_DIR" -mkdir -p "$MODULES_DIR" -mkdir -p "$MODULES_ENABLED_DIR" -mkdir -p "$AGENTS_ENABLED_DIR" -mkdir -p "$SERVICE_DIR" - -# If source code dir does not exist then quit -if [ ! -d "$SRC_DIR" ];then - echo -e "[$YELLOW ERROR $RESET] Some linupdate core files are missing. You might reinstall linupdate." - exit 1 -fi - -# Sourcing all files inside src dir -for SRC_FILE in $(ls -A1 "$SRC_DIR");do - source "$SRC_DIR/$SRC_FILE" -done - -# Check that system is valid before continue -checkSystem - -# Generate config file if not exist -generateConf - -# Patch 2.2.12 -if [ -f "$CONF" ];then - # Replace NEED_RESTART by SERVICE_RESTART - sed -i "s/^NEED_RESTART/SERVICE_RESTART/g" "$CONF" - # Remove ALLOW_SELF_UPDATE - sed -i "/^ALLOW_SELF_UPDATE/d" "$CONF" - # Replace "yes" by "true" in MAIL_ENABLED - sed -i 's/^MAIL_ENABLED="yes"/MAIL_ENABLED="true"/g' "$CONF" - # Replace "no" by "false" in MAIL_ENABLED - sed -i 's/^MAIL_ENABLED="no"/MAIL_ENABLED="false"/g' "$CONF" -fi - - -## ↓ EXECUTION ↓ ## - -# Params pre-check -# If --from-agent param is passed, then rewrite output log filename and make it hidden -if echo "$@" | grep -q "\-\-from\-agent";then - REPORT=".${DATE_YMD}_${TIME_FULL}_linupdate_${HOSTNAME}-agent.log" - LOG="${LOGS_DIR}/${REPORT}" -fi - -# Create logs dir if not exist -mkdir -p "$LOGS_DIR" - -# Create/clean logfile -echo -n> "$LOG" -chmod 660 "$LOG" - -# Writing everything happening to the log file -exec &> >(tee -a "$LOG") - -# Detect virtualization type -if [ -f "/usr/sbin/virt-what" ];then - VIRT_TYPE=$(/usr/sbin/virt-what | tr '\n' ' ') - if [ -z "$VIRT_TYPE" ];then - VIRT_TYPE="Bare metal" - fi -fi - -while [ $# -ge 1 ];do - case "$1" in - --help|-h) - help - clean_exit - ;; - --version|-v|-V) - echo "Version: ${YELLOW}$VERSION${RESET}" - clean_exit - ;; - -vv|--vv) - VERBOSE="1" - ;; - --force|--assume-yes) - ASSUME_YES="1" - ;; - --profile|--type) - # If nothing has been specified then print actual profile - if [ -z "$2" ];then - PROFILE=$(grep "^PROFILE=" $CONF | sed 's/PROFILE=//g' | sed 's/"//g') - echo -e "Current profile: ${YELLOW}$PROFILE${RESET}" - else - # If a profile name has been specified then replace actual profile with it - if grep -q "PROFILE=" $CONF;then - sed -i "s/PROFILE=.*/PROFILE=\"$2\"/g" $CONF && - echo -e "Profil has been changed: ${YELLOW}$2${RESET}" - fi - fi - clean_exit - ;; - --environment|--env) - # If nothing has been specified then print actual env - if [ -z "$2" ];then - CURRENT_ENV=$(grep "^ENV=" $CONF | sed 's/ENV=//g' | sed 's/"//g') - echo -e "Current environment: ${YELLOW}$CURRENT_ENV${RESET}" - else - # If an env name has been specified then replace actual env with it - if grep -q "ENV=" $CONF;then - sed -i "s/ENV=.*/ENV=\"$2\"/g" $CONF && - echo -e "Environment has been changed: ${YELLOW}$2${RESET}" - fi - fi - clean_exit - ;; - --list-modules|--list-mod|-m) - listModules - clean_exit - ;; - --ignore-exclude|-ie) - IGNORE_EXCLUDE=1 - ;; - --check-updates|-cu) - ONLY_CHECK_UPDATE="true" - getConf - checkPackagesBeforeUpdate - clean_exit - ;; - --dist-upgrade|-du) - APT_UPGRADE="dist-upgrade" - ;; - --keep-oldconf|-ko) - export DEBIAN_FRONTEND="noninteractive" - APT_OPTIONS='-o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"' - ;; - --exclude|-e) - if [ ! -z "$2" ];then - READ_PACKAGES_TO_EXCLUDE="$2" - fi - exclude - clean_exit - ;; - --exclude-major|-em) - if [ ! -z "$2" ];then - READ_PACKAGES_TO_EXCLUDE="$2" - fi - READ_PACKAGES_TO_EXCLUDE_MAJOR="1" - exclude - clean_exit - ;; - --mod-enable|-mod-enable|-me) - MODULE=$2 - shift - if [ ! -f "${MODULES_DIR}/${MODULE}.mod" ];then - echo "Error: unknown module ${YELLOW}${MODULE}${RESET}" - clean_exit - fi - - # Enable module - source "${MODULES_DIR}/${MODULE}.mod" - mod_enable && - echo -e "Module ${YELLOW}${MODULE}${RESET} enabled" - clean_exit - ;; - --mod-disable|-mod-disable|-md) - MODULE=$2 - shift - if [ ! -f "${MODULES_DIR}/${MODULE}.mod" ];then - echo "Error: unknown module ${YELLOW}${MODULE}${RESET}" - clean_exit - fi - - # Disable module - source "${MODULES_DIR}/${MODULE}.mod" - mod_disable && - echo -e "Module ${YELLOW}${MODULE}${RESET} disabled" - clean_exit - ;; - --mod-configure|-mc|--mod-exec) - getConf - MODULE=$2 - shift - if [ ! -f "${MODULES_DIR}/${MODULE}.mod" ];then - echo "Error: unknown module ${YELLOW}${MODULE}${RESET}" - clean_exit - fi - - # Configure module - source "${MODULES_DIR}/${MODULE}.mod" - mod_configure $@ && - echo -e "Module ${YELLOW}${MODULE}${RESET} configured" - clean_exit - ;; - --agent-start) - startAgent - clean_exit - ;; - --agent-stop) - stopAgent - clean_exit - ;; - --agent-restart) - restartAgent - clean_exit - ;; - --agent-enable) - enableAgent - clean_exit - ;; - --agent-disable) - disableAgent - clean_exit - ;; - --from-agent) - FROM_AGENT="1" - ;; - *) - echo "Unknown parameter: $1" - help - clean_exit - ;; - esac - shift -done - -echo -e "\n\n - .__ .__ .___ __ - | | |__| ____ __ ________ __| _/____ _/ |_ ____ - | | | |/ \| | \____ \ / __ |\__ \\ __\/ __ \ - | |_| | | \ | / |_> > /_/ | / __ \| | \ ___/ - |____/__|___| /____/| __/\____ |(____ /__| \___ > - \/ |__| \/ \/ \/ - - - ${YELLOW}linupdate${RESET} - advanced package updater for linux distributions\n\n" - -# Reading configuration file -getConf - -# Check if a new linupdate release is available and update (if auto-update enabled) -# selfUpdate - -# Loading modules -loadModules - -# Execute pre-update modules -execPreModules - -echo -e " Hostname: ${YELLOW}${HOSTNAME}${RESET}" -echo -e " OS: ${YELLOW}${OS_NAME} $OS_VERSION ${RESET}" -echo -e " Kernel: ${YELLOW}$KERNEL${RESET}" -if [ ! -z "$VIRT_TYPE" ];then - echo -e " Virtualization: ${YELLOW}${VIRT_TYPE}${RESET}" -fi -echo -e " Profile: ${YELLOW}${PROFILE}${RESET}" -echo -e " Environment: ${YELLOW}${ENV}${RESET}" -echo -e " Executed on: ${YELLOW}${DATE_DMY} ${TIME}${RESET}" -echo -ne " Executed by: ${YELLOW} "; whoami; echo -ne "${RESET}" -echo -ne " Execution method: " -if [ "$FROM_AGENT" == "1" ];then - echo -e " ${YELLOW}from linupdate agent${RESET}" -else - if [ -t 0 ];then - echo -e " ${YELLOW}manual${RESET}" - else - echo -e " ${YELLOW}automatic (no tty)${RESET}" - fi -fi - -# Check if some packages need to be excluded -checkPackagesBeforeUpdate - -# Execute packages update -update - -# Reactivate holded packages by apt-mark -if [ "$OS_FAMILY" == "Debian" ];then - HOLDED_PACKAGES=$(apt-mark showhold) - if [ ! -z "$HOLDED_PACKAGES" ];then - OLD_IFS="$IFS" - IFS=$'\n' - for HOLDED_PACKAGE in $(echo "$HOLDED_PACKAGES");do - apt-mark unhold "$HOLDED_PACKAGE" - done - IFS="$OLD_IFS" - fi -fi - -# Execute post-update modules -execPostModules - -# Restarting services -restartService - -if [ "$UPDATE_ERROR" -gt "0" ];then - echo -e "\nOperation completed with errors\n" -else - echo -e "\nOperation completed\n" -fi - -# Check if a system reboot is required -checkRebootNeeded -if [ "$REBOOT_REQUIRED" == "true" ];then - echo -e "${YELLOW}Reboot is required${RESET}\n" -fi - -# Sending mail report -sendMail - -clean_exit \ No newline at end of file diff --git a/linupdate.py b/linupdate.py new file mode 100755 index 0000000..a9eccc7 --- /dev/null +++ b/linupdate.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 +# coding: utf-8 + +# Import libraries +import socket +from pathlib import Path +from datetime import datetime +from colorama import Fore, Style + +# Import classes +from src.controllers.Log import Log +from src.controllers.App.App import App +from src.controllers.App.Config import Config +from src.controllers.Args import Args +from src.controllers.System import System +from src.controllers.Module.Module import Module +from src.controllers.Package.Package import Package +from src.controllers.Service.Service import Service +from src.controllers.Exit import Exit +from src.controllers.ArgsException import ArgsException + +#----------------------------------------------------------------------------------------------- +# +# Main function +# +#----------------------------------------------------------------------------------------------- +def main(): + exit_code = 0 + send_mail = True + + try: + # Get current date and time + todaydatetime = datetime.now() + date = todaydatetime.strftime('%Y-%m-%d') + time = todaydatetime.strftime('%Hh%Mm%Ss') + logsdir = '/var/log/linupdate' + logfile = date + '_' + time + '_linupdate_' + socket.gethostname() + '.log' + + # Create logs directory + Path(logsdir).mkdir(parents=True, exist_ok=True) + Path(logsdir).chmod(0o750) + + # Instanciate classes + my_exit = Exit() + my_app = App() + my_app_config = Config() + my_args = Args() + my_system = System() + my_module = Module() + my_package = Package() + my_service = Service() + + # Pre-parse arguments to check if --from-agent param is passed + my_args.pre_parse() + + # If --from-agent param is passed, then add -agent to the log filename and make it hidden + if my_args.from_agent: + logfile = '.' + date + '_' + time + '_linupdate_' + socket.gethostname() + '-agent.log' + + # Create log file with correct permissions + Path(logsdir + '/' + logfile).touch() + Path(logsdir + '/' + logfile).chmod(0o640) + + # Log everything to the log file + with Log(logsdir + '/' + logfile): + # Print Logo + my_app.print_logo() + + # Exit if the user is not root + if not my_system.is_root(): + print(Fore.YELLOW + 'Must be executed with sudo' + Style.RESET_ALL) + my_exit.clean_exit(1) + + # Check if the system is supported + my_system.check() + + # Create lock file + my_app.set_lock() + + # Create base directories + my_app.initialize() + + # Generate config file if not exist + my_app_config.generate_conf() + + # Check if there are missing parameters + my_app_config.check_conf() + + # Parse arguments + my_args.parse() + + # Print system & app summary + my_app.print_summary(my_args.from_agent) + + # Load modules + my_module.load() + + # Execute pre-update modules functions + my_module.pre() + + # Execute packages update + my_package.update(my_args.assume_yes, + my_args.ignore_exclude, + my_args.check_updates, + my_args.dist_upgrade, + my_args.keep_oldconf) + + # Execute post-update modules functions + my_module.post(my_package.summary) + + # Restart services + my_service.restart(my_package.summary) + + # Check if reboot is required + if my_system.reboot_required() is True: + print(' ' + Fore.YELLOW + 'Reboot is required' + Style.RESET_ALL) + + # If an ArgsException is raised, print the error message and do not send an email + except ArgsException as e: + print('\n' + Fore.RED + ' ✕ ' + Style.RESET_ALL + str(e) + '\n') + send_mail = False + exit_code = 1 + + # If an exception is raised, print the error message and send an email + except Exception as e: + print('\n' + Fore.RED + ' ✕ ' + Style.RESET_ALL + str(e) + '\n') + exit_code = 1 + + # Exit with exit code and logfile for email report + my_exit.clean_exit(exit_code, send_mail, logsdir + '/' + logfile) + +# Run main function +main() diff --git a/mods-available/agent/reposerver.agent b/mods-available/agent/reposerver.agent deleted file mode 100644 index d901d03..0000000 --- a/mods-available/agent/reposerver.agent +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env bash - -REPOSERVER_CONF="/etc/linupdate/modules/reposerver.conf" -INOTIFY_RUNNING="no" -# ngrep parameters -NGREP_ENABLED="yes" -NGREP_CMD="ngrep -q -t -W byline" -NGREP_GNR_RUNNING="no" -NGREP_PKG_UPDATE_RUNNING="no" -NGREP_PKG_STATUS_RUNNING="no" -NGREP_FULL_HISTORY_RUNNING="no" -MAX_HISTORY="0" -COUNTER="0" - - -## Functions ## - -function general_checks { - # Checking that reposerver module is enabled - if [ ! -f "/opt/linupdate/mods-enabled/reposerver.mod" ];then - echo -e "[ ERROR ] reposerver module is disabled." - exit 1 - fi - - # Checking that a configuration file exists for reposerver module - if [ ! -f "$REPOSERVER_CONF" ];then - echo -e "[ ERROR ] No configuration file '$REPOSERVER_CONF' has been found" - exit 1 - fi - - # Checking that a log file exists for yum/dnf or apt - if [ -f "/var/log/yum.log" ];then - LOGFILE="/var/log/yum.log" - - elif [ -f "/var/log/dnf.log" ];then - LOGFILE="/var/log/dnf.log" - - elif [ -f "/var/log/apt/history.log" ];then - LOGFILE="/var/log/apt/history.log" - - else - echo -e "[ ERROR ] No log file for yum or apt has been found." - exit 1 - fi - - # Retrieving repomanager server IP address - # If this informations is missing then wait for some seconds and retry - REPOSERVER_IP=$(grep "^IP=" "$REPOSERVER_CONF" | sed 's/IP=//g' | sed 's/"//g') - if [ -z "$REPOSERVER_IP" ];then - sleep 3 - - REPOSERVER_IP=$(grep "^IP=" "$REPOSERVER_CONF" | sed 's/IP=//g' | sed 's/"//g') - if [ -z "$REPOSERVER_IP" ];then - echo -e "[ ERROR ] Cannot retrieve Repomanager server IP address" - exit 1 - fi - fi -} - -# Check if this service needs to be restarted -function checkRestartNeeded -{ - if [ -f "/tmp/linupdate-service.restart" ];then - # Only restart the service if linupdate is not running otherwise it could cut off a running update... - if [ -f "/tmp/linupdate.lock" ];then - return - fi - - echo "A restart of this service is required. Restarting..." - rm "/tmp/linupdate-service.restart" -f - exec systemctl --quiet restart repomanager - fi -} - -# Sending general informations -function send_general_status { - /opt/linupdate/linupdate --mod-exec reposerver --from-agent --send-general-status > /dev/null - if [ $? != "0" ];then - echo -e "An error occured while sending data\n" - else - echo -e "Data have been sent\n" - fi -} - -# Sending full events history -function send_full_history { - if [ "$MAX_HISTORY" -gt "0" ];then - /opt/linupdate/linupdate --mod-exec reposerver --from-agent --send-full-history $MAX_HISTORY > /dev/null - else - /opt/linupdate/linupdate --mod-exec reposerver --from-agent --send-full-history > /dev/null - fi - if [ $? != "0" ];then - echo -e "An error occured while sending data\n" - else - echo -e "History has been send\n" - fi -} - -# Sending packages status -function send_packages_status { - /opt/linupdate/linupdate --mod-exec reposerver --from-agent --send-packages-status > /dev/null - if [ $? != "0" ];then - echo -e "An error occured while sending data\n" - else - echo -e "Data have been sent\n" - fi -} - -# Sending full status -function send_full_status { - /opt/linupdate/linupdate --mod-exec reposerver --from-agent --send-full-status > /dev/null - if [ $? != "0" ];then - echo -e "An error occured while sending data\n" - else - echo -e "Data have been sent\n" - fi -} - -# Sending last events history -function inotify_package_event { - if [ "$INOTIFY_RUNNING" == "no" ];then - # If apt/yum log file is being modified then send history to the Repomanager server - while true; do - /usr/bin/inotifywait -q -e modify "$LOGFILE" - echo "New event has been detected in '$LOGFILE' - sending history to the Repomanager server." - - # Executing - MAX_HISTORY="2" - send_full_history - - sleep 2 - done & - - INOTIFY_RUNNING="yes" - fi -} - -# Wait for a general informations sending request -function ngrep_general_update_request { - if [ "$NGREP_GNR_RUNNING" == "no" ];then - while true; do - $NGREP_CMD 'r-general-status' "src host $REPOSERVER_IP" -n 1 > /dev/null - - echo "General informations are requested by the Repomanager server - sending data." - - # Executing - send_general_status - - sleep 2 - done & - - NGREP_GNR_RUNNING="yes" - fi -} - -# Wait for a packages informations sending request -function ngrep_packages_status_request { - if [ "$NGREP_PKG_STATUS_RUNNING" == "no" ];then - while true; do - $NGREP_CMD 'r-pkgs-status' "src host $REPOSERVER_IP" -n 1 > /dev/null - - echo "Packages informations are requested by the Repomanager server - sending data." - - # Executing - send_packages_status - - sleep 2 - done & - - NGREP_PKG_STATUS_RUNNING="yes" - fi -} - -# Wait for a package update request -function ngrep_packages_update_requested { - if [ "$NGREP_PKG_UPDATE_RUNNING" == "no" ];then - while true; do - $NGREP_CMD 'r-update-pkgs' "src host $REPOSERVER_IP" -n 1 > /dev/null - - echo "A package update is requested by the Repomanager server - executing update with linupdate." - - # Executing packages update - /opt/linupdate/linupdate --assume-yes --keep-oldconf - if [ $? != "0" ];then - echo -e "An error occured while executing package update\n" - else - echo -e "Package update is completed\n" - fi - - sleep 2 - done & - - NGREP_PKG_UPDATE_RUNNING="yes" - fi -} - - -## Execution ## - -# Checking that all the necessary elements are present for the agent execution -general_checks - -# Checking if ngrep scans have been disabled -WATCH_FOR_REQUEST=$(grep "^WATCH_FOR_REQUEST=" "$REPOSERVER_CONF" | sed 's/WATCH_FOR_REQUEST=//g' | sed 's/"//g') -if [ "$WATCH_FOR_REQUEST" == "disabled" ];then - NGREP_ENABLED="no" -fi - -# If scans are enabled -if [ "$NGREP_ENABLED" == "yes" ];then - # Retrieving network interface to scan if specified - NGREP_INT=$(grep "^WATCH_INTERFACE=" $REPOSERVER_CONF | sed 's/WATCH_INTERFACE=//g' | sed 's/"//g') - - # If network interface is specified with "auto" or is empty, then try to automatically retrieve default interface - if [ -z "$NGREP_INT" ] || [ "$NGREP_INT" == "auto" ];then - DEFAULT_INT=$(route | grep '^default' | grep -o '[^ ]*$') - DEFAULT_INT_COUNT=$(echo "$DEFAULT_INT" | wc -l) - - if [ -z "$DEFAULT_INT" ];then - echo -e "[ ERROR ] Cannot determine default network interface." - exit 1 - fi - - if [ "$DEFAULT_INT_COUNT" != "1" ];then - echo -e "[ ERROR ] Cannot determine default network interface: multiple default interfaces have been detected." - exit 1 - fi - - # Taking into account the detected network interface - NGREP_CMD="$NGREP_CMD -d $DEFAULT_INT" - - else - # Else taking into account the specified network interface name - NGREP_CMD="$NGREP_CMD -d $NGREP_INT" - fi -fi - -# Executing regular tasks -while true; do - # Checking that all the necessary elements are present for the agent execution. - # This is checked every time in case that a configuration change has been made in the configuration file - general_checks - - # Check if a restart of this service is needed - checkRestartNeeded - - # Regulary sending data to the Repomanager server (every hour) - # 3600 / 5sec (sleep 5) = 720 - if [ "$COUNTER" -eq "0" ] || [ "$COUNTER" -eq "720" ];then - # Sending full status - echo -e "Periodically sending informations about this host to the repomanager server" - send_full_status - - # Reset counter - COUNTER="0" - fi - - # If no inotify process is running, then execute it in background - inotify_package_event - - # If ngrep scans are enabled, then execute them in background - if [ "$NGREP_ENABLED" == "yes" ];then - # Monitor general informations sending requests - ngrep_general_update_request - - # Monitor packages informations sending requests - ngrep_packages_status_request - - # Monitor package update requests - ngrep_packages_update_requested - fi - - sleep 5 - - (( COUNTER++ )) -done - -exit \ No newline at end of file diff --git a/mods-available/configurations/reposerver.conf b/mods-available/configurations/reposerver.conf deleted file mode 100644 index b0457c9..0000000 --- a/mods-available/configurations/reposerver.conf +++ /dev/null @@ -1,17 +0,0 @@ -[MODULE] -FAILLEVEL="" - -[CLIENT] -ID="" -TOKEN="" -GET_PROFILE_PKG_CONF_FROM_REPOSERVER="" -GET_PROFILE_REPOS_FROM_REPOSERVER="" - -[REPOSERVER] -IP="" -URL="" -PACKAGE_TYPE="" - -[AGENT] -WATCH_FOR_REQUEST="enabled" -WATCH_INTERFACE="auto" diff --git a/mods-available/reposerver.mod b/mods-available/reposerver.mod deleted file mode 100644 index 43a6872..0000000 --- a/mods-available/reposerver.mod +++ /dev/null @@ -1,1490 +0,0 @@ -#!/usr/bin/env bash -# Module reposerver -# Module permettant de se ratacher à un serveur de repo exécutant repomanager - -# Fichier de configuration du module -MOD_CONF="${MODULES_CONF_DIR}/reposerver.conf" - -#### FONCTIONS #### - -# Enregistrement auprès d'un serveur Repomanager -function register -{ - # Check that API key is set - if [ -z "$REPOSERVER_API_KEY" ];then - echo -e " [$YELLOW ERROR $RESET] Cannot register to reposerver. You must specify an API key from a Repomanager user account." - ERROR_STATUS=1 - clean_exit - fi - - # Au préalable, récupération des informations concernant le serveur repomanager - # Si la configuration est incomplète alors on quitte - getModConf - - if [ -z "$REPOSERVER_URL" ];then - echo -e " [$YELLOW ERROR $RESET] Cannot register to reposerver. You must configure target reposerver URL." - ERROR_STATUS=1 - clean_exit - fi - - # On teste l'accès à l'url avec un curl pour vérifier que le serveur est joignable - testConnection - - # Tentative d'enregistrement - # Si l'enregistrement fonctionne, on récupère un id et un token - echo -ne " Registering to ${YELLOW}${REPOSERVER_URL}${RESET}: " - REGISTER_HOSTNAME=$(hostname -f) - - if [ -z "$REGISTER_HOSTNAME" ];then - echo -e "[$YELLOW ERROR $RESET] Cannot determine this host's hostname." - ERROR_STATUS=1 - clean_exit - fi - - # Si on n'a pas précisé d'adresse IP à enregistrer alors on tente de récupérer l'adresse IP publique de cette machine - if [ -z "$REGISTER_IP" ];then - REGISTER_IP=$(curl -s -4 ifconfig.io) - if [ -z "$REGISTER_IP" ];then - echo -e "[$YELLOW ERROR $RESET] Cannot determine this host's IP address." - ERROR_STATUS=1 - clean_exit - fi - fi - - CURL=$(curl -L --post301 -s -q -H "Authorization: Bearer $REPOSERVER_API_KEY" -H "Content-Type: application/json" -X POST -d "{\"ip\":\"$REGISTER_IP\",\"hostname\":\"$REGISTER_HOSTNAME\"}" "${REPOSERVER_URL}/api/v2/host/registering" 2> /dev/null) - - # Parsage de la réponse et affichage des messages si il y en a - curl_result_parse - - # Si il y a eu des erreurs suite à la requete alors on quitte - if [ "$CURL_ERROR" -gt "0" ];then - clean_exit - fi - - # Le serveur a dû renvoyer un id et token d'identification qu'on récupère - REGISTER_ID=$(jq -r '.results.id' <<< "$CURL") - REGISTER_TOKEN=$(jq -r '.results.token' <<< "$CURL") - - # Si l'enregistrement a été effectué, on vérifie qu'on a bien pu récupérer un id - if [ -z "$REGISTER_ID" ] || [ "$REGISTER_ID" == "null" ];then - echo -e "[$YELLOW ERROR $RESET] Unable to retrieve an authentication Id from registering." - ERROR_STATUS=1 - clean_exit - fi - - # Si l'enregistrement a été effectué, on vérifie qu'on a bien pu récupérer un token - if [ -z "$REGISTER_TOKEN" ] || [ "$REGISTER_TOKEN" == "null" ];then - echo -e "[$YELLOW ERROR $RESET] Unable to retrieve an authentication token from registering." - ERROR_STATUS=1 - clean_exit - fi - - # Enfin si tout s'est bien passé jusque là, on ajoute l'id et le token dans le fichier de conf et on affiche un message - sed -i "s/^ID.*/ID=\"$REGISTER_ID\"/g" $MOD_CONF - sed -i "s/^TOKEN.*/TOKEN=\"$REGISTER_TOKEN\"/g" $MOD_CONF - clean_exit -} - -# Suppression de l'enregistrement auprès d'un serveur Repomanager -function unregister -{ - # Au préalable, récupération des informations concernant le serveur repomanager - # Si la configuration est incomplète alors on quitte - getModConf - - if [ -z "$REPOSERVER_URL" ];then - echo -e " [$YELLOW ERROR $RESET] Cannot delete registering from reposerver. You must configure target reposerver URL." - ERROR_STATUS=1 - clean_exit - fi - - # Si pas d'ID configuré alors on quitte - if [ -z "$HOST_ID" ];then - echo -e " [$YELLOW ERROR $RESET] This host has no authentication Id." - ERROR_STATUS=1 - clean_exit - fi - - # Si pas de token configuré alors on quitte - if [ -z "$TOKEN" ];then - echo -e " [$YELLOW ERROR $RESET] This host has no authentication token." - ERROR_STATUS=1 - clean_exit - fi - - # On teste l'accès à l'url avec un curl pour vérifier que le serveur est joignable - testConnection - - # Tentative de suppression de l'enregistrement - echo -ne " Unregistering from ${YELLOW}${REPOSERVER_URL}${RESET}: " - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X DELETE "${REPOSERVER_URL}/api/v2/host/registering" 2> /dev/null) - - # Parsage de la réponse et affichage des messages si il y en a - curl_result_parse - - # Si il y a eu des erreurs suite à la requete alors on quitte - if [ "$CURL_ERROR" -gt "0" ];then - clean_exit - fi - - clean_exit -} - -# Teste la connexion au serveur Repomanager -function testConnection -{ - if [ ! -z "$REPOSERVER_API_KEY" ];then - AUTH_HEADER="Authorization: Bearer $REPOSERVER_API_KEY" - fi - - if [ ! -z "$HOST_ID" ] && [ ! -z "$TOKEN" ];then - AUTH_HEADER="Authorization: Host $HOST_ID:$TOKEN" - fi - - if ! curl -L --post301 -s -q -H "$AUTH_HEADER" -H "Content-Type: application/json" -X GET "${REPOSERVER_URL}/api/v2/status" > /dev/null;then - echo -e " [$YELLOW ERROR $RESET] Cannot reach reposerver from ${YELLOW}${REPOSERVER_URL}${RESET}" - ERROR_STATUS=1 - clean_exit - fi -} - -# Analyse le retour d'une requête curl et affiche les éventuels message d'erreurs / de succès rencontrés -function curl_result_parse -{ - CURL_ERROR="0"; - UPDATE_RETURN="" - - # On récupère le code retour si il y en a un - if echo "$CURL" | grep -q ".return";then - UPDATE_RETURN=$(jq -r '.return' <<< "$CURL") - fi - - # Si le code retour est vide il y a probablement eu une erreur côté serveur. - if [ -z "$UPDATE_RETURN" ];then - echo -e "[$YELLOW ERROR $RESET] Reposerver has sent no return code, unknown error." - ERROR_STATUS=1 - CURL_ERROR=1 - return - fi - - # Récupération et affichage des messages - - OLD_IFS=$IFS - IFS=$'\n' - - # Si il y a eu des messages d'erreur on les affiche - if echo "$CURL" | grep -q "message_error";then - # array - UPDATE_MESSAGE_ERROR=($(jq -r '.message_error[]' <<< "$CURL")) - - # $UPDATE_MESSAGE_ERROR est un array pouvant contenir plusieurs messages d'erreurs - for MESSAGE in "${UPDATE_MESSAGE_ERROR[@]}"; do - echo -e "[$YELLOW ERROR $RESET] $MESSAGE" - done - ERROR_STATUS=1 - CURL_ERROR=2 - fi - - # Si il y a eu des message de succès on les affiche - if echo "$CURL" | grep -q '"message"';then - # array - UPDATE_MESSAGE_SUCCESS=($(jq -r '.message[]' <<< "$CURL")) - - # $UPDATE_MESSAGE_SUCCESS est un array pouvant contenir plusieurs messages d'erreurs - for MESSAGE in "${UPDATE_MESSAGE_SUCCESS[@]}"; do - echo -e "[$GREEN OK $RESET] $MESSAGE" - done - fi - - IFS=$OLD_IFS -} - -# Aide -function mod_help -{ - echo -e "${YELLOW}reposerver${RESET} module params:\n\n" - echo -e " Main:" - echo -e " --url http(s)://... → Configure target reposerver URL" - echo -e " --fail-level 1|2|3 → Configure module criticality (between 1 and 3)" - echo -e " 1: linupdate stops no matter the module error (disabled module, the target reposerver does not handle the same package type, minor or critical error...)" - echo -e " 2: linupdate stops on module critical error (continues on minor error)" - echo -e " 3: linupdate continues even in case of module critical error" - echo -e " --register → Register this host to reposerver" - echo -e " --unregister → Unregister this host from reposerver" - echo -e " --get-server-conf → Get reposerver global configuration." - echo -e " --get-profile-packages-conf → Get profile packages excludes configurtion from reposerver." - echo -e " --get-profile-repos → Get repos sources configuration from reposerver." - echo -e " --send-general-status → Send host global informations to reposerver (OS, version, kernel..)" - echo -e " --send-full-history → Send host packages events history to reposerver (installation, update, uninstallation...)" - echo -e " --send-packages-status → Send host packages informations (installed, available...) to resposerver" - echo -e " --send-full-status → Execute the tree previous parameters" - echo -e "" - echo -e " Agent:" - echo -e " --enable-agent → Enable reposerver module agent" - echo -e " This agent will regularly send informations about this host to reposerver (global informations, packages informations...)" - echo -e " --disable-agent → Disable reposerver module agent" - echo -e " --agent-watch-enable → Enable reposerver requests watching" - echo -e " --agent-watch-disable → Disable reposerver requests watching" - echo -e " --agent-watch-int → Specify the network interface on which to watch requests comming from reposerver" - echo -e "" -} - -# Activation du module -function mod_enable -{ - cd ${MODULES_ENABLED_DIR}/ && - ln -sfn "../mods-available/${MODULE}.mod" && - return 0 -} - -# Désactivation du module -function mod_disable -{ - rm "${MODULES_ENABLED_DIR}/reposerver.mod" -f && - return 0 -} - -# Installation du module -function mod_install -{ - # Copie du fichier de configuration - mkdir -p "${MODULES_CONF_DIR}" && - \cp "${MODULES_DIR}/configurations/${MODULE}.conf" ${MODULES_CONF_DIR}/ && - - # Activation du module - mod_enable && - echo -e "Installing ${YELLOW}reposerver${RESET} module: [$GREEN OK $RESET]" - - # Configuration du module - mod_configure -} - -# Activation de l'agent reposerver -function enableReposerverAgent -{ - cd ${AGENTS_ENABLED_DIR}/ && - ln -sfn "../mods-available/agent/reposerver.agent" && - echo -e "${YELLOW}reposerver${RESET} agent enabled" - return 0 -} - -# Désactivation de l'agent reposerver -function disableReposerverAgent -{ - rm "${AGENTS_ENABLED_DIR}/reposerver.agent" -f && - echo -e "${YELLOW}reposerver${RESET} agent disabled" - return 0 -} - -# Configuration du module -function mod_configure -{ - # Si il n'y a aucun fichier de configuration pour ce module, on lance l'installation - if [ ! -f "$MOD_CONF" ];then - mod_install - fi - - REGISTER_HOSTNAME="" - REGISTER_IP="" - - # Configuration du module reposerver.mod (fichier de configuration reposerver.conf) - REPOSERVER_URL="" - FAILLEVEL="" - GET_PROFILE_PKG_CONF_FROM_REPOSERVER="" - GET_PROFILE_REPOS_FROM_REPOSERVER="" - - # Variables de status - UPDATE_REQUEST_TYPE="" - UPDATE_REQUEST_STATUS="" - SEND_GENERAL_STATUS="false" - SEND_PACKAGES_STATUS="false" - SEND_FULL_HISTORY="false" - SEND_FULL_HISTORY_LIMIT="" - - # Défini le status de l'agent linupdate (démarré, arrêté) - if systemctl is-active --quiet linupdate.service;then - AGENT_STATUS="running" - else - AGENT_STATUS="stopped" - fi - # Cependant si le module d'agent reposerver n'est pas activé alors on défini le status de l'agent à 'disabled' - if [ ! -f "${AGENTS_ENABLED_DIR}/reposerver.agent" ];then - AGENT_STATUS="disabled" - fi - - # Récupération des paramètres passés à la fonction - while [ $# -ge 1 ];do - case "$1" in - --help) - mod_help - clean_exit - ;; - --api-key) - REPOSERVER_API_KEY="$2" - shift - - # Check that provided API key name is valid (must start with "ak_") - if ! echo "$REPOSERVER_API_KEY" | grep -q "^ak_";then - ERROR_STATUS=1 - echo "Invalid API key name, must start with 'ak_'" - fi - ;; - --agent-watch-int) - WATCH_INTERFACE="$2" - shift - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^WATCH_INTERFACE" $MOD_CONF;then - sed -i "/^\[AGENT\]/a WATCH_INTERFACE=\"$WATCH_INTERFACE\"" $MOD_CONF - else - sed -i "s|WATCH_INTERFACE=.*|WATCH_INTERFACE=\"$WATCH_INTERFACE\"|g" $MOD_CONF - fi - ;; - --agent-watch-enable) - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^WATCH_FOR_REQUEST" $MOD_CONF;then - sed -i "/^\[AGENT\]/a WATCH_FOR_REQUEST=\"enabled\"" $MOD_CONF - else - sed -i "s|WATCH_FOR_REQUEST=.*|WATCH_FOR_REQUEST=\"enabled\"|g" $MOD_CONF - fi - ;; - --agent-watch-disable) - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^WATCH_FOR_REQUEST" $MOD_CONF;then - sed -i "/^\[AGENT\]/a WATCH_FOR_REQUEST=\"disabled\"" $MOD_CONF - else - sed -i "s|WATCH_FOR_REQUEST=.*|WATCH_FOR_REQUEST=\"disabled\"|g" $MOD_CONF - fi - ;; - --url) - REPOSERVER_URL="$2" - shift - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^URL" $MOD_CONF;then - sed -i "/^\[REPOSERVER\]/a URL=\"$REPOSERVER_URL\"" $MOD_CONF - else - sed -i "s|URL=.*|URL=\"$REPOSERVER_URL\"|g" $MOD_CONF - fi - ;; - --fail-level) - FAILLEVEL="$2" - shift - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^FAILLEVEL" $MOD_CONF;then - sed -i "/^\[MODULE\]/a FAILLEVEL=\"$FAILLEVEL\"" $MOD_CONF - else - sed -i "s/FAILLEVEL=.*/FAILLEVEL=\"$FAILLEVEL\"/g" $MOD_CONF - fi - ;; - --allow-conf-update) - if [ "$2" == "yes" ];then - GET_PROFILE_PKG_CONF_FROM_REPOSERVER="true" - else - GET_PROFILE_PKG_CONF_FROM_REPOSERVER="false" - fi - shift - - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^GET_PROFILE_PKG_CONF_FROM_REPOSERVER" $MOD_CONF;then - sed -i "/^\[CLIENT\]/a GET_PROFILE_PKG_CONF_FROM_REPOSERVER=\"$GET_PROFILE_PKG_CONF_FROM_REPOSERVER\"" $MOD_CONF - else - sed -i "s/GET_PROFILE_PKG_CONF_FROM_REPOSERVER=.*/GET_PROFILE_PKG_CONF_FROM_REPOSERVER=\"$GET_PROFILE_PKG_CONF_FROM_REPOSERVER\"/g" $MOD_CONF - fi - ;; - --allow-repos-update) - if [ "$2" == "yes" ];then - GET_PROFILE_REPOS_FROM_REPOSERVER="true" - else - GET_PROFILE_REPOS_FROM_REPOSERVER="false" - fi - shift - - # Ajout du paramètre dans le fichier de conf - if ! grep -q "^GET_PROFILE_REPOS_FROM_REPOSERVER" $MOD_CONF;then - sed -i "/^\[CLIENT\]/a GET_PROFILE_REPOS_FROM_REPOSERVER=\"$GET_PROFILE_REPOS_FROM_REPOSERVER\"" $MOD_CONF - else - sed -i "s/GET_PROFILE_REPOS_FROM_REPOSERVER=.*/GET_PROFILE_REPOS_FROM_REPOSERVER=\"$GET_PROFILE_REPOS_FROM_REPOSERVER\"/g" $MOD_CONF - fi - ;; - # Récupération de la configuration complète du serveur Repomanager distant - --get-server-conf|--server-get-conf) - getModConf - getServerConf - clean_exit - ;; - # --get-profile-conf|--profile-get-conf) - # getModConf - # getProfileConf - # clean_exit - # ;; - --get-profile-packages-conf) - getModConf - getProfilePackagesConf - clean_exit - ;; - --get-profile-repos|--profile-get-repos) - getModConf - getProfileRepos - clean_exit - ;; - --register) - # Si une adresse IP est précisée alors on enregistrera cette adresse IP - if [ "$2" == "--ip" ] && [ ! -z "$3" ];then - REGISTER_IP="$3" - fi - register - ;; - --unregister) - unregister - ;; - --enable-agent) - enableReposerverAgent - clean_exit - ;; - --disable-agent) - disableReposerverAgent - clean_exit - ;; - --send-full-status) - # Envoi du status complet du serveur - SEND_GENERAL_STATUS="true" - SEND_PACKAGES_STATUS="true" - send_status - ;; - --send-general-status) - # Envoi du status général du serveur (OS, kernel..) - SEND_GENERAL_STATUS="true" - send_status - ;; - --send-packages-status) - SEND_PACKAGES_STATUS="true" - send_status - ;; - --send-full-history) - # Si un chiffre est précisé alors il définira le nombre maximum d'évènements à envoyer - if [ ! -z "$2" ];then - SEND_FULL_HISTORY_LIMIT="$2" - fi - SEND_FULL_HISTORY="true" - send_status - ;; - # *) - # echo "Paramètre de module inconnu: $1" - # clean_exit - # ;; - esac - shift - done -} - -# La fonction mod_load() permet de s'assurer que le module est un minimum configuré avant qu'il soit intégré à l'exécution du programme principal -# Retourner 1 si des éléments sont manquants -# Retourner 0 si tout est OK -function mod_load -{ - # Patch 2.2.12 - if [ -f "$MOD_CONF" ];then - # Remove REPOSERVER_MANAGE_CLIENT_CONF - sed -i '/REPOSERVER_MANAGE_CLIENT_CONF/d' "$MOD_CONF" - # Remove REPOSERVER_MANAGE_CLIENT_REPOS - sed -i '/REPOSERVER_MANAGE_CLIENT_REPOS/d' "$MOD_CONF" - # Remove GET_PROFILE_PARAMS_OVERWRITE - sed -i '/GET_PROFILE_PARAMS_OVERWRITE/d' "$MOD_CONF" - fi - - echo -e " - ${YELLOW}reposerver${RESET}" - - # Si le fichier de configuration du module est introuvable alors on ne charge pas le module - if [ ! -f "$MOD_CONF" ] || [ ! -s "$MOD_CONF" ];then - echo -e " [$YELLOW WARNING $RESET] Module config file is missing. Module cannot be loaded." - return 1 - fi - - # Vérification du contenu du fichier de conf - # On utilise un fichier temporaire pour vérifier et rajouter les éventuels paramètres manquants - TMP_MOD_CONF="/tmp/.linupdate_${PROCID}_mod_reposerver_conf.tmp" - - # Section MODULE - echo -e "[MODULE]" > "$TMP_MOD_CONF" - - # Si le paramètre FAILLEVEL est manquant alors on l'ajoute avec une valeur par défaut - if ! grep -q "^FAILLEVEL=" "$MOD_CONF";then - echo "FAILLEVEL=\"3\"" >> "$TMP_MOD_CONF" - else - grep "^FAILLEVEL=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Section CLIENT - echo -e "\n[CLIENT]" >> "$TMP_MOD_CONF" - - # Si le paramètre ID est manquant alors on l'ajoute avec une valeur par défaut (vide) - if ! grep -q "^ID=" "$MOD_CONF";then - echo "ID=\"\"" >> "$TMP_MOD_CONF" - else - grep "^ID=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Si le paramètre TOKEN est manquant alors on l'ajoute avec une valeur par défaut (vide) - if ! grep -q "^TOKEN=" "$MOD_CONF";then - echo "TOKEN=\"\"" >> "$TMP_MOD_CONF" - else - grep "^TOKEN=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Si le paramètre GET_PROFILE_PKG_CONF_FROM_REPOSERVER est manquant alors on l'ajoute avec une valeur par défaut - if ! grep -q "^GET_PROFILE_PKG_CONF_FROM_REPOSERVER=" "$MOD_CONF";then - echo "GET_PROFILE_PKG_CONF_FROM_REPOSERVER=\"false\"" >> "$TMP_MOD_CONF" - else - grep "^GET_PROFILE_PKG_CONF_FROM_REPOSERVER=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Si le paramètre GET_PROFILE_REPOS_FROM_REPOSERVER est manquant alors on l'ajoute avec une valeur par défaut - if ! grep -q "^GET_PROFILE_REPOS_FROM_REPOSERVER=" "$MOD_CONF";then - echo "GET_PROFILE_REPOS_FROM_REPOSERVER=\"false\"" >> "$TMP_MOD_CONF" - else - grep "^GET_PROFILE_REPOS_FROM_REPOSERVER=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Section REPOSERVER - echo -e "\n[REPOSERVER]" >> "$TMP_MOD_CONF" - - # Si le paramètre URL est manquant alors on l'ajoute avec une valeur par défaut - if ! grep -q "^URL=" "$MOD_CONF";then - echo "URL=\"\"" >> "$TMP_MOD_CONF" - else - grep "^URL=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - if grep -q "^IP=" "$MOD_CONF";then - grep "^IP=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - if grep -q "^PACKAGE_TYPE=" "$MOD_CONF";then - grep "^PACKAGE_TYPE=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Section [AGENT] - echo -e "\n[AGENT]" >> "$TMP_MOD_CONF" - if ! grep -q "^WATCH_FOR_REQUEST=" "$MOD_CONF";then - echo "WATCH_FOR_REQUEST=\"enabled\"" >> "$TMP_MOD_CONF" - else - grep "^WATCH_FOR_REQUEST=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - if ! grep -q "^WATCH_INTERFACE=" "$MOD_CONF";then - echo "WATCH_INTERFACE=\"auto\"" >> "$TMP_MOD_CONF" - else - grep "^WATCH_INTERFACE=" "$MOD_CONF" >> "$TMP_MOD_CONF" - fi - - # Remplacement du fichier de conf par le fichier précédemment construit - cat "$TMP_MOD_CONF" > "$MOD_CONF" - rm -f "$TMP_MOD_CONF" - - # Si l'URL du serveur de repo n'est pas renseignée alors on ne charge pas le module - if [ -z $(grep "^URL=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g') ];then - echo -e " [$YELLOW WARNING $RESET] ${YELLOW}reposerver${RESET} URL is not defined. Module cannot be loaded." - return 1 - fi - - LOADED_MODULES+=("reposerver") - echo -e " - reposerver module: ${YELLOW}Enabled${RESET}" - - return 0 -} - -# Récupération de la configuration complète du module, dans son fichier de conf -function getModConf -{ - # Configuration client (section [CLIENT]) - HOST_ID="$(grep "^ID=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - TOKEN="$(grep "^TOKEN=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - GET_PROFILE_PKG_CONF_FROM_REPOSERVER="$(grep "^GET_PROFILE_PKG_CONF_FROM_REPOSERVER=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - GET_PROFILE_REPOS_FROM_REPOSERVER="$(grep "^GET_PROFILE_REPOS_FROM_REPOSERVER=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - - # Configuration serveur (section [REPOSERVER]) - REPOSERVER_URL="$(grep "^URL=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - REPOSERVER_PACKAGE_TYPE="$(grep "^PACKAGE_TYPE=" $MOD_CONF | cut -d'=' -f2 | sed 's/"//g')" - - # Récupération du FAILLEVEL pour ce module - FAILLEVEL=$(grep "^FAILLEVEL=" "$MOD_CONF" | cut -d'=' -f2 | sed 's/"//g') - - # Si on n'a pas pu récupérer le FAILLEVEL dans le fichier de conf alors on le set à 1 par défaut - # De même si le FAILLEVEL récupéré n'est pas un chiffre alors on le set à 1 - if [ -z "$FAILLEVEL" ];then - echo -e " [$YELLOW WARNING $RESET] FAILLEVEL parameter is not defined for this module → default to 1 (stops on critical or minor error)" - FAILLEVEL="1" - fi - if ! [[ "$FAILLEVEL" =~ ^[1-3]+$ ]];then - echo -e " [$YELLOW WARNING $RESET] FAILLEVEL parameter is not properly defined for this module (must be a numeric value) → default to 1 (stops on critical or minor error)" - FAILLEVEL="1" - fi - - if [ -z "$REPOSERVER_URL" ] || [ "$REPOSERVER_URL" == "null" ];then - echo -e " [$YELLOW ERROR $RESET] reposerver URL is not defined" - return 2 - fi - - return 0 -} - -# Récupération de la configuration générale du serveur de repos -function getServerConf -{ - TMP_FILE_MODULE="/tmp/.linupdate_${PROCID}_mod_reposerver_section_module.tmp" - TMP_FILE_CLIENT="/tmp/.linupdate_${PROCID}_mod_reposerver_section_client.tmp" - TMP_FILE_AGENT="/tmp/.linupdate_${PROCID}_mod_reposerver_section_agent.tmp" - TMP_FILE_REPOSERVER="/tmp/.linupdate_${PROCID}_mod_reposerver_section_reposerver.tmp" - - # On charge les paramètres du module - getModConf - - # Demande de la configuration auprès du serveur de repos - # Ce dernier renverra la configuration au format JSON - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -X GET "${REPOSERVER_URL}/api/v2/profile/server-settings" 2> /dev/null) - curl_result_parse - - # Si il y a eu une erreur lors de la requête on quitte la fonction - if [ "$CURL_ERROR" != "0" ];then - return 2 - fi - - # Puis on récupère la configuration transmise par le serveur au format JSON - # On parcourt chaque configuration et on récupère le nom du fichier à créer, la description et le contenu à insérer - # On remplace à la volée l'environnement dans le contenu récupéré - for ROW in $(echo "${CURL}" | jq -r '.results[] | @base64'); do - _jq() { - echo ${ROW} | base64 --decode | jq -r ${1} - } - - REPOSERVER_IP=$(_jq '.Ip') - REPOSERVER_URL=$(_jq '.Url') - REPOSERVER_PACKAGE_TYPE=$(_jq '.Package_type') - done - - # Retrieve the server IP address from the server URL - i="1" - REPOSERVER_IP=$(echo "$REPOSERVER_URL" | sed 's/https\?:\/\///g' | cut -d'/' -f1) - - # Some DNS servers return multiple IP addresses for a single domain name or a CNAME value - # Loop until we get a real single IP address - while [[ ! "$REPOSERVER_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]];do - REPOSERVER_IP=$(dig +short "$REPOSERVER_IP" | head -n${i} | tail -n1) - (( i++ )) - - # If we can't retrieve the IP address after 10 tries, stop the loop - if [ "$i" -gt 10 ];then - echo -e " [$YELLOW ERROR $RESET] Unable to retrieve the IP address of the reposerver" - fi - done - - # Sauvegarde de la partie [MODULE] - sed -n -e '/\[MODULE\]/,/^$/p' "$MOD_CONF" > "$TMP_FILE_MODULE" - # Ajout d'un saut de ligne car chaque section doit être correctement séparée - echo "" >> "$TMP_FILE_MODULE" - - # Sauvegarde de la partie [CLIENT] - sed -n -e '/\[CLIENT\]/,/^$/p' "$MOD_CONF" > "$TMP_FILE_CLIENT" - # Ajout d'un saut de ligne car chaque section doit être correctement séparée - echo "" >> "$TMP_FILE_CLIENT" - - # Sauvegarde de la partie [AGENT] si existe - sed -n -e '/\[AGENT\]/,/^$/p' "$MOD_CONF" > "$TMP_FILE_AGENT" - # Ajout d'un saut de ligne car chaque section doit être correctement séparée - echo "" >> "$TMP_FILE_AGENT" - - # Nouvelle conf [REPOSERVER] - echo "[REPOSERVER]" >> "$TMP_FILE_REPOSERVER" - echo "URL=\"$REPOSERVER_URL\"" >> "$TMP_FILE_REPOSERVER" - echo "IP=\"$REPOSERVER_IP\"" >> "$TMP_FILE_REPOSERVER" - echo "PACKAGE_TYPE=\"$REPOSERVER_PACKAGE_TYPE\"" >> "$TMP_FILE_REPOSERVER" - echo "" >> "$TMP_FILE_REPOSERVER" - - # On reconstruit le fichier de configuration - cat "$TMP_FILE_MODULE" > "$MOD_CONF" - cat "$TMP_FILE_CLIENT" >> "$MOD_CONF" - cat "$TMP_FILE_REPOSERVER" >> "$MOD_CONF" - cat "$TMP_FILE_AGENT" >> "$MOD_CONF" - - # Suppression des doubles saut de ligne si il y en a - sed -i '/^$/N;/^\n$/D' "$MOD_CONF" - - # Suppression des fichiers temporaires - rm "$TMP_FILE_MODULE" -f - rm "$TMP_FILE_CLIENT" -f - rm "$TMP_FILE_AGENT" -f - rm "$TMP_FILE_REPOSERVER" -f - - # Puis on recharge à nouveau les paramètres - getModConf -} - -# Fonction exécutée pre-mise à jour -function preCheck -{ - # Vérification que le serveur Repomanager gère le même type de paquet que cet hôte - if ! echo "$REPOSERVER_PACKAGE_TYPE" | grep -q "$PKG_TYPE";then - echo -e " [${YELLOW} ERROR ${RESET}] reposerver do not handle the same package type as this host. Reposerver: $REPOSERVER_PACKAGE_TYPE / Host: $PKG_TYPE" - return 2 - fi -} - -# Get profile general configuration from reposerver -# function getProfileConf -# { -# # Si le serveur reposerver ne gère pas les profils ou que le client refuse d'être mis à jour par son serveur de repo, on quitte la fonction -# echo -ne " → Getting ${YELLOW}${PROFILE}${RESET} profile configuration: " - -# # Demande de la configuration des repos auprès du serveur de repos -# # Ce dernier renverra la configuration au format JSON -# CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -X GET "${REPOSERVER_URL}/api/v2/profile/${PROFILE}" 2> /dev/null) -# curl_result_parse - -# # Si il y a eu une erreur lors de la requête on quitte la fonction -# if [ "$CURL_ERROR" != "0" ];then -# return 2 -# fi - -# # Puis on récupère la configuration transmise par le serveur au format JSON -# # On parcourt chaque configuration et on récupère le nom du fichier à créer, la description et le contenu à insérer -# # On remplace à la volée l'environnement dans le contenu récupéré -# for ROW in $(echo "${CURL}" | jq -r '.results[] | @base64'); do -# _jq() { -# echo ${ROW} | base64 --decode | jq -r ${1} -# } - -# GET_PROFILE_PKG_CONF_FROM_REPOSERVER=$(_jq '.Linupdate_get_pkg_conf') -# GET_PROFILE_REPOS_FROM_REPOSERVER=$(_jq '.Linupdate_get_repos_conf') -# done - -# if [ "$GET_PROFILE_PKG_CONF_FROM_REPOSERVER" == "null" ];then -# echo -e "[$YELLOW ERROR $RESET] Server sent ${YELLOW}null${RESET} data" -# return 2 -# fi - -# if [ "$GET_PROFILE_REPOS_FROM_REPOSERVER" == "null" ];then -# echo -e "[$YELLOW ERROR $RESET] Server sent ${YELLOW}null${RESET} data" -# return 2 -# fi - -# # converting to boolean -# if [ "$GET_PROFILE_PKG_CONF_FROM_REPOSERVER" == "no" ];then -# GET_PROFILE_PKG_CONF_FROM_REPOSERVER="false" -# fi -# if [ "$GET_PROFILE_PKG_CONF_FROM_REPOSERVER" == "yes" ];then -# GET_PROFILE_PKG_CONF_FROM_REPOSERVER="true" -# fi -# if [ "$GET_PROFILE_REPOS_FROM_REPOSERVER" == "no" ];then -# GET_PROFILE_REPOS_FROM_REPOSERVER="false" -# fi -# if [ "$GET_PROFILE_REPOS_FROM_REPOSERVER" == "yes" ];then -# GET_PROFILE_REPOS_FROM_REPOSERVER="true" -# fi - -# # On applique la nouvelle configuration récupérée -# sed -i "s/GET_PROFILE_PKG_CONF_FROM_REPOSERVER.*/GET_PROFILE_PKG_CONF_FROM_REPOSERVER=\"$GET_PROFILE_PKG_CONF_FROM_REPOSERVER\"/g" "$MOD_CONF" -# sed -i "s/GET_PROFILE_REPOS_FROM_REPOSERVER.*/GET_PROFILE_REPOS_FROM_REPOSERVER=\"$GET_PROFILE_REPOS_FROM_REPOSERVER\"/g" "$MOD_CONF" - -# echo -e "[${GREEN} OK ${RESET}]" - -# # Enfin on applique la nouvelle conf en récupérant de nouveau les paramètres du fichier de conf : -# getConf -# } - -# Get profile packages configuratin (packages excludes) -function getProfilePackagesConf -{ - echo -ne " → Getting ${YELLOW}${PROFILE}${RESET} profile packages configuration: " - - # Demande de la configuration des paquets auprès du serveur de repos - # Ce dernier renverra la configuration au format JSON - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -X GET "${REPOSERVER_URL}/api/v2/profile/${PROFILE}/excludes" 2> /dev/null) - curl_result_parse - - # Si il y a eu une erreur lors de la requête on quitte la fonction - if [ "$CURL_ERROR" != "0" ];then - return 2 - fi - - if [ "$GET_PROFILE_PKG_CONF_FROM_REPOSERVER" == "false" ];then - echo -e "${YELLOW}Disabled${RESET}" - return 1 - fi - - # Puis on récupère la configuration transmise par le serveur au format JSON - # On parcourt chaque configuration et on récupère le nom du fichier à créer, la description et le contenu à insérer - # On remplace à la volée l'environnement dans le contenu récupéré - for ROW in $(echo "${CURL}" | jq -r '.results[] | @base64'); do - _jq() { - echo ${ROW} | base64 --decode | jq -r ${1} - } - - EXCLUDE_MAJOR=$(_jq '.Package_exclude_major') - EXCLUDE=$(_jq '.Package_exclude') - SERVICE_RESTART=$(_jq '.Service_restart') - done - - # Si la valeur des paramètres == null alors cela signifie qu'il n'y a aucune exclusion de paquet - if [ "$EXCLUDE_MAJOR" == "null" ];then - EXCLUDE_MAJOR="" - fi - if [ "$EXCLUDE" == "null" ];then - EXCLUDE="" - fi - if [ "$SERVICE_RESTART" == "null" ];then - SERVICE_RESTART="" - fi - - # On applique la nouvelle configuration récupérée - # D'abord on nettoie la partie [SOFT] du fichier de conf car c'est cette partie qui va être remplacée par la nouvelle conf : - sed -i '/^\[SOFTWARE CONFIGURATION\]/,$d' "$CONF" && - - # Puis on injecte la nouvelle conf récupérée - echo -e "[SOFTWARE CONFIGURATION]\nEXCLUDE_MAJOR=\"${EXCLUDE_MAJOR}\"\nEXCLUDE=\"${EXCLUDE}\"\nSERVICE_RESTART=\"${SERVICE_RESTART}\"" >> "$CONF" - - echo -e "[${GREEN} OK ${RESET}]" - - # Enfin on applique la nouvelle conf en récupérant de nouveau les paramètres du fichier de conf : - getConf -} - - -# Récupération de la configuration des repos du profil de l'hôte, auprès du serveur de repos -function getProfileRepos -{ - # Si on est autorisé à mettre à jour les fichiers de conf de repos et si le serveur de repos le gère - echo -ne " → Getting ${YELLOW}${PROFILE}${RESET} profile repositories: " - - # Demande de la configuration des repos auprès du serveur de repos - # Ce dernier renverra la configuration au format JSON - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -X GET "${REPOSERVER_URL}/api/v2/profile/${PROFILE}/repos" 2> /dev/null) - curl_result_parse - - # Si il y a eu une erreur lors de la requête on quitte la fonction - if [ "$CURL_ERROR" != "0" ];then - return 2 - fi - - # Sinon on récupère les configurations de repos que la requête a renvoyé - # On s'assure que le paramètre 'configuraiton' fait bien partie de la réponse JSON renvoyée par le serveur - # Ce paramètre peut être vide toutefois si la configuration du profil côté serveur n'a aucun repo de configuré - if ! echo "$CURL" | grep -q "results";then - echo -e "[$YELLOW ERROR $RESET] $PROFILE profile repos sources configuration have not been sent by reposerver." - return 2 - fi - - if [ "$GET_PROFILE_REPOS_FROM_REPOSERVER" == "false" ];then - echo -e "${YELLOW}Disabled${RESET}" - return 1 - fi - - # Si le paramètre existe alors on peut continuer le traitement - # D'abord on vide les fichiers .repo ou .list présents sur l'hôte car ils seront remplacés par la configuration transférée par le serveur. - if [ "$OS_FAMILY" == "Redhat" ];then - rm /etc/yum.repos.d/*.repo -f - fi - if [ "$OS_FAMILY" == "Debian" ];then - echo -n> /etc/apt/sources.list - rm /etc/apt/sources.list.d/*.list -f - fi - - # Puis on récupère la configuration des nouveaux fichiers .repo ou .list transmis par le serveur au format JSON - # On parcourt chaque configuration et on récupère le nom du fichier à créer, la description et le contenu à insérer - # On remplace à la volée l'environnement dans le contenu récupéré - IFS=$'\n' - for ROW in $(echo "${CURL}" | jq -r '.results[] | @base64'); do - _jq() { - echo ${ROW} | base64 --decode | jq -r ${1} - } - - FILENAME=$(_jq '.filename') - DESCRIPTION=$(_jq '.description') - CONTENT=$(_jq '.content' | sed "s/__ENV__/${ENV}/g") - - if [ "$OS_FAMILY" == "Redhat" ];then - FILENAME_PATH="/etc/yum.repos.d/$FILENAME" - fi - - if [ "$OS_FAMILY" == "Debian" ];then - FILENAME_PATH="/etc/apt/sources.list.d/$FILENAME" - fi - - # Si le fichier n'existe pas déjà on insert la description en début de fichier - if [ ! -f "$FILENAME_PATH" ];then - echo "# $DESCRIPTION" > "$FILENAME_PATH" - fi - - # Puis on insert le contenu - echo -e "${CONTENT}\n" >> "$FILENAME_PATH" - done - - # Set permissions and reload cache - if [ "$OS_FAMILY" == "Redhat" ];then - if ls -A /etc/yum.repos.d/ | grep -q "\.repo";then - chown root:root /etc/yum.repos.d/*.repo - chmod 660 /etc/yum.repos.d/*.repo - fi - - checkYumLock - yum clean all -q - fi - if [ "$OS_FAMILY" == "Debian" ];then - if ls -A /etc/apt/sources.list.d/ | grep -q "\.list";then - chown root:root /etc/apt/sources.list.d/*.list - chmod 660 /etc/apt/sources.list.d/*.list - fi - - apt-get clean - fi - - echo -e "[$GREEN OK $RESET]" -} - -# Exécution pre-mise à jour des paquets -function pre -{ - # Fail-level : - # 1 = quitte à la moindre erreur (module désactivé, le serveur ne gère pas le même OS, erreur mineure, critique) - # 2 = quitte seulement en cas d'erreur critique - # 3 = continue même en cas d'erreur critique (ex : impossible de récupérer la conf du serveur de repo), la machine se mettra à jour selon la conf actuellement en place dans son fichier de conf - - # Codes de retour : - # Aucune erreur : return 0 - # Erreur mineure : return 1 - # Erreur critique : return 2 - - echo -e " → Executing ${YELLOW}reposerver${RESET} module" - - # On récupère la configuration du module, en l'occurence la configuration du serveur de repo - getModConf - RESULT="$?" - if [ "$FAILLEVEL" -le "2" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi # Si FAILLEVEL = 1 ou 2 - if [ "$FAILLEVEL" -eq "3" ] && [ "$RESULT" -gt "0" ];then return 1;fi # Si FAILLEVEL = 3 et qu'il y a une erreur au chargement de la conf du module alors on quitte le module sans pour autant quitter repomanager (clean_exit) - - # On met à jour la configuration du serveur de repo distant en lui demandant de renvoyer sa conf - getServerConf - RESULT="$?" - if [ "$FAILLEVEL" -eq "1" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi - if [ "$FAILLEVEL" -eq "2" ] && [ "$RESULT" -ge "2" ];then (( MOD_ERROR++ )); clean_exit;fi - if [ "$FAILLEVEL" -eq "3" ] && [ "$RESULT" -gt "0" ];then return 1;fi # Si FAILLEVEL = 3 et qu'il y a une erreur au chargement de la conf du module alors on quitte le module sans pour autant quitter repomanager (clean_exit) - - # On vérifie que la configuration du serveur de repo est compatible avec notre OS - preCheck - RESULT="$?" - if [ "$FAILLEVEL" -eq "1" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi - if [ "$FAILLEVEL" -eq "2" ] && [ "$RESULT" -ge "2" ];then (( MOD_ERROR++ )); clean_exit;fi - - # On met à jour notre configuration à partir du serveur de repo (profils), si cela est autorisé des deux côtés - # getProfileConf - # RESULT="$?" - # if [ "$FAILLEVEL" -eq "1" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi - # if [ "$FAILLEVEL" -eq "2" ] && [ "$RESULT" -ge "2" ];then (( MOD_ERROR++ )); clean_exit;fi - - getProfilePackagesConf - RESULT="$?" - if [ "$FAILLEVEL" -eq "1" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi - if [ "$FAILLEVEL" -eq "2" ] && [ "$RESULT" -ge "2" ];then (( MOD_ERROR++ )); clean_exit;fi - - # On met à jour notre configuration des repos à partir du serveurs de repo (profils), si cela est autorisé des deux côtés - getProfileRepos - RESULT="$?" - if [ "$FAILLEVEL" -eq "1" ] && [ "$RESULT" -gt "0" ];then (( MOD_ERROR++ )); clean_exit;fi - if [ "$FAILLEVEL" -eq "2" ] && [ "$RESULT" -ge "2" ];then (( MOD_ERROR++ )); clean_exit;fi - - # Aquittement du status auprès du serveur reposerver - UPDATE_REQUEST_TYPE="packages-update" - UPDATE_REQUEST_STATUS="running" - update_request_status - - return 0 -} - -# Exécution post-mise à jour des paquets -function post -{ - # Aquittement du status auprès du serveur reposerver - UPDATE_REQUEST_TYPE="packages-update" - if [ "$UPDATE_ERROR" -gt "0" ];then - UPDATE_REQUEST_STATUS="error" - else - UPDATE_REQUEST_STATUS="done" - fi - update_request_status - - # Si il y a eu des paquets à mettre à jour lors de cette exécution alors on exécute les actions suivantes - if [ "$SOMETHING_TO_UPDATE" == "true" ];then - # Généralement les paquets "*-release" sur Redhat/CentOS remettent en place des fichiers .repo. Si un paquet de ce type a été mis à jour alors on remets à jour la configuration des repos à partir du serveurs de repo (profils), si cela est autorisé des deux côtés - if echo "${PACKAGES[*]}" | grep -q "-release";then - getProfileRepos - fi - - # On renvoie les 4 derniers historique d'évènements au serveur reposerver - /opt/linupdate/linupdate --mod-configure reposerver --from-agent --send-full-history 4 - fi - - return 0 -} - -## Envoi de status (API) ## - -# Envoi au serveur Repomanager l'état actuel de l'hôte -# Fonction principale -function send_status -{ - # Au préalable, récupération des informations concernant le serveur repomanager - # Si la configuration est incomplète alors on quitte - getModConf - - OLD_IFS=$IFS - IFS=$'\n' - - # On teste l'accès à l'url avec un curl pour vérifier que le serveur est joignable - testConnection - - # Envoi du récapitulatif de toutes les mises à jour effectuées à partir du fichier historique - - # Si on n'a pas d'ID ou de token alors on ne peut pas effectuer cette opération - if [ -z "$HOST_ID" ];then - echo -e "[$YELLOW ERROR $RESET] Host Id not defined" - ERROR_STATUS=1 - clean_exit - fi - if [ -z "$TOKEN" ];then - echo -e "[$YELLOW ERROR $RESET] Host token not defined" - ERROR_STATUS=1 - clean_exit - fi - - # Exécution des sous-fonctions - - # Général - if [ "$SEND_GENERAL_STATUS" == "true" ];then - send_general_status - fi - - # Paquets - if [ "$SEND_PACKAGES_STATUS" == "true" ];then - send_packages_status - fi - - # Historique des évènements apt ou yum - if [ "$SEND_FULL_HISTORY" == "true" ];then - genFullHistory - fi - - IFS="$OLD_IFS" - - clean_exit -} - -# Mettre à jour le status d'une demande initialisée par le serveur repomanager -function update_request_status -{ - if [ -z "$UPDATE_REQUEST_TYPE" ];then - return - fi - if [ -z "$UPDATE_REQUEST_STATUS" ];then - return - fi - - if [ "$VERBOSE" -gt "0" ];then - echo -ne " Acknowledging reposerver request: " - fi - - CURL_PARAMS="\"status\":\"$UPDATE_REQUEST_STATUS\"" - - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X PUT -d "{$CURL_PARAMS}" "${REPOSERVER_URL}/api/v2/host/request/$UPDATE_REQUEST_TYPE" 2> /dev/null) - - # On n'affiche les message d'erreur et de succès uniquement si la verbosité est supérieur à 0 - if [ "$VERBOSE" -gt "0" ];then - curl_result_parse - fi -} - -# Envoi au serveur Repomanager l'état général de l'hôte (son os, version, profil, env) -function send_general_status -{ - UPDATE_REQUEST_TYPE="general-status-update" - UPDATE_REQUEST_STATUS="running" - update_request_status - - UPDATE_MESSAGE_SUCCESS="" - UPDATE_MESSAGE_ERROR="" - - # Check if reboot is needed - checkRebootNeeded - - CURL_PARAMS="" - - # Paramètres généraux (os, version, profil...) - if [ ! -z "$HOSTNAME" ];then - CURL_PARAMS+="\"hostname\":\"$HOSTNAME\"," - fi - if [ ! -z "$OS_NAME" ];then - CURL_PARAMS+="\"os\":\"$OS_NAME\"," - fi - if [ ! -z "$OS_VERSION" ];then - CURL_PARAMS+="\"os_version\":\"$OS_VERSION\"," - fi - if [ ! -z "$OS_FAMILY" ];then - CURL_PARAMS+="\"os_family\":\"$OS_FAMILY\"," - fi - if [ ! -z "$VIRT_TYPE" ];then - CURL_PARAMS+="\"type\":\"$VIRT_TYPE\"," - fi - if [ ! -z "$KERNEL" ];then - CURL_PARAMS+="\"kernel\":\"$KERNEL\"," - fi - if [ ! -z "$ARCH" ];then - CURL_PARAMS+="\"arch\":\"$ARCH\"," - fi - if [ ! -z "$PROFILE" ];then - CURL_PARAMS+="\"profile\":\"$PROFILE\"," - fi - if [ ! -z "$ENV" ];then - CURL_PARAMS+="\"env\":\"$ENV\"," - fi - if [ ! -z "$AGENT_STATUS" ];then - CURL_PARAMS+="\"agent_status\":\"$AGENT_STATUS\"," - fi - if [ ! -z "$VERSION" ];then - CURL_PARAMS+="\"linupdate_version\":\"$VERSION\"," - fi - if [ ! -z "$REBOOT_REQUIRED" ];then - CURL_PARAMS+="\"reboot_required\":\"$REBOOT_REQUIRED\"," - fi - - # Delete the last comma - CURL_PARAMS=$(echo "${CURL_PARAMS::-1}") - - # Fin de construction des paramètres curl puis envoi. - - # Envoi des données : - echo -e "→ Sending status to ${YELLOW}${REPOSERVER_URL}${RESET}: " - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X PUT -d "{$CURL_PARAMS}" "${REPOSERVER_URL}/api/v2/host/status" 2> /dev/null) - - # Récupération et affichage des messages - curl_result_parse - - if [ "$CURL_ERROR" -eq "0" ];then - UPDATE_REQUEST_STATUS="done" - else - UPDATE_REQUEST_STATUS="error" - fi - - update_request_status -} - -# Envoi du status des paquets (installés, disponibles) -function send_packages_status -{ - INSTALLED_PACKAGES="" - UPDATE_MESSAGE_SUCCESS="" - UPDATE_MESSAGE_ERROR="" - - UPDATE_REQUEST_TYPE="packages-status-update" - UPDATE_REQUEST_STATUS="running" - update_request_status - - # Exécution des différentes fonctions - - # Sauf si il y a une erreur, le status sera done - UPDATE_REQUEST_STATUS="done" - - genFullHistory - if [ "$?" -ne "0" ];then - UPDATE_REQUEST_STATUS="error" - fi - - send_available_packages_status - if [ "$?" -ne "0" ];then - UPDATE_REQUEST_STATUS="error" - fi - - send_installed_packages_status - if [ "$?" -ne "0" ];then - UPDATE_REQUEST_STATUS="error" - fi - - update_request_status -} - -# Envoi au serveur Repomanager de la liste des paquets installés sur l'hôte -function send_installed_packages_status -{ - INSTALLED_PACKAGES="" - UPDATE_MESSAGE_SUCCESS="" - UPDATE_MESSAGE_ERROR="" - - # Paramètres concernant les paquets installés sur le système (tous les paquets) - echo "Building installed packages list..." - - INSTALLED_PACKAGES_TMP="/tmp/.linupdate_${PROCID}_mod_reposerver_installed_pkgs.tmp" - - # Construction de la liste des paquets - # Cas Redhat - if [ "$OS_FAMILY" == "Redhat" ];then - repoquery -a --installed --qf="%{name} %{epoch}:%{version}-%{release}.%{arch}" > "$INSTALLED_PACKAGES_TMP" - fi - # Cas Debian - if [ "$OS_FAMILY" == "Debian" ];then - dpkg-query -W -f='${Status}\t${package}\t${version}\t\n' | grep "^install ok installed" | awk '{print $4, $5}' > "$INSTALLED_PACKAGES_TMP" - fi - - # Parsage des lignes des fichiers splités - for LINE in $(cat "$INSTALLED_PACKAGES_TMP");do - - if [ "$OS_FAMILY" == "Redhat" ];then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}') - PACKAGE_ACT_VERSION=$(echo "$LINE" | awk '{print $2}') - # On retire l'epoch si celui-ci vaut 0: (epoch : https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/RPM_Guide/ch09s03.html) - PACKAGE_ACT_VERSION=$(echo "$PACKAGE_ACT_VERSION" | sed 's/^0://g') - fi - if [ "$OS_FAMILY" == "Debian" ];then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_ACT_VERSION=$(echo "$LINE" | awk '{print $2}' | sed 's/"//g' | sed "s/'//g") - fi - - # Si le nom du paquet est vide, on passe au suivant - if [ -z "$PACKAGE_NAME" ];then - continue - fi - - # Ajout du nom du paquet, sa version actuelle et sa version disponible à l'array $INSTALLED_PACKAGES - INSTALLED_PACKAGES+="${PACKAGE_NAME}|${PACKAGE_ACT_VERSION}," - done - - rm "$INSTALLED_PACKAGES_TMP" -f - - # Suppression de la dernière virgule : - INSTALLED_PACKAGES=$(echo "${INSTALLED_PACKAGES::-1}") - - # Construction des paramètres curl à envoyer - CURL_PARAMS="\"installed_packages\":\"$INSTALLED_PACKAGES\"" - - # Envoi des données : - echo -ne "→ Sending data to ${YELLOW}${REPOSERVER_URL}${RESET}: " - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X PUT -d "{$CURL_PARAMS}" "${REPOSERVER_URL}/api/v2/host/packages/installed" 2> /dev/null) - - # Récupération et affichage des messages - curl_result_parse - - if [ "$CURL_ERROR" -eq "0" ];then - UPDATE_REQUEST_STATUS="done" - return 0 - - else - UPDATE_REQUEST_STATUS="error" - return 1 - fi -} - -# Envoi au serveur Repomanager de la liste des paquets disponibles pour mettre à jour -function send_available_packages_status -{ - AVAILABLE_PACKAGES="" - - # Paramètres concernant les paquets (paquets disponibles...) - - echo "Building available packages list..." - - # Récupération des paquets disponibles - AVAILABLE_PACKAGES_TMP="/tmp/.linupdate_${PROCID}_mod_reposerver_available_pkgs.tmp" - - # Cas Redhat - if [ "$OS_FAMILY" == "Redhat" ];then - # Récupération des paquets disponibles pour mise à jour - repoquery -q -a --qf="%{name} %{epoch}:%{version}-%{release}.%{arch}" --pkgnarrow=updates > "$AVAILABLE_PACKAGES_TMP" - fi - # Cas Debian - if [ "$OS_FAMILY" == "Debian" ];then - # Récupération des paquets disponibles pour mise à jour - aptitude -F"%p %v %V" --disable-columns search ~U > "$AVAILABLE_PACKAGES_TMP" - fi - - # Si le fichier généré est vide, alors il n'y a aucun paquet à mettre à jour, on n'envoit rien à Repomanager - if [ ! -s "$AVAILABLE_PACKAGES_TMP" ];then - AVAILABLE_PACKAGES="none" - - else - # Sinon on parcourt toutes les lignes du fichiers pour lister les paquets disponibles - for LINE in $(cat "$AVAILABLE_PACKAGES_TMP");do - if [ "$OS_FAMILY" == "Redhat" ];then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}') - PACKAGE_AVL_VERSION=$(echo "$LINE" | awk '{print $2}') - # On retire l'epoch si celui-ci vaut 0: - PACKAGE_AVL_VERSION=$(echo "$PACKAGE_AVL_VERSION" | sed 's/^0://g') - fi - if [ "$OS_FAMILY" == "Debian" ];then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}') - PACKAGE_AVL_VERSION=$(echo "$LINE" | awk '{print $3}') - fi - - # Si le nom du paquet est vide alors on passe au suivant - if [ -z "$PACKAGE_NAME" ];then - continue - fi - - # Ajout du nom du paquet, sa version actuelle et sa version disponible à l'array $AVAILABLE_PACKAGES - AVAILABLE_PACKAGES+="${PACKAGE_NAME}|${PACKAGE_AVL_VERSION}," - done - - # Suppression de la dernière virgule : - AVAILABLE_PACKAGES=$(echo "${AVAILABLE_PACKAGES::-1}") - fi - - rm "$AVAILABLE_PACKAGES_TMP" -f - - # Construction des paramètres curl à envoyer - CURL_PARAMS="\"available_packages\":\"$AVAILABLE_PACKAGES\"" - - # Envoi des données : - echo -ne "→ Sending data to ${YELLOW}${REPOSERVER_URL}${RESET}: " - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X PUT -d "{$CURL_PARAMS}" "${REPOSERVER_URL}/api/v2/host/packages/available" 2> /dev/null) - - # Récupération et affichage des messages - curl_result_parse - - if [ "$CURL_ERROR" -eq "0" ];then - UPDATE_REQUEST_STATUS="done" - return 0 - - else - UPDATE_REQUEST_STATUS="error" - return 1 - fi -} - -# Envoi au serveur repomanager l'historique des opérations effectuées sur les paquets (installation, mises à jour, suppression...) -# Se base sur les historiques de yum et d'apt -function genFullHistory -{ - # Contiendra la liste de tous les évènements - EVENTS_JSON="" - IGNORE_EVENT="" - - UPDATE_REQUEST_TYPE="full-history-update" - UPDATE_REQUEST_STATUS="running" - update_request_status - - # Le paramètre SEND_FULL_HISTORY_LIMIT défini le nb maximum d'évènements à envoyer, cela permet - # d'éviter d'envoyer inutilement l'historique complet du serveur dans certains cas. - # Si ce paramètre est laissé vide alors il n'y a aucune limite, on le set par défaut à 99999999. - if [ -z "$SEND_FULL_HISTORY_LIMIT" ];then - SEND_FULL_HISTORY_LIMIT="99999999" - # Dans le cas où on n'a pas précisé de limite alors il faudra traiter les évènements à partir du plus ancien au plus récent - HISTORY_START="oldest" - else - # Dans le cas où on a précisé une limite alors il faudra traiter les évènements à partir du plus récent au plus ancien - HISTORY_START="newest" - fi - # On initialise une variable à 0 qui sera incrémentée jusqu'à atteindre la limite SEND_FULL_HISTORY_LIMIT. - HISTORY_LIMIT_COUNTER="0" - - # Fichier JSON final qui sera envoyé au serveur Repomanager - JSON_FILE="/tmp/.linupdate_${PROCID}_mod_reposerver_events_history.json" - - OLD_IFS=$IFS - - if [ "$OS_FAMILY" == "Redhat" ];then - echo "Building yum events history..." - - checkYumLock - - # Récupération de tous les ID d'évènements dans la base de données de yum - if [ "$HISTORY_START" == "newest" ];then - YUM_HISTORY_IDS=$(yum history list all | tail -n +4 | awk '{print $1}' | grep -v "history") - fi - if [ "$HISTORY_START" == "oldest" ];then - YUM_HISTORY_IDS=$(yum history list all | tail -n +4 | awk '{print $1}' | grep -v "history" | tac) - fi - - # Pour chaque évènement on peut récupérer la date et l'heure de début et la date et l'heure de fin - for YUM_HISTORY_ID in $(echo "$YUM_HISTORY_IDS");do - # On sort de la boucle si on a atteint la limite d'évènement à envoyer fixée par l'utilisateur - if [ "$HISTORY_LIMIT_COUNTER" == "$SEND_FULL_HISTORY_LIMIT" ];then - break - fi - - # Parsage de l'évènement - yumHistoryParser - - if [ ! -z "$YUM_HISTORY_PARSER_RETURN" ];then - # Récupération du retour de la fonction yumHistoryParser - EVENTS_JSON+="$YUM_HISTORY_PARSER_RETURN" - - # Ajout d'une virgule pour séparer chaque évènement - EVENTS_JSON+="," - fi - - (( HISTORY_LIMIT_COUNTER++ )) - done - fi - - # Cas Debian - if [ "$OS_FAMILY" == "Debian" ];then - echo "Building apt events history..." - - if [ "$HISTORY_START" == "newest" ];then - APT_HISTORY_FILES=$(ls -t1 /var/log/apt/history.log*) - fi - if [ "$HISTORY_START" == "oldest" ];then - APT_HISTORY_FILES=$(ls -t1 /var/log/apt/history.log* | tac) - fi - - # On va traiter tous les fichiers d'historique d'apt, même ceux compréssés - for APT_LOG_FILE in $APT_HISTORY_FILES;do - IFS=$'\n' - - # On traite chaque évènement trouvé dans le fichier de log - for START_DATE in $(zgrep "^Start-Date:*" "$APT_LOG_FILE");do - # On sort de la boucle si on a atteint la limite d'évènement à envoyer fixée par l'utilisateur - if [ "$HISTORY_LIMIT_COUNTER" == "$SEND_FULL_HISTORY_LIMIT" ];then - break - fi - - # Parsage de l'évènement - aptHistoryParser - - if [ ! -z "$APT_HISTORY_PARSER_RETURN" ];then - # Récupération du retour de la fonction yumHistoryParser - EVENTS_JSON+="$APT_HISTORY_PARSER_RETURN" - - # Ajout d'une virgule pour séparer chaque évènement - EVENTS_JSON+="," - fi - - (( HISTORY_LIMIT_COUNTER++ )) - done - done - fi - - # Suppression de la dernière virgule après le dernier array de date ajouté (},<= ici) - EVENTS_JSON=$(echo "${EVENTS_JSON::-1}") - - # Construction du JSON final - echo "{\"events\" : [$EVENTS_JSON]}" | jq . > "$JSON_FILE" - - IFS=$OLD_IFS - - # Envoi des données : - echo -ne "→ Sending history to ${YELLOW}${REPOSERVER_URL}${RESET}: " - CURL=$(curl -L --post301 -s -q -H "Authorization: Host $HOST_ID:$TOKEN" -H "Content-Type: application/json" -X PUT -d @${JSON_FILE} "${REPOSERVER_URL}/api/v2/host/packages/event" 2> /dev/null) - - # Récupération et affichage des messages - curl_result_parse - - if [ "$CURL_ERROR" -eq "0" ];then - UPDATE_REQUEST_STATUS="done" - else - UPDATE_REQUEST_STATUS="error" - fi - - update_request_status - - rm "$JSON_FILE" -f - - if [ "$UPDATE_REQUEST_STATUS" == "error" ];then - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/service.py b/service.py new file mode 100755 index 0000000..126a3b3 --- /dev/null +++ b/service.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 -u +# coding: utf-8 + +# Import libraries +import sys + +# Import classes +from src.controllers.App.Service import Service + +# Instantiate Service class +my_service = Service() + +# If an argument is passed, execute the corresponding module agent +if len(sys.argv) > 1: + my_service.run_agent(sys.argv[1]) + sys.exit(0) + +# Else, execute main function +my_service.main() + +sys.exit(0) diff --git a/service/linupdate-agent b/service/linupdate-agent deleted file mode 100755 index ee71c7f..0000000 --- a/service/linupdate-agent +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -trap 'kill 0' EXIT - -AGENTS_ENABLED_DIR="/opt/linupdate/agents-enabled" - -# Laisse un délai de 60sec pour laisser le temps au système de démarrer complètememnt et éviter le crash du service -sleep 60 - -# On vérifie qu'au moins 1 agent est activé -if ls -A $AGENTS_ENABLED_DIR/* 2> /dev/null;then - # Inclusion de tous les 'modules agent' - for AGENT in $(ls -A1 ${AGENTS_ENABLED_DIR}/);do - source "${AGENTS_ENABLED_DIR}/${AGENT}" - done -else - echo "No agent enabled" -fi - -exit 0 \ No newline at end of file diff --git a/service/linupdate.systemd.template b/service/linupdate.systemd.template index 72fccef..909f0a7 100644 --- a/service/linupdate.systemd.template +++ b/service/linupdate.systemd.template @@ -1,9 +1,10 @@ [Unit] -Description=linupdate-agent +Description=linupdate-service +After=network.target [Service] Type=simple -ExecStart=/opt/linupdate/service/linupdate-agent +ExecStart=/opt/linupdate/service.py [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/src/00_check-system b/src/00_check-system deleted file mode 100644 index 83917b5..0000000 --- a/src/00_check-system +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash - -# Détection du système - -function checkSystem -{ - # Le fichier /etc/os-release est présent sur les OS récents et permet de récupérer toutes les infos nécéssaires - if [ -f "/etc/os-release" ];then - if grep -q "^ID_LIKE=" /etc/os-release;then - OS_FAMILY=$(grep "^ID_LIKE=" /etc/os-release | cut -d'=' -f2 | sed 's/"//g') - fi - if grep -q "^ID=" /etc/os-release;then - OS_FAMILY=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | sed 's/"//g') - fi - - if [ -z "$OS_FAMILY" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS family" - exit - fi - - # Si OS_FAMILY contient l'un des termes suivants alors c'est la famille redhat - if echo "$OS_FAMILY" | egrep -q -i 'rhel|centos|fedora|rocky|alma';then - OS_FAMILY="Redhat" - fi - - # Si OS_FAMILY contient l'un des termes suivants alors c'est la famille debian - if echo "$OS_FAMILY" | egrep -q -i 'debian|ubuntu|kubuntu|xubuntu|armbian|mint';then - OS_FAMILY="Debian" - fi - - # Enfin si OS_FAMILY n'est ni égale à Redhat ni à Debian alors on est sur un OS non pris en charge - if ! echo "$OS_FAMILY" | egrep -q -i 'Redhat|Debian';then - echo -e "[${RED} ERROR ${RESET}] Unsupported OS family ($OS_FAMILY)" - exit - fi - - # Toujours à partir de /etc/os-release, on récupère le nom de l'OS et sa version - OS_NAME=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | sed 's/"//g') - if [ -z "$OS_NAME" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS name" - exit - fi - - OS_VERSION=$(grep "^VERSION_ID=" /etc/os-release | cut -d'=' -f2 | sed 's/"//g') - if [ -z "$OS_VERSION" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS release" - exit - fi - fi - - # Cas où /etc/os-release n'existe pas - if [ ! -f "/etc/os-release" ];then - # Si apt est présent, on est sur un os de la famille Debian - if [ -f "/usr/bin/apt" ];then - OS_FAMILY="Debian" - fi - # Si yum ou dnf est présent, on est sur un os de la famille Redhat - if [ -f "/usr/bin/yum" ] || [ -f "/usr/bin/dnf" ];then - OS_FAMILY="Redhat" - fi - - # A ce stade si OS_FAMILY est vide alors on est sur un OS non pris en charge - if [ -z "$OS_FAMILY" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS family, unsupported system" - exit - fi - - # /usr/bin/lsb_release est un programme uniquement présent sur les OS de la famille Debian - if [ -f "/usr/bin/lsb_release" ];then - OS_NAME=$(lsb_release -a 2> /dev/null | grep 'Distributor ID:' | awk '{print $NF}') - OS_VERSION=$(lsb_release -a 2> /dev/null | grep 'Release:' | awk '{print $NF}') - fi - - # Dernier recours pour les OS de la famille Redhat - # Si /etc/centos-release existe, c'est un centos - if [ -f "/etc/centos-release" ];then - OS_NAME="CentOS" - OS_ID="centos" - OS_VERSION=$(rpm --eval '%{centos_ver}') - # Si /etc/fedora-release existe, c'est un fedora - elif [ -f "/etc/fedora-release" ];then - OS_NAME="Fedora" - OS_ID="fedora" - OS_VERSION=$(cat /etc/os-release | grep "^VERSION_ID" | cut -d'=' -f2 | sed 's/"//g') - else - # Dernier recours on vérifie la présence d'un fichier os-release sinon on quitte - if [ ! -f "/etc/os-release" ];then - echo -e "[${RED} ERROR ${RESET}] Cannot determine OS release" - exit - fi - - OS_NAME=$(cat /etc/os-release | grep "^NAME=" | cut -d'=' -f2 | sed 's/"//g') - OS_ID=$(cat /etc/os-release | grep "^ID=" | cut -d'=' -f2 | sed 's/"//g') - OS_VERSION=$(cat /etc/os-release | grep "^VERSION_ID=" | cut -d'=' -f2 | sed 's/"//g') - fi - - # On quitte le script si on n'a rien trouvé à ce stade - if [ -z "$OS_NAME" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS name" - exit - fi - if [ -z "$OS_VERSION" ];then - echo -e "[${RED} ERROR ${RESET}] Unknown OS release" - exit - fi - fi - - if [ "$OS_FAMILY" == "Debian" ];then - PKG_MANAGER="/usr/bin/apt" - PKG_TYPE="deb" - fi - if [ "$OS_FAMILY" == "Redhat" ];then - if [ -f "/usr/bin/yum" ];then - PKG_MANAGER="/usr/bin/yum" - fi - if [ -f "/usr/bin/dnf" ];then - PKG_MANAGER="/usr/bin/dnf" - fi - # Si les deux sont présents (fedora) alors on utilisera yum de préférence - if [ -f "/usr/bin/yum" ] && [ -f "/usr/bin/dnf" ];then - PKG_MANAGER="/usr/bin/yum" - fi - - PKG_TYPE="rpm" - fi -} \ No newline at end of file diff --git a/src/00_exclude b/src/00_exclude deleted file mode 100644 index 4301bcd..0000000 --- a/src/00_exclude +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -function exclude -{ - # Si les paquets à exclure n'ont pas été précisé en paramètre, on les demande - if [ -z "$READ_PACKAGES_TO_EXCLUDE" ];then - echo -ne "Specify packages to exclude (separated by a comma): "; read -p "" READ_PACKAGES_TO_EXCLUDE - fi - - # Suppression des éventuels espaces vides - READ_PACKAGES_TO_EXCLUDE=$(echo "$READ_PACKAGES_TO_EXCLUDE" | sed 's/ //g') - - # Si la valeur renseignée est "none" alors on supprime l'exclusion en place - if [ "$READ_PACKAGES_TO_EXCLUDE" == "none" ];then - READ_PACKAGES_TO_EXCLUDE="" - fi - - # Remplacement de la valeur dans le fichier de conf - # Cas où on a souhaité exclure des versions majeures uniquement - if [ "$READ_PACKAGES_TO_EXCLUDE_MAJOR" == "1" ];then - sed -i "s/^EXCLUDE_MAJOR=.*/EXCLUDE_MAJOR=\"$READ_PACKAGES_TO_EXCLUDE\"/g" "$CONF" - else - sed -i "s/^EXCLUDE=.*/EXCLUDE=\"$READ_PACKAGES_TO_EXCLUDE\"/g" "$CONF" - fi - - # Afficher un message de confirmation -} \ No newline at end of file diff --git a/src/00_generateConf b/src/00_generateConf deleted file mode 100644 index 3096c2e..0000000 --- a/src/00_generateConf +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# Generate config file if not exist - -function generateConf -{ - if [ -f "$CONF" ]; then - return - fi - - echo '[CONFIGURATION]' > "$CONF" - echo 'PROFILE="Bare-metal"' >> "$CONF" - echo 'ENV="prod"' >> "$CONF" - echo 'MAIL_ENABLED="false"' >> "$CONF" - echo 'MAIL_RECIPIENT=""' >> "$CONF" - - echo -e '\n[SOFTWARE CONFIGURATION]' >> "$CONF" - echo 'EXCLUDE_MAJOR=""' >> "$CONF" - echo 'EXCLUDE=""' >> "$CONF" - echo 'SERVICE_RESTART=""' >> "$CONF" -} \ No newline at end of file diff --git a/src/00_get-conf b/src/00_get-conf deleted file mode 100644 index f2a355b..0000000 --- a/src/00_get-conf +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Retrieve configuration from linupdate.conf - -function getConf -{ - # If config file doesn't exist, exit - if [ ! -f "$CONF" ];then - echo -e "[${YELLOW} ERROR ${RESET}] No config file was found on this server. Use --install param to finalize linupdate installation.\n" - (( UPDATE_ERROR++ )) - - # Delete log file - rm "$LOG" -f - clean_exit - fi - - PROFILE=$(egrep "^PROFILE=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Profile name - ENV=$(egrep "^ENV=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Environment - MAIL_ENABLED=$(egrep "^MAIL_ENABLED=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Enable mail notification - MAIL_RECIPIENT=$(egrep "^MAIL_RECIPIENT=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Mail recipient - PACKAGES_EXCLUDE_MAJOR=$(egrep "^EXCLUDE_MAJOR=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Packages to exclude on a major update - PACKAGES_EXCLUDE=$(egrep "^EXCLUDE=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Packages to always exclude - SERVICE_RESTART=$(egrep "^SERVICE_RESTART=" "$CONF" | cut -d'=' -f 2 | sed 's/"//g') # Services to restart after update - - if [ -z "$PROFILE" ];then - echo -e "[${RED} ERROR ${RESET}] No profile is defined\n" - (( UPDATE_ERROR++ )) - clean_exit - fi - - if [ -z "$ENV" ];then - echo -e "[${RED} ERROR ${RESET}] No environment is defined\n" - (( UPDATE_ERROR++ )) - clean_exit - fi -} \ No newline at end of file diff --git a/src/00_get-modules b/src/00_get-modules deleted file mode 100644 index 556cb26..0000000 --- a/src/00_get-modules +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# Récupérer les modules à jour depuis github - -function getModules -{ - TMP_DIR="/tmp/linupdate" - rm "$TMP_DIR" -rf - - # Clonage du repo - cd /tmp && - git clone --quiet https://github.com/lbr38/linupdate.git > /dev/null && - - # Copie de tous les modules dans le répertoire de modules - \cp -r ${TMP_DIR}/mods-available/* ${MODULES_DIR}/ && - rm "$TMP_DIR" -rf -} \ No newline at end of file diff --git a/src/00_help b/src/00_help deleted file mode 100644 index 05b903e..0000000 --- a/src/00_help +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -# Print help - -function help { - echo -e "Available parameters:\n" - echo -e " Main:" - echo -e " --vv|-vv → Enable verbose mode" - echo -e " --version|-v → Print current version" - echo -e " --profile|--type|--print-profile PROFILE → Configure host profile (leave empty to print actual)" - echo -e " --environment|--env ENV → Configure host environment (leave empty to print actual)" - echo -e "" - echo -e " Package update configuration" - echo -e " --exclude-major|-em PACKAGE → Configure packages to exclude on major release update, separated by a comma. Specify 'none' to clean." - echo -e " --exclude|-e PACKAGE → Configure packages to exclude, separated by a comma. Specify 'none' to clean." - echo -e "" - echo -e " Package update execution" - echo -e " --check-updates|-cu → Check packages to be updated and quit" - echo -e " --assume-yes|--force → Enable 'assume yes' (answer 'yes' to every confirm prompt)" - echo -e " --dist-upgrade|-du → Enable 'dist-upgrade' for apt (Debian only)" - echo -e " --keep-oldconf|-ko → Keep actual configuration file when attempting to be overwrited by apt during package update (Debian only)" - echo -e " --ignore-exclude|-ie → Ignore all packages minor or major release update exclusions" - echo -e "" - echo -e " Modules" - echo -e " --list-modules|--list-mod|-m → List available modules" - echo -e " --mod-enable|-mod-enable|-me MODULE → Enable specified module" - echo -e " --mod-disable|-mod-disable|-md MODULE → Disable specified module" - echo -e " --mod-configure|-mc|--mod-exec MODULE → Configure specified module (using module commands, see module help or documentation)" - echo -e " --mod-configure MODULE --help → Print module help" - echo -e "" - echo -e " Agent" - echo -e " --agent-start → Start linupdate agent" - echo -e " --agent-stop → Stop linupdate agent" - echo -e " --agent-restart → Restart linupdate agent" - echo -e " --agent-enable → Enable linupdate agent start on boot" - echo -e " --agent-disable → Disable linupdate agent start on boot" - echo -e "" -} \ No newline at end of file diff --git a/src/00_list-modules b/src/00_list-modules deleted file mode 100644 index 4a22005..0000000 --- a/src/00_list-modules +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Voir la liste des modules - -function listModules -{ - # Récupération de tous les modules à jour depuis github - getModules - - ENABLED_MODULES=() - - # Affiche les modules activés - echo -e "\n Enabled modules:" - if [ "$(ls -A $MODULES_ENABLED_DIR)" ];then - for MODULE in $(ls -A1 "${MODULES_ENABLED_DIR}/"*.mod);do - MODULE=$(basename $MODULE) - MODULE=${MODULE%.mod} - echo -e " $GREEN $MODULE $RESET" - - ENABLED_MODULES+=("$MODULE") - done - else - echo " No module enabled" - fi - - # Affiche les modules disponibles (qui ne sont pas dans ENABLED_MODULES) - echo -e "\n Available modules:" - if [ "$(ls -A $MODULES_DIR)" ];then - for MODULE in $(ls -A1 "${MODULES_DIR}/"*.mod);do - MODULE=$(basename $MODULE) - MODULE=${MODULE%.mod} - if [[ ! ${ENABLED_MODULES[*]} =~ "$MODULE" ]]; then - MODULE=$(basename $MODULE | sed 's/.mod//g') - echo -e " $YELLOW $MODULE $RESET" - fi - done - fi -} \ No newline at end of file diff --git a/src/00_space-left b/src/00_space-left deleted file mode 100644 index da8ffe3..0000000 --- a/src/00_space-left +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# Vérification que l'espace disque restant est suffisant pour effectuer la mise à jour (au moins 1Go d'espace) - -function spaceLeft -{ - echo -ne "\nSpace left: " - - SPACE_LEFT=$(df | egrep "/$" | awk '{print $4}') - if [ "$SPACE_LEFT" -lt 1000000 ];then - echo -ne "${RED}"; df -h | egrep "/$" | awk '{print $4}'; echo -ne "${RESET}" - (( UPDATE_ERROR++ )) - if [ "$MAIL_ENABLED" -eq "true" ];then - sendMail - fi - clean_exit - - else - df -h | egrep "/$" | awk '{print $4}' - fi -} \ No newline at end of file diff --git a/src/01_exec-pre-modules b/src/01_exec-pre-modules deleted file mode 100644 index fc573db..0000000 --- a/src/01_exec-pre-modules +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Exécution de modules complémentaires pre-mise à jour - -function execPreModules -{ - if [ -d "$MODULES_ENABLED_DIR" ];then - if [ "$(ls -A $MODULES_ENABLED_DIR)" ];then - echo -e " Executing pre-update mods:" - - for MODULE in $(ls -A1 ${MODULES_ENABLED_DIR});do - # On récupère le nom exact du module (sans le .mod) - MODULE_FORMATTED=$(echo "${MODULE%.mod}") - - # Si le module fait parti des modules chargés par loadModules alors on peut charger son code - if printf '%s\n' "${LOADED_MODULES[@]}" | grep -q "^${MODULE_FORMATTED}$";then - # On charge le code du module et on exécute sa fonction pre-mise à jour (pre) - source "${MODULES_ENABLED_DIR}/${MODULE}" - pre - fi - done - - echo "" - fi - fi -} \ No newline at end of file diff --git a/src/01_load-modules b/src/01_load-modules deleted file mode 100644 index 17ea24e..0000000 --- a/src/01_load-modules +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -# Chargement des modules complémentaires si existent dans mods-enabled -# Ce chargement a pour but d'afficher à l'utilisateur quels modules sont activés pendant la mise à jour et de faire une première vérification de la conf du module avant de poursuivre - -function loadModules -{ - if [ -d "$MODULES_ENABLED_DIR" ];then - if [ "$(ls -A $MODULES_ENABLED_DIR)" ];then - echo -e " Loading mods:" - #cd "$MODULES_ENABLED_DIR" - for MODULE in $(ls -A1 ${MODULES_ENABLED_DIR}/*.mod);do - # Source de module.mod - source "$MODULE" - # Appel de la fonction mod_load à l'intérieur du fichier précédemment sourcé - mod_load - done - - echo "" - fi - fi -} \ No newline at end of file diff --git a/src/02_check-packages-before-update b/src/02_check-packages-before-update deleted file mode 100644 index 17370ae..0000000 --- a/src/02_check-packages-before-update +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/env bash - -# This function is executed just before packages update. -# It checks if packages to exclude have been specified in the configuration file and, if so, then exclude them if needed. - -function checkPackagesBeforeUpdate -{ - if [ "$OS_FAMILY" == "Redhat" ];then - # Clean yum cache - yum clean all -q - fi - if [ "$OS_FAMILY" == "Debian" ];then - # Clean and reload apt cache - apt-get clean && apt-get -qq --allow-releaseinfo-change update - fi - - # Temporary file to work with - CHECK_UPDATE_TMP="/tmp/linupdate_check-update.tmp" - - # List packages available for update - if [ "$OS_FAMILY" == "Redhat" ];then - if [ -f "/usr/bin/dnf" ];then - repoquery -q -a --qf="%{name} %{version}-%{release}.%{arch} %{repoid}" --upgrades >> "$CHECK_UPDATE_TMP" - else - repoquery -q -a --qf="%{name} %{version}-%{release}.%{arch} %{repoid}" --pkgnarrow=updates > "$CHECK_UPDATE_TMP" - fi - fi - if [ "$OS_FAMILY" == "Debian" ];then - aptitude -F"%p %V %v" --disable-columns search ~U > "$CHECK_UPDATE_TMP" - fi - - # Add the available packages list in an array, it could be needed for others operations - OLD_IFS=$IFS - PACKAGES=() - - while IFS=$'\n' read -r LINE; do - PACKAGES+=("$LINE") - done < <(awk '{print $1}' "$CHECK_UPDATE_TMP") - - IFS=$OLD_IFS - - # If --ignore-exclude param has been set then ignore all packages exclusions that have been specified by the user - if [ "$IGNORE_EXCLUDE" -eq "1" ];then - echo -e "${YELLOW}--ignore-exclude${RESET} param is enabled, no exclusion will be taken into account." - - if [ "$OS_FAMILY" == "Debian" ];then - # Get all packages that could have been holded (on another linupdate execution) - HOLDED_PACKAGES=$(apt-mark showhold) - - # Then unhold them all - if [ ! -z "$HOLDED_PACKAGES" ];then - OLD_IFS=$IFS - IFS=$'\n' - - for HOLDED_PACKAGE in $(echo "$HOLDED_PACKAGES");do - apt-mark unhold "$HOLDED_PACKAGE" - done - - IFS="$OLD_IFS" - fi - fi - fi - - # If --ignore-exclude param has NOT been set, then check if some packages need to be excluded from packages update - if [ "$IGNORE_EXCLUDE" -eq "0" ];then - # Gestion des exclusions de paquets - # - # Exemple pour illustrer le fonctionnement : - # - # Extrait de linupdate.conf : Extrait de /tmp/linupdate_check-update.tmp (liste des paquets disponibles pour mise à jour, généré avec repoquery) - # EXCLUDE_MAJOR="httpd.*,php.*" php-cli.x86_64 7.1.10.xxx updates - # EXCLUDE="kernel.*," php-common.x86_64 7.1.10.xxx updates - # ... ... - # - # Lorsqu'on utilise un wildcard (par exemple php.*), le - # script va vérifier le nb d'occurences dans le fichier - # /tmp/linupdate_check-update.tmp (dans cet exemple 2 occurences - # php-cli et php-common). Le script devra vérifier que - # pour ces deux paquets à mettre à jour si il s'agit d'une - # maj majeure ou non. Si php.* serait renseigné dans - # EXCLUDE= alors le script excluerai les deux occurences - #  trouvées quoi qu'il arrive. - - # Process packages to exclude on major release update (EXCLUDE_MAJOR): - if [ ! -z "$PACKAGES_EXCLUDE_MAJOR" ];then - - PACKAGES_EXCLUDE_MAJOR_ARRAY="" - IFS=',' - - # Inject exclusion list into PACKAGES_EXCLUDE_MAJOR_ARRAY - read -ra PACKAGES_EXCLUDE_MAJOR_ARRAY <<< "$PACKAGES_EXCLUDE_MAJOR" - - # Run through all packages in PACKAGES_EXCLUDE_MAJOR_ARRAY - # For each package name, check if an update is available by looking in $CHECK_UPDATE_TMP - for PACKAGE in "${PACKAGES_EXCLUDE_MAJOR_ARRAY[@]}"; do - - # If package occurence is found in $CHECK_UPDATE_TMP, then it means that an update is available for this package - # We will have to check if it is a major release update or a minor release update - if egrep -q "^${PACKAGE} " $CHECK_UPDATE_TMP;then - - # Save the actual IFS (comma) because it will be needed again by the previous FOR loop - OLD_IFS=$IFS - IFS=$'\n' - - # For every occurence of the package found in $CHECK_UPDATE_TMP (there could be more than one), - # check if its available upsate is a major release or a minor release update - for OCCURENCE in $(egrep "^${PACKAGE} " $CHECK_UPDATE_TMP | awk '{print $1}');do - - echo -e "${YELLOW} ⚠ ${OCCURENCE}:$RESET" - - # Get package exact name - PKG_NAME=$(egrep "^${OCCURENCE} " "$CHECK_UPDATE_TMP" | awk '{print $1}') - - # And its available release version - PKG_VER=$(egrep "^${OCCURENCE} " "$CHECK_UPDATE_TMP" | awk '{print $2}') - - # Get the package actual release version that is installed on the system - - # If Debian - if [ "$OS_FAMILY" == "Debian" ];then - PKG_ACTUAL_VERSION=$(egrep "^${OCCURENCE} " "$CHECK_UPDATE_TMP" | awk '{print $3}') - fi - # If RHEL - if [ "$OS_FAMILY" == "Redhat" ];then - PKG_ACTUAL_VERSION=$(rpm -qi $OCCURENCE | grep Version | head -n1 | awk '{print $3}') - PKG_RELEASE=$(rpm -qi $OCCURENCE | grep Release | head -n1 | awk '{print $3}') - # Concatenate both - PKG_ACTUAL_VERSION="${PKG_ACTUAL_VERSION}-${PKG_RELEASE}" - fi - - # If one of the variables is empty then print an error because we have not all the necessary informations to check if it's a major or minor update - # In doubt, the package will be excluded to avoid any problem - if [ -z "$PKG_NAME" ] || [ -z "$PKG_VER" ] || [ -z "$PKG_ACTUAL_VERSION" ];then - echo -e "[$RED ERROR $RESET] while checking ${YELLOW}${OCCURENCE}${RESET} package. It will be excluded from update." - - # Add the package name to the final exclude list - UPDATE_EXCLUDE+=" $OCCURENCE" - fi - - # Parsing and comparing retrieved release version - # ex : 9.2.24-1.el7_5 - # |_______ - # | - # If first number does not change, then it is not a major update but a minor update that should not be problematic. - # Else (if the first number is different), it is a major update - PARSE_PKG_VER="$(echo "$PKG_VER" | awk -F. '{print $1}')" - PARSE_PKG_ACTUAL_VERSION="$(echo "$PKG_ACTUAL_VERSION" | awk -F. '{print $1}')" - - # Cas it is a major update - if [ "$PARSE_PKG_VER" != "$PARSE_PKG_ACTUAL_VERSION" ];then - echo -e " [$YELLOW WARNING $RESET] A major release version is available for this package" - echo -e " → Current version : ${YELLOW}${PKG_ACTUAL_VERSION}${RESET}" - echo -e " → Available version : ${YELLOW}${PKG_VER}${RESET}" - - # Add the package name to the final exclude list - UPDATE_EXCLUDE+=" $OCCURENCE" - - # Case it is a minor update: just print a message - else - echo -e " An update is available for this package but no major release version (Current version: $PKG_ACTUAL_VERSION / Avail. version: $PKG_VER)." - fi - done - - # Set back the prviously saved IFS (comma) for the previous FOR loop to work - IFS=$OLD_IFS - - # Cas no update is available for this package - else - echo -e "${YELLOW} ⚠ ${PACKAGE} :$RESET" && - echo -e " No available update for this package." - fi - done - fi - - # Process packages to exclude no matter the release update, they always have to be excluded. - if [ ! -z "$PACKAGES_EXCLUDE" ];then - - PACKAGES_EXCLUDE_ARRAY="" - IFS=',' - - # Inject exclusion list into PACKAGES_EXCLUDE_ARRAY - read -ra PACKAGES_EXCLUDE_ARRAY <<< "$PACKAGES_EXCLUDE" - - # Run through all packages in PACKAGES_EXCLUDE - # For each package name, check if an update is available by looking in $CHECK_UPDATE_TMP - for PACKAGE in "${PACKAGES_EXCLUDE_ARRAY[@]}";do - - # If package occurence is found in $CHECK_UPDATE_TMP, then it means that an update is available for this package - # It will be excluded - if egrep -q "^${PACKAGE} " $CHECK_UPDATE_TMP;then - - # Save the actual IFS (comma) because it will be needed again by the previous FOR loop - OLD_IFS=$IFS - IFS=$'\n' - - # For every occurence of the package found in $CHECK_UPDATE_TMP (there could be more than one), - # add it to the final exclude list - for OCCURENCE in $(egrep "^${PACKAGE} " $CHECK_UPDATE_TMP | awk '{print $1}');do - UPDATE_EXCLUDE+=" $OCCURENCE" - done - - # Set back the prviously saved IFS (comma) for the previous FOR loop to work - IFS=$OLD_IFS - fi - done - fi - fi - - # Finalize the list by adding a white space in the end to be sure the last package in the list will be taken into account when processing exclusion from this list - if [ ! -z "$UPDATE_EXCLUDE" ];then - UPDATE_EXCLUDE="${UPDATE_EXCLUDE} " - # Delete leading white space - UPDATE_EXCLUDE=$(echo "$UPDATE_EXCLUDE" | sed -e 's/^[[:space:]]*//') - fi - - # Process services that will need a restart after packages update - if [ ! -z "$SERVICE_RESTART" ];then - OLD_IFS=$IFS - IFS=',' - - # Inject services list into SERVICES_TO_RESTART - read -ra SERVICES_TO_RESTART <<< "$SERVICE_RESTART" - - # Run through SERVICES_TO_RESTART - for SERVICE in "${SERVICES_TO_RESTART[@]}"; do - # If service restart is conditionned by a specific package update, then get the package name, e.g: - # httpd:ca-certificates => httpd service will be restarted if ca-certificates package is updated - if echo "$SERVICE" | grep -q ":"; then - SERVICE_CONDITIONNAL_PACKAGE_NAME=$(echo "$SERVICE" | awk -F: '{print $2}') - SERVICE=$(echo "$SERVICE" | awk -F: '{print $1}') - - # If conditionnal package is empty, ignore this service and continue - if [ -z "$SERVICE_CONDITIONNAL_PACKAGE_NAME" ];then - continue - fi - - # Check if the package is in the list of packages that will be updated, if not then ignore this service and continue - if ! printf '%s\n' "${PACKAGES[@]}" | grep -q "^${SERVICE_CONDITIONNAL_PACKAGE_NAME}$";then - continue - fi - fi - - # Check if specified service really exists, if yes, then add it to the final services to restart list - if systemctl list-units --all -t service --full | grep -q "${SERVICE}.service";then - # Also check if the service is active - if systemctl is-active --quiet "$SERVICE";then - SERVICE_TO_BE_RESTARTED+=" $SERVICE" - fi - fi - done - - SERVICE_TO_BE_RESTARTED="${SERVICE_TO_BE_RESTARTED} " - SERVICE_TO_BE_RESTARTED="${SERVICE_TO_BE_RESTARTED/ /}" - - IFS=$OLD_IFS - fi - - echo "" - - # Print all packages that will be excluded, if any - if [ ! -z "$UPDATE_EXCLUDE" ];then - echo -e "Following packages will be excluded from update: ${YELLOW}${UPDATE_EXCLUDE}${RESET}" - fi - - # Print all services that will be restarted, if any - if [ ! -z "$SERVICE_TO_BE_RESTARTED" ];then - echo -e "Following services will be restarted after update: ${YELLOW}${SERVICE_TO_BE_RESTARTED}${RESET}" - fi - - echo -e "\n$SEP\n" - - # If $CHECK_UPDATE_TMP is empty then there is no update available - if [ ! -s "$CHECK_UPDATE_TMP" ];then - echo -e "${YELLOW}No available package for update${RESET}\n" - - # Indicate that there is no need to execute apt/yum packages update - SOMETHING_TO_UPDATE="false" - - return - fi - - # Print packages that will be updated in columns - if [ "$OS_FAMILY" == "Debian" ];then - COLUMS_SIZE="%-40s %-20s %-45s %-45s\n" - printf "$COLUMS_SIZE" " Package" "" " Current version" " Available version" - fi - if [ "$OS_FAMILY" == "Redhat" ];then - COLUMS_SIZE="%-40s %-20s %-45s %-45s %-30s\n" - printf "$COLUMS_SIZE" " Package" "" " Current version" " Available version" " Repo" - fi - - while read PACKAGE;do - # Package name - PKG_NAME=$(echo "$PACKAGE" | awk '{print $1}') - - # Package version update that is available and will be installed - if [ "$OS_FAMILY" == "Redhat" ];then - PKG_VER=$(echo "$PACKAGE" | awk '{print $2}') - fi - if [ "$OS_FAMILY" == "Debian" ];then - PKG_VER=$(echo $PACKAGE | awk '{print $2}' | sed 's/(//g') - fi - - # Package actual version that is installed on the system - if [ "$OS_FAMILY" == "Redhat" ];then - PKG_ACTUAL_VERSION=$(rpm -qi $PKG_NAME | grep Version | head -n1 | awk '{print $3}') - fi - if [ "$OS_FAMILY" == "Debian" ];then - PKG_ACTUAL_VERSION=$(echo $PACKAGE | awk '{print $3}') - fi - - # On RHEL, also get the actual package release and the repo from where the package will be updated - if [ "$OS_FAMILY" == "Redhat" ];then - PKG_ACTUAL_RELEASE=$(rpm -qi $PKG_NAME | grep Release | head -n1 | awk '{print $3}') - REPO=$(echo "$PACKAGE" | awk '{print $3}') - fi - - # Now print the line with all the package informations - # Columns size is different depending on whether the system is Debian or RHEL - - # Cas the package to print will be excluded, a "excluded" tag will be printed - if echo "$UPDATE_EXCLUDE" | grep -q "$PKG_NAME ";then - EXCLUDE_STATE="(excluded)" - - if [ "$OS_FAMILY" == "Redhat" ];then - echo -ne "${RED} ✕ ${RESET}" - printf "$COLUMS_SIZE" "$PKG_NAME" "$EXCLUDE_STATE" "${PKG_ACTUAL_VERSION}-${PKG_ACTUAL_RELEASE}" "$PKG_VER" "$REPO" - echo -n "" - fi - if [ "$OS_FAMILY" == "Debian" ];then - echo -ne "${RED} ✕ ${RESET}" - printf "$COLUMS_SIZE" "$PKG_NAME" "$EXCLUDE_STATE" "$PKG_ACTUAL_VERSION" "$PKG_VER" - echo -n "" - fi - else - EXCLUDE_STATE="" - if [ "$OS_FAMILY" == "Redhat" ];then - echo -ne "${GREEN} ✔ ${RESET}" - printf "$COLUMS_SIZE" "$PKG_NAME" "$EXCLUDE_STATE" "${PKG_ACTUAL_VERSION}-${PKG_ACTUAL_RELEASE}" "$PKG_VER" "$REPO" - echo -n "" - fi - if [ "$OS_FAMILY" == "Debian" ];then - echo -ne "${GREEN} ✔ ${RESET}" - printf "$COLUMS_SIZE" "$PKG_NAME" "$EXCLUDE_STATE" "$PKG_ACTUAL_VERSION" "$PKG_VER" - echo -n "" - fi - fi - - done < "$CHECK_UPDATE_TMP" - - echo -e "\n${SEP}\n" - - # If --check-updates param, then quit - if [ "$ONLY_CHECK_UPDATE" == "true" ];then - exit - fi - - # If --assume-yes param has not been specified, then ask for confirmation before installing the printed packages update list - if [ "$ASSUME_YES" == "0" ];then - echo -ne "${YELLOW}→ Confirm installation (yes/no): $RESET"; read -p "" CONFIRM && - if [ "$CONFIRM" != "yes" ];then - clean_exit - fi - fi -} \ No newline at end of file diff --git a/src/03_check-yum-lock b/src/03_check-yum-lock deleted file mode 100644 index 42dd987..0000000 --- a/src/03_check-yum-lock +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -# Vérifie la présence d'un lock yum et attend pour sa libération si c'est le cas - -function checkYumLock -{ - if [ -f "/var/run/yum.pid" ];then - echo -e "Waiting for yum ${YELLOW}lock${RESET}..." - fi - - while [ -f "/var/run/yum.pid" ];do - sleep 2 - done -} \ No newline at end of file diff --git a/src/04_update b/src/04_update deleted file mode 100644 index 0972393..0000000 --- a/src/04_update +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash - -function update -{ - # Si la vérification des paquets n'a trouvé aucun paquet à mettre à jour alors il est inutile d'exécuter les mises à jour - # On sort de la fonction - if [ "$SOMETHING_TO_UPDATE" == "false" ];then - return - fi - - # Fonction lançant les mises à jour yum - - echo -e "\nUpdate is running..." - - echo -ne "Cleaning cache: " - if [ "$OS_FAMILY" == "Redhat" ];then - checkYumLock - yum clean all -q - fi - if [ "$OS_FAMILY" == "Debian" ];then - apt-get clean - fi - - echo -e "[$GREEN OK $RESET]" - - # Si c'est du Debian, on charge la liste des paquets - if [ "$OS_FAMILY" == "Debian" ];then - apt-get -o Acquire::Check-Valid-Until=false -qq --allow-releaseinfo-change update - fi - - # Si $UPDATE_EXCLUDE n'est pas vide, alors on exclu les paquets de la mise à jour - if [ ! -z "$UPDATE_EXCLUDE" ];then - if [ "$OS_FAMILY" == "Redhat" ];then - YUM_OPTIONS+=" --exclude=${UPDATE_EXCLUDE}" - # Trim whitespaces - YUM_OPTIONS="${YUM_OPTIONS#"${YUM_OPTIONS%%[![:space:]]*}"}" - YUM_OPTIONS="${YUM_OPTIONS%"${YUM_OPTIONS##*[![:space:]]}"}" - fi - - if [ "$OS_FAMILY" == "Debian" ];then - echo -e "\nExcluding critical packages: $YELLOW" - IFS=' ' - - for PACKAGE in ${UPDATE_EXCLUDE[@]};do # Attention ne pas placer de double quote sur cette variable - apt-mark hold "$PACKAGE" - if [ "$?" -eq "0" ];then - echo -e "Error while excluding $PACKAGE" - (( UPDATE_ERROR++ )) - return - fi - done - - echo -e "$RESET" - fi - fi - - # Updating packages - if [ "$OS_FAMILY" == "Redhat" ];then - if [ ! -z "$YUM_OPTIONS" ];then - yum "$YUM_OPTIONS" update -y - else - yum update -y - fi - if [ "$?" -ne "0" ];then - (( UPDATE_ERROR++ )) - fi - fi - - if [ "$OS_FAMILY" == "Debian" ];then - if [ ! -z "$APT_OPTIONS" ];then - apt-get "$APT_UPGRADE" -y "$APT_OPTIONS" - else - apt-get "$APT_UPGRADE" -y - fi - if [ "$?" -ne "0" ];then - (( UPDATE_ERROR++ )) - fi - fi - - if [ "$UPDATE_ERROR" -gt "0" ];then - echo -e "\n${RED}Update has failed${RESET}" - return - fi - - echo -e "\n${GREEN}Update completed${RESET}\n" -} \ No newline at end of file diff --git a/src/09_exec-post-modules b/src/09_exec-post-modules deleted file mode 100644 index b715211..0000000 --- a/src/09_exec-post-modules +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -# Exécution de modules complémentaires post-mise à jour - -function execPostModules -{ - if [ -d "$MODULES_ENABLED_DIR" ];then - if [ "$(ls -A $MODULES_ENABLED_DIR)" ];then - echo -e " Executing post-update mods:" - - for MODULE in $(ls -A1 ${MODULES_ENABLED_DIR});do - # On récupère le nom exact du module (sans le .mod) - MODULE_FORMATTED=$(echo "${MODULE%.mod}") - - # Si le module fait parti des modules chargés par loadModules alors on peut charger son code - if printf '%s\n' "${LOADED_MODULES[@]}" | grep -q "^${MODULE_FORMATTED}$";then - # On charge le code du module et on exécute sa fonction pre-mise à jour (pre) - source "${MODULES_ENABLED_DIR}/${MODULE}" - post - fi - done - fi - fi -} \ No newline at end of file diff --git a/src/09_service-restart b/src/09_service-restart deleted file mode 100644 index dfd1e51..0000000 --- a/src/09_service-restart +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# Restart services after update - -function restartService -{ - # If $SERVICE_TO_BE_RESTARTED contains services that need to be restarted - if [ -z "$SERVICE_TO_BE_RESTARTED" ];then - return - fi - - OLD_IFS=$IFS - IFS=' ' - - for SERVICE in $(echo "$SERVICE_TO_BE_RESTARTED"); do - # Clean eventual spaces - SERVICE=$(echo "$SERVICE" | sed 's/ //g') - - echo -ne "→ Restarting ${YELLOW}${SERVICE}${RESET} service: " - - systemctl restart "$SERVICE" --quiet - - if [ "$?" != "0" ];then - echo -e "[$YELLOW ERROR $RESET] while restarting" - else - echo -e "[$GREEN OK $RESET]" - fi - done - - IFS=$OLD_IFS -} \ No newline at end of file diff --git a/src/10_check-reboot-needed b/src/10_check-reboot-needed deleted file mode 100644 index 5821b01..0000000 --- a/src/10_check-reboot-needed +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# Check if a system reboot is required - -function checkRebootNeeded -{ - if [ "$OS_FAMILY" == "Redhat" ] && [ -f "/usr/bin/needs-restarting" ];then - # If following command does not return 0 then reboot is required - if ! /usr/bin/needs-restarting -r > /dev/null;then - REBOOT_REQUIRED="true" - fi - fi - - if [ "$OS_FAMILY" == "Debian" ];then - # If following file exists then reboot is required - if [ -f "/var/run/reboot-required" ];then - REBOOT_REQUIRED="true" - fi - fi -} \ No newline at end of file diff --git a/src/10_send-mail b/src/10_send-mail deleted file mode 100644 index 4e01a9b..0000000 --- a/src/10_send-mail +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -function sendMail -{ - if [ "$MAIL_ENABLED" == "true" ];then - # Remove ANSI color codes before sending mail: - sed 's,\x1B[[(][0-9;]*[a-zA-Z],,g' "$LOG" > "$LOG_REPORT_MAIL" - - echo -ne "→ Sending update mail report: " - - if [ "$MOD_ERROR" -gt "0" ];then - echo "Error while executing module" | mutt -s "[ ERROR ] Update has failed - $HOSTNAME - $DATE_DMY at $TIME - linupdate" -a "$LOG_REPORT_MAIL" -- $MAIL_RECIPIENT - fi - - if [ ! -z "$UPDATE_EXCLUDE" ];then - echo "Critical packages have been detected. They have been excluded from update. Check attached update report." | mutt -s "[ WARNING ] Update completed but critical packages update have been ignored - $HOSTNAME - $DATE_DMY at $TIME - linupdate" -a "$LOG_REPORT_MAIL" -- $MAIL_RECIPIENT - elif [ ! -z "$UPDATE_EXCLUDE" ] && [ "$UPDATE_ERROR" -gt "0" ];then - echo "Critical packages have been detected. They have been excluded from failed update. Check attached update report." | mutt -s "[ ERROR & WARNING ] Update failed and critical packages excluded - $HOSTNAME - $DATE_DMY at $TIME - linupdate" -a "$LOG_REPORT_MAIL" -- $MAIL_RECIPIENT - elif [ "$UPDATE_ERROR" -gt "0" ];then - # If there was error during update - echo "Update report is attached" | mutt -s "[ ERROR ] Update failed - $HOSTNAME - $DATE_DMY at $TIME - linupdate" -a "$LOG_REPORT_MAIL" -- $MAIL_RECIPIENT - else - # If there was no error during update - echo "Update report is attached" | mutt -s "[ OK ] Update completed - $HOSTNAME - $DATE_DMY at $TIME - linupdate" -a "$LOG_REPORT_MAIL" -- $MAIL_RECIPIENT - fi - - echo -e "[$GREEN OK $RESET]" - fi -} \ No newline at end of file diff --git a/src/200_apt-history-parser b/src/200_apt-history-parser deleted file mode 100644 index e3c5065..0000000 --- a/src/200_apt-history-parser +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env bash - -function apt_multiple_event_parser -{ - # Si le fichier temporaire est vide alors on ne traite pas - if [ ! -s "$MULTIPLE_EVENTS_TMP" ];then - return - fi - - # Sur les multiples dates et heures affichées dans le fichier, on n'en garde qu'une seule - DATE_START=$(grep "^Start-Date:" "$MULTIPLE_EVENTS_TMP" | awk '{print $2}' | head -n1) - TIME_START=$(grep "^Start-Date:" "$MULTIPLE_EVENTS_TMP" | awk '{print $3}' | head -n1) - DATE_END=$(grep "^End-Date:" "$MULTIPLE_EVENTS_TMP" | awk '{print $2}' | tail -n1) - TIME_END=$(grep "^End-Date:" "$MULTIPLE_EVENTS_TMP" | awk '{print $3}' | tail -n1) - - # On traite tous les Install qu'il y a eu à ces évènements communs - if grep -q "^Install:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Install:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_INSTALLED_LIST+=$(echo "$OCCURENCE" | sed 's/Install: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_INSTALLED_LIST=$(echo "${PACKAGES_INSTALLED_LIST::-2}") - fi - - # On traite tous les Upgrade qu'il y a eu à ces évènements communs - if grep -q "^Upgrade:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Upgrade:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_UPGRADED_LIST+=$(echo "$OCCURENCE" | sed 's/Upgrade: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_UPGRADED_LIST=$(echo "${PACKAGES_UPGRADED_LIST::-2}") - fi - - # On traite tous les Remove qu'il y a eu à ces évènements communs - if grep -q "^Remove:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Remove:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_REMOVED_LIST+=$(echo "$OCCURENCE" | sed 's/Remove: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_REMOVED_LIST=$(echo "${PACKAGES_REMOVED_LIST::-2}") - fi - - # On traite tous les Purge qu'il y a eu à ces évènements communs - if grep -q "^Purge:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Purge:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_PURGED_LIST+=$(echo "$OCCURENCE" | sed 's/Purge: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_PURGED_LIST=$(echo "${PACKAGES_PURGED_LIST::-2}") - fi - - # On traite tous les Downgrade qu'il y a eu à ces évènements communs - if grep -q "^Downgrade:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Downgrade:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_DOWNGRADED_LIST+=$(echo "$OCCURENCE" | sed 's/Downgrade: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_DOWNGRADED_LIST=$(echo "${PACKAGES_DOWNGRADED_LIST::-2}") - fi - - # On traite tous les Reinstall qu'il y a eu à ces évènements communs - if grep -q "^Reinstall:" "$MULTIPLE_EVENTS_TMP";then - for OCCURENCE in $(grep "^Reinstall:" "$MULTIPLE_EVENTS_TMP");do - PACKAGES_REINSTALLED_LIST+=$(echo "$OCCURENCE" | sed 's/Reinstall: //g')", " - done - # Suppression de la dernière virgule : - PACKAGES_REINSTALLED_LIST=$(echo "${PACKAGES_REINSTALLED_LIST::-2}") - fi -} - -function aptHistoryParser -{ - OLD_IFS=$IFS - IFS=$'\n' - - # Il faut fournir l'Id de l'évènement (sa date de début) à parser - if [ -z "$START_DATE" ];then - echo -e "[$YELLOW ERROR $RESET] No apt event Id has been specified"; - return - fi - - # On ignore cet évènement si celui-ci a le même Id (même date) que le précédent - if [ ! -z "$IGNORE_EVENT" ] && [ "$IGNORE_EVENT" == "$START_DATE" ];then - continue - fi - - PACKAGES_INSTALLED_JSON="" - PACKAGES_UPGRADED_JSON="" - PACKAGES_REMOVED_JSON="" - PACKAGES_PURGED_JSON="" - PACKAGES_DOWNGRADED_JSON="" - PACKAGES_REINSTALLED_JSON="" - APT_HISTORY_PARSER_RETURN="" - - # Avant de commencer à parser, on vérifie qu'il n'y a pas eu plusieurs évènements exactement à la même date et à la même heure - COUNT_EVENT=$(zgrep "$START_DATE" "$APT_LOG_FILE" | wc -l) - - # Si il y a plusieurs évènements à la même date, on récupère leur contenu complet dans un fichier temporaire - if [ "$COUNT_EVENT" -gt "1" ];then - # echo "Plusieurs évènements pour : $START_DATE" - # continue - - MULTIPLE_EVENTS_TMP="/tmp/.linupdate_${PROCID}_mod_reposerver_multiple-events-history.tmp" - - # Si le fichier de log est compréssé, on doit utiliser zcat pour le lire - if echo "$APT_LOG_FILE" | egrep -q ".gz";then - zcat "$APT_LOG_FILE" | sed -n "/$START_DATE/,/^$/p" > "$MULTIPLE_EVENTS_TMP" - - # Si le fichier n'est pas compressé on peut utiliser sed directement - else - sed -n "/$START_DATE/,/^$/p" "$APT_LOG_FILE" > "$MULTIPLE_EVENTS_TMP" - fi - - # On traite tous les évènements à la même date avec la fonction suivante - apt_multiple_event_parser - - # Enfin comme on a traité plusieurs mêmes évènements du fichier de log, on ignore tous les prochaines évènements qui seraient à la même date (pour ne pas qu'ils soient traités deux fois) - IGNORE_EVENT="$START_DATE" - - else - # On récupère tout le bloc de l'évènement en cours : à partir de la date de début (START_DATE) et jusqu'à rencontrer un saut de ligne - # Si le fichier est compréssé, on doit utiliser zcat pour le lire - if echo "$APT_LOG_FILE" | egrep -q ".gz";then - EVENT=$(zcat "$APT_LOG_FILE" | sed -n "/$START_DATE/,/^$/p") - # Si le fichier n'est pas compréssé on peut utiliser sed directement - else - EVENT=$(sed -n "/$START_DATE/,/^$/p" "$APT_LOG_FILE") - fi - - # A partir du bloc de l'évènement récupéré, on peut récupérer la date et l'heure de début et la date et l'heure de fin - DATE_START=$(echo "$EVENT" | grep "^Start-Date:" | awk '{print $2}') - TIME_START=$(echo "$EVENT" | grep "^Start-Date:" | awk '{print $3}') - DATE_END=$(echo "$EVENT" | grep "^End-Date:" | awk '{print $2}') - TIME_END=$(echo "$EVENT" | grep "^End-Date:" | awk '{print $3}') - - # Commande exécutée - COMMAND=$(echo "$EVENT" | grep "^Commandline:" | sed 's/Commandline: //g') - - # On peut également récupérer la liste des paquets installés, mis à jour jour, supprimés... - PACKAGES_INSTALLED_LIST=$(echo "$EVENT" | grep "^Install:" | sed 's/Install: //g') - PACKAGES_UPGRADED_LIST=$(echo "$EVENT" | grep "^Upgrade:" | sed 's/Upgrade: //g') - PACKAGES_REMOVED_LIST=$(echo "$EVENT" | grep "^Remove:" | sed 's/Remove: //g') - PACKAGES_PURGED_LIST="$(echo "$EVENT" | grep "^Purge:" | sed 's/Purge: //g')" - PACKAGES_DOWNGRADED_LIST=$(echo "$EVENT" | grep "^Downgrade:" | sed 's/Downgrade: //g') - PACKAGES_REINSTALLED_LIST="$(echo "$EVENT" | grep "^Reinstall:" | sed 's/Reinstall: //g')" - fi - - # Traitement de la liste des paquets installés à cette date et heure - if [ ! -z "$PACKAGES_INSTALLED_LIST" ];then - for LINE in $(echo "$PACKAGES_INSTALLED_LIST");do - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_INSTALLED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_INSTALLED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - # Traitement de la liste des paquets mis à jour à cette date et heure - if [ ! -z "$PACKAGES_UPGRADED_LIST" ];then - for LINE in $(echo "$PACKAGES_UPGRADED_LIST");do - - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $3}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_UPGRADED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $3}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_UPGRADED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - # Traitement de la liste des paquets supprimés à cette date et heure - if [ ! -z "$PACKAGES_REMOVED_LIST" ];then - for LINE in $(echo "$PACKAGES_REMOVED_LIST");do - - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_REMOVED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_REMOVED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - # Traitement de la liste des paquets purgés à cette date et heure - if [ ! -z "$PACKAGES_PURGED_LIST" ];then - for LINE in $(echo "$PACKAGES_PURGED_LIST");do - - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_PURGED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_PURGED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - # Traitement de la liste des paquets rétrogradés à cette date et heure - if [ ! -z "$PACKAGES_DOWNGRADED_LIST" ];then - for LINE in $(echo "$PACKAGES_DOWNGRADED_LIST");do - - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $3}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_DOWNGRADED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $3}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_DOWNGRADED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - # Traitement de la liste des paquets réinstallés à cette date et heure - if [ ! -z "$PACKAGES_REINSTALLED_LIST" ];then - for LINE in $(echo "$PACKAGES_REINSTALLED_LIST");do - - # Si plusieurs paquets sur la même ligne - if echo "$LINE" | grep -q "), ";then - LINE=$(echo "$LINE" | sed 's/), /\n/g') - for FORMATTED_LINE in $(echo "$LINE");do - PACKAGE_NAME=$(echo "$FORMATTED_LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$FORMATTED_LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_REINSTALLED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - done - else - PACKAGE_NAME=$(echo "$LINE" | awk '{print $1}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g' | sed 's/:amd64//g' | sed 's/:i386//g' | sed 's/:armhf//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed 's/,//g' | sed 's/(//g' | sed 's/)//g' | sed 's/ //g') - PACKAGES_REINSTALLED_JSON+="{\"name\":\"${PACKAGE_NAME}\",\"version\":\"${PACKAGE_VERSION}\"}," - fi - done - fi - - if [ ! -z "$PACKAGES_INSTALLED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_INSTALLED_JSON=$(echo "${PACKAGES_INSTALLED_JSON::-1}") - # Création de l'array contenant les paquets installés, au format JSON - PACKAGES_INSTALLED_JSON="\"installed\":[$PACKAGES_INSTALLED_JSON]," - fi - - if [ ! -z "$PACKAGES_UPGRADED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_UPGRADED_JSON=$(echo "${PACKAGES_UPGRADED_JSON::-1}") - # Création de l'array contenant les paquets mis à jour, au format JSON - PACKAGES_UPGRADED_JSON="\"upgraded\":[$PACKAGES_UPGRADED_JSON]," - fi - - if [ ! -z "$PACKAGES_REMOVED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_REMOVED_JSON=$(echo "${PACKAGES_REMOVED_JSON::-1}") - # Création de l'array contenant les paquets supprimés, au format JSON - PACKAGES_REMOVED_JSON="\"removed\":[$PACKAGES_REMOVED_JSON]," - fi - - if [ ! -z "$PACKAGES_PURGED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_PURGED_JSON=$(echo "${PACKAGES_PURGED_JSON::-1}") - # Création de l'array contenant les paquets purgés, au format JSON - PACKAGES_PURGED_JSON="\"purged\":[$PACKAGES_PURGED_JSON]," - fi - - if [ ! -z "$PACKAGES_DOWNGRADED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_DOWNGRADED_JSON=$(echo "${PACKAGES_DOWNGRADED_JSON::-1}") - # Création de l'array contenant les paquets rétrogradés, au format JSON - PACKAGES_DOWNGRADED_JSON="\"downgraded\":[$PACKAGES_DOWNGRADED_JSON]," - fi - - if [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_REINSTALLED_JSON=$(echo "${PACKAGES_REINSTALLED_JSON::-1}") - # Création de l'array contenant les paquets réinstallés, au format JSON - PACKAGES_REINSTALLED_JSON="\"reinstalled\":[$PACKAGES_REINSTALLED_JSON]," - fi - - # Si des paquets ont été parsé pour cet évènement alors on va pouvoir générer un JSON - if [ ! -z "$PACKAGES_INSTALLED_JSON" ] || [ ! -z "$PACKAGES_UPGRADED_JSON" ] || [ ! -z "$PACKAGES_REMOVED_JSON" ] || [ ! -z "$PACKAGES_PURGED_JSON" ] || [ ! -z "$PACKAGES_DOWNGRADED_JSON" ] || [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - # Construction de l'évènement au format JSON : - # D'abord on renseigne la date et l'heure de début / fin - JSON="{\"date_start\":\"$DATE_START\",\"date_end\":\"$DATE_END\",\"time_start\":\"$TIME_START\",\"time_end\":\"$TIME_END\",\"command\":\"$COMMAND\"," - - # Puis on ajoute les paquets installés si il y en a eu - if [ ! -z "$PACKAGES_INSTALLED_JSON" ];then - JSON+="$PACKAGES_INSTALLED_JSON" - fi - - # Puis on ajoute les paquets mis à jour si il y en a eu - if [ ! -z "$PACKAGES_UPGRADED_JSON" ];then - JSON+="$PACKAGES_UPGRADED_JSON" - fi - - # Puis on ajoute les paquets supprimés si il y en a eu - if [ ! -z "$PACKAGES_REMOVED_JSON" ];then - JSON+="$PACKAGES_REMOVED_JSON" - fi - - # Puis on ajoute les paquets purgés si il y en a eu - if [ ! -z "$PACKAGES_PURGED_JSON" ];then - JSON+="$PACKAGES_PURGED_JSON" - fi - - # Puis on ajoute les paquets rétrogradés si il y en a eu - if [ ! -z "$PACKAGES_DOWNGRADED_JSON" ];then - JSON+="$PACKAGES_DOWNGRADED_JSON" - fi - - # Puis on ajoute les paquets réinstallés si il y en a eu - if [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - JSON+="$PACKAGES_REINSTALLED_JSON" - fi - - # Suppression de la dernière virgule après le dernier array ajouté ( ], <= ici) - JSON=$(echo "${JSON::-1}") - # Fermeture de l'évènement en cours avant de passer au suivant (début de la boucle FOR) - JSON+="}" - - # On retourne le JSON de l'évènement parsé - APT_HISTORY_PARSER_RETURN="$JSON" - fi -} \ No newline at end of file diff --git a/src/200_yum-history-parser b/src/200_yum-history-parser deleted file mode 100644 index 478a031..0000000 --- a/src/200_yum-history-parser +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env bash - -function yumHistoryParser -{ - OLD_IFS=$IFS - IFS=$'\n' - - # Il faut fournir l'Id de la transaction yum à parser - if [ -z "$YUM_HISTORY_ID" ];then - echo -e "[$YELLOW ERROR $RESET] No yum transaction Id has been specified"; - return - fi - - # On extrait tout le contenu de l'évènement dans un fichier - TMP_EVENT_FILE="/tmp/.linupdate_${PROCID}_yum-history-parser.tmp" - - LC_ALL="en_US.UTF-8" yum history info "$YUM_HISTORY_ID" > "$TMP_EVENT_FILE" - - # Suppression de '**' si présent dans le fichier - sed -i 's/**//g' "$TMP_EVENT_FILE" - - # Extrait la date et l'heure au format : Thu Mar 25 10:40:37 2021 - EVENT_DATE=$(grep "^Begin time" "$TMP_EVENT_FILE" | sed 's/ //g' | sed 's/Begin time : //g') - # Extrait la date de la chaine précédemment récupéréé - DATE_START=$(date -d "$EVENT_DATE" +'%Y-%m-%d') - # Extrait l'heure de la chaine précédemment récupéréé - TIME_START=$(date -d "$EVENT_DATE" +'%H:%M:%S') - #DATE_END=$(echo "$EVENT" | grep "^End-Date:" | awk '{print $2}') - #TIME_END=$(echo "$EVENT" | grep "^End-Date:" | awk '{print $3}') - - # Commande exécutée - COMMAND=$(grep "^Command Line" $TMP_EVENT_FILE | sed 's/ / /g' | sed 's/Command Line ://g') - - # On reformate le fichier temporaire pour être sûr de ne garder que les paquets traités - sed -i -n '/Packages Altered/,$p' "$TMP_EVENT_FILE" - - LINES=$(egrep "^ +Install |^ +Dep-Install |^ +Updated |^ +Update |^ +Obsoleting |^ +Erase |^ +Downgrade |^ +Reinstall" "$TMP_EVENT_FILE") - echo "$LINES" > "$TMP_EVENT_FILE" - - PACKAGES_INSTALLED_JSON="" - DEPENDENCIES_INSTALLED_JSON="" - PACKAGES_UPGRADED_JSON="" - PACKAGES_REMOVED_JSON="" - PACKAGES_DOWNGRADED_JSON="" - PACKAGES_REINSTALLED_JSON="" - YUM_HISTORY_PARSER_RETURN="" - - for LINE in $(cat "$TMP_EVENT_FILE");do - - if echo "$LINE" | egrep -q "^ +Install ";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed "s/$PACKAGE_NAME//g" | sed 's/^-//g') - REPO=$(echo "$LINE" | awk '{print $3}') - PACKAGES_INSTALLED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\",\"repo\":\"$REPO\"}," - fi - - if echo "$LINE" | egrep -q "^ +Dep-Install ";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed "s/$PACKAGE_NAME//g" | sed 's/^-//g') - REPO=$(echo "$LINE" | awk '{print $3}') - DEPENDENCIES_INSTALLED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\",\"repo\":\"$REPO\"}," - fi - - if echo "$LINE" | egrep -q "^ +Updated ";then - # Si la ligne suivante commence par un chiffre alors on peut récupérer la version - # Sinon on ignore cette ligne - if grep -A1 "^${LINE}" "$TMP_EVENT_FILE" | grep -v "$LINE" | awk '{print $2}' | grep -q "^[0-9]";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(grep -A1 "^${LINE}" "$TMP_EVENT_FILE" | grep -v "$LINE" | awk '{print $2}') - REPO=$(grep -A1 "^${LINE}" "$TMP_EVENT_FILE" | grep -v "$LINE" | awk '{print $3}') - PACKAGES_UPGRADED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\",\"repo\":\"$REPO\"}," - fi - fi - - if echo "$LINE" | egrep -q "^ +Erase ";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed "s/$PACKAGE_NAME//g" | sed 's/^-//g') - PACKAGES_REMOVED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\"}," - fi - - if echo "$LINE" | egrep -q "^ +Downgrade ";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed "s/$PACKAGE_NAME//g" | sed 's/^-//g') - PACKAGES_DOWNGRADED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\"}," - fi - - if echo "$LINE" | egrep -q "^ +Reinstall ";then - PACKAGE_NAME=$(echo "$LINE" | awk '{print $2}' | sed 's/-[0-9].*//g') - PACKAGE_VERSION=$(echo "$LINE" | awk '{print $2}' | sed "s/$PACKAGE_NAME//g" | sed 's/^-//g') - REPO=$(echo "$LINE" | awk '{print $3}') - PACKAGES_REINSTALLED_JSON+="{\"name\":\"$PACKAGE_NAME\",\"version\":\"$PACKAGE_VERSION\",\"repo\":\"$REPO\"}," - fi - done - - rm "$TMP_EVENT_FILE" -f - - if [ ! -z "$PACKAGES_INSTALLED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_INSTALLED_JSON=$(echo "${PACKAGES_INSTALLED_JSON::-1}") - # Création de l'array contenant les paquets installés, au format JSON - PACKAGES_INSTALLED_JSON="\"installed\":[$PACKAGES_INSTALLED_JSON]," - fi - - if [ ! -z "$DEPENDENCIES_INSTALLED_JSON" ];then - # Suppression de la dernière virgule : - DEPENDENCIES_INSTALLED_JSON=$(echo "${DEPENDENCIES_INSTALLED_JSON::-1}") - # Création de l'array contenant les paquets installés, au format JSON - DEPENDENCIES_INSTALLED_JSON="\"dep_installed\":[$DEPENDENCIES_INSTALLED_JSON]," - fi - - if [ ! -z "$PACKAGES_UPGRADED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_UPGRADED_JSON=$(echo "${PACKAGES_UPGRADED_JSON::-1}") - # Création de l'array contenant les paquets mis à jour, au format JSON - PACKAGES_UPGRADED_JSON="\"upgraded\":[$PACKAGES_UPGRADED_JSON]," - fi - - if [ ! -z "$PACKAGES_REMOVED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_REMOVED_JSON=$(echo "${PACKAGES_REMOVED_JSON::-1}") - # Création de l'array contenant les paquets supprimés, au format JSON - PACKAGES_REMOVED_JSON="\"removed\":[$PACKAGES_REMOVED_JSON]," - fi - - if [ ! -z "$PACKAGES_DOWNGRADED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_DOWNGRADED_JSON=$(echo "${PACKAGES_DOWNGRADED_JSON::-1}") - # Création de l'array contenant les paquets rétrogradés, au format JSON - PACKAGES_DOWNGRADED_JSON="\"downgraded\":[$PACKAGES_DOWNGRADED_JSON]," - fi - - if [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - # Suppression de la dernière virgule : - PACKAGES_REINSTALLED_JSON=$(echo "${PACKAGES_REINSTALLED_JSON::-1}") - # Création de l'array contenant les paquets rétrogradés, au format JSON - PACKAGES_REINSTALLED_JSON="\"reinstalled\":[$PACKAGES_REINSTALLED_JSON]," - fi - - # Si des paquets ont été parsé pour cet évènement alors on va pouvoir générer un JSON - if [ ! -z "$PACKAGES_INSTALLED_JSON" ] || [ ! -z "$DEPENDENCIES_INSTALLED_JSON" ] || [ ! -z "$PACKAGES_UPGRADED_JSON" ] || [ ! -z "$PACKAGES_REMOVED_JSON" ] || [ ! -z "$PACKAGES_DOWNGRADED_JSON" ] || [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - # Construction de l'évènement au format JSON : - # D'abord on renseigne la date et l'heure de début / fin - JSON="{\"date_start\":\"$DATE_START\",\"date_end\":\"$DATE_END\",\"time_start\":\"$TIME_START\",\"time_end\":\"$TIME_END\",\"command\":\"$COMMAND\"," - - # Puis on ajoute les paquets installés si il y en a eu - if [ ! -z "$PACKAGES_INSTALLED_JSON" ];then - JSON+="$PACKAGES_INSTALLED_JSON" - fi - - # Puis on ajoute les dépendances installées si il y en a eu - if [ ! -z "$DEPENDENCIES_INSTALLED_JSON" ];then - JSON+="$DEPENDENCIES_INSTALLED_JSON" - fi - - # Puis on ajoute les paquets mis à jour si il y en a eu - if [ ! -z "$PACKAGES_UPGRADED_JSON" ];then - JSON+="$PACKAGES_UPGRADED_JSON" - fi - - # Puis on ajoute les paquets supprimés si il y en a eu - if [ ! -z "$PACKAGES_REMOVED_JSON" ];then - JSON+="$PACKAGES_REMOVED_JSON" - fi - - # Puis on ajoute les paquets rétrogradés si il y en a eu - if [ ! -z "$PACKAGES_DOWNGRADED_JSON" ];then - JSON+="$PACKAGES_DOWNGRADED_JSON" - fi - - # Puis on ajoute les paquets réinstallés si il y en a eu - if [ ! -z "$PACKAGES_REINSTALLED_JSON" ];then - JSON+="$PACKAGES_REINSTALLED_JSON" - fi - - # Suppression de la dernière virgule après le dernier array ajouté ( ], <= ici) - JSON=$(echo "${JSON::-1}") - - # Fermeture de l'évènement en cours avant de passer au suivant (début de la boucle FOR) - JSON+="}" - - # On retourne le JSON de l'évènement parsé - # YUM_HISTORY_PARSER_RETURN=$(echo "$JSON" | jq .) - YUM_HISTORY_PARSER_RETURN="$JSON" - fi - - IFS=$OLD_IFS -} \ No newline at end of file diff --git a/src/96_start-agent b/src/96_start-agent deleted file mode 100644 index 1e1ba07..0000000 --- a/src/96_start-agent +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -function startAgent -{ - systemctl start linupdate && - echo -e "${YELLOW}linupdate${RESET} agent started" -} \ No newline at end of file diff --git a/src/97_stop-agent b/src/97_stop-agent deleted file mode 100644 index 8132c17..0000000 --- a/src/97_stop-agent +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -function stopAgent -{ - systemctl stop linupdate && - echo -e "${YELLOW}linupdate${RESET} agent stopped" -} \ No newline at end of file diff --git a/src/98_restart-agent b/src/98_restart-agent deleted file mode 100644 index 6c9f3f0..0000000 --- a/src/98_restart-agent +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -function restartAgent -{ - systemctl restart linupdate && - echo -e " ${YELLOW}linupdate${RESET} agent restarted" -} \ No newline at end of file diff --git a/src/99_clean-exit b/src/99_clean-exit deleted file mode 100644 index d968fc3..0000000 --- a/src/99_clean-exit +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -function clean_exit -{ - # Clean temp files - # rm "/opt/linupdate.maj.sh" -f - # rm "/opt/reposconf/" -rf - rm "$LOG_REPORT_MAIL" -f - rm "/tmp/linupdate"* -rf - rm "/tmp/.linupdate_${PROCID}"* -rf - - if [ "$MOD_ERROR" -gt "0" ];then - sendMail - fi - - rm "/tmp/linupdate.lock" -f - - if [ "$UPDATE_ERROR" -gt "0" ];then - (( ERROR_STATUS++ )) - fi - - exit $ERROR_STATUS -} \ No newline at end of file diff --git a/src/99_disable-agent b/src/99_disable-agent deleted file mode 100644 index b794da8..0000000 --- a/src/99_disable-agent +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -function disableAgent -{ - systemctl disable --quiet linupdate && - echo -e "${YELLOW}linupdate${RESET} agent disabled on boot" -} \ No newline at end of file diff --git a/src/99_enable-agent b/src/99_enable-agent deleted file mode 100644 index 62f8eeb..0000000 --- a/src/99_enable-agent +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -function enableAgent -{ - systemctl enable --quiet linupdate && - echo -e "${YELLOW}linupdate${RESET} agent enabled on boot" -} \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/App/App.py b/src/controllers/App/App.py new file mode 100644 index 0000000..25ff13f --- /dev/null +++ b/src/controllers/App/App.py @@ -0,0 +1,155 @@ +# coding: utf-8 + +# Import libraries +from datetime import datetime +from pathlib import Path +import sys, socket, getpass, subprocess +from colorama import Fore, Style + +# Import classes +from src.controllers.App.Config import Config +from src.controllers.System import System + +class App: + #----------------------------------------------------------------------------------------------- + # + # Return current version of the application + # + #----------------------------------------------------------------------------------------------- + def get_version(self): + try: + file = open('/opt/linupdate/version', 'r') + version = file.read() + file.close() + except Exception as e: + version = 'unknown' + + return version + + + #----------------------------------------------------------------------------------------------- + # + # Get linupdate daemon agent status + # + #----------------------------------------------------------------------------------------------- + def get_agent_status(self): + # If systemctl is not installed (e.g. in docker container of linupdate's CI), return disabled + if not Path('/usr/bin/systemctl').is_file(): + return 'disabled' + + result = subprocess.run( + ["systemctl", "is-active", "linupdate"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + return 'stopped' + + return 'running' + + + #----------------------------------------------------------------------------------------------- + # + # Create lock file + # + #----------------------------------------------------------------------------------------------- + def set_lock(self): + try: + Path('/tmp/linupdate.lock').touch() + except Exception as e: + raise Exception('Could not create lock file /tmp/linupdate.lock: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Remove lock file + # + #----------------------------------------------------------------------------------------------- + def remove_lock(self): + if not Path('/tmp/linupdate.lock').is_file(): + return + + try: + Path('/tmp/linupdate.lock').unlink() + except Exception as e: + raise Exception('Could not remove lock file /tmp/linupdate.lock: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Create base directories + # + #----------------------------------------------------------------------------------------------- + def initialize(self): + # Create base directories + try: + Path('/etc/linupdate').mkdir(parents=True, exist_ok=True) + Path('/etc/linupdate/modules').mkdir(parents=True, exist_ok=True) + Path('/opt/linupdate').mkdir(parents=True, exist_ok=True) + Path('/opt/linupdate/service').mkdir(parents=True, exist_ok=True) + Path('/var/log/linupdate').mkdir(parents=True, exist_ok=True) + except Exception as e: + raise Exception('Could not create base directories: ' + str(e)) + + # Set permissions + try: + Path('/opt/linupdate').chmod(0o750) + Path('/opt/linupdate/src').chmod(0o750) + Path('/opt/linupdate/service').chmod(0o750) + Path('/etc/linupdate').chmod(0o750) + Path('/etc/linupdate/modules').chmod(0o750) + Path('/var/log/linupdate').chmod(0o750) + except Exception as e: + raise Exception('Could not set permissions to base directories: ' + str(e)) + + # Check if the .src directory is empty + if not len(list(Path('/opt/linupdate/src').rglob('*'))): + raise Exception('Some linupdate core files are missing, please reinstall linupdate') + + + #----------------------------------------------------------------------------------------------- + # + # Print app logo + # + #----------------------------------------------------------------------------------------------- + def print_logo(self): + space = ' ' + print(space + ' __ ') + print(space + '.__ .__ ____ __( o`- .___ __ ') + print(space + '| | |__| ____ __ _\ \/ / \__ ________ __| _/____ _/ |_ ____ ') + print(space + '| | | |/ \| | \ /| | | \____ \ / __ |\__ \\ ___/ __ \ ') + print(space + '| |_| | | | | / \ ^^| | | |_> / /_/ | / __ \| | \ ___/ ') + print(space + '|____|__|___| |____/___/\ \ |____/| __/\____ |(____ |__| \___ >') + print(space + ' \/ \_/ |__| \/ \/ \/ ') + print(Style.DIM + ' ' + self.get_version() + Style.RESET_ALL + '\n') + + + #----------------------------------------------------------------------------------------------- + # + # Print system and app summary + # + #----------------------------------------------------------------------------------------------- + def print_summary(self, fromAgent: bool = False): + myAppConfig = Config() + mySystem = System() + + # Define execution method + if fromAgent: + exec_method = 'automatic (agent)' + else: + if not sys.stdin.isatty(): + exec_method = 'automatic (no tty)' + else: + exec_method = 'manual (tty)' + + print(' Hostname: ' + Fore.YELLOW + socket.gethostname() + Style.RESET_ALL) + print(' OS: ' + Fore.YELLOW + mySystem.get_os_name() + ' ' + mySystem.get_os_version() + Style.RESET_ALL) + print(' Kernel: ' + Fore.YELLOW + mySystem.get_kernel() + Style.RESET_ALL) + print(' Virtualization: ' + Fore.YELLOW + mySystem.get_virtualization() + Style.RESET_ALL) + print(' Profile: ' + Fore.YELLOW + myAppConfig.get_profile() + Style.RESET_ALL) + print(' Environment: ' + Fore.YELLOW + myAppConfig.get_environment() + Style.RESET_ALL) + print(' Execution date: ' + Fore.YELLOW + datetime.now().strftime('%d-%m-%Y %H:%M:%S') + Style.RESET_ALL) + print(' Execution method: ' + Fore.YELLOW + exec_method + Style.RESET_ALL) + print(' Executed by user: ' + Fore.YELLOW + getpass.getuser() + Style.RESET_ALL + '\n') diff --git a/src/controllers/App/Config.py b/src/controllers/App/Config.py new file mode 100644 index 0000000..7e90c96 --- /dev/null +++ b/src/controllers/App/Config.py @@ -0,0 +1,657 @@ +# coding: utf-8 + +# Import libraries +from pathlib import Path +import shutil +import yaml +from colorama import Style + +# Import classes +from src.controllers.Yaml import Yaml + +class Config: + def __init__(self): + self.config_file = '/etc/linupdate/linupdate.yml' + + #----------------------------------------------------------------------------------------------- + # + # Return linupdate configuration from config file + # + #----------------------------------------------------------------------------------------------- + def get_conf(self): + # Open main YAML config file: + with open('/etc/linupdate/linupdate.yml') as stream: + try: + # Read YAML and return configuration + main = yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception(str(e)) + + # Open update YAML config file: + with open('/etc/linupdate/update.yml') as stream: + try: + # Read YAML and return configuration + update = yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception(str(e)) + + # Merge and return both configurations + configuration = {**main, **update} + + return configuration + + + #----------------------------------------------------------------------------------------------- + # + # Check if the config file exists and if it contains the required parameters + # + #----------------------------------------------------------------------------------------------- + def check_conf(self): + try: + # Check if main config file exists + if not Path(self.config_file).is_file(): + raise Exception('configuration file ' + self.config_file + ' is missing') + + # Check if update config file exists + if not Path(self.config_file).is_file(): + raise Exception('configuration file ' + self.config_file + ' is missing') + + # Retrieve configuration + configuration = self.get_conf() + + # Check if main is set + if 'main' not in configuration: + raise Exception('main key is missing in ' + self.config_file) + + # Check if main.profile is set + if 'profile' not in configuration['main']: + raise Exception('main.profile key is missing in ' + self.config_file) + + # Check if main.profile is not empty + if configuration['main']['profile'] == None: + raise Exception('main.profile key is empty in ' + self.config_file) + + # Check if main.environment is set + if 'environment' not in configuration['main']: + raise Exception('main.environment key is missing in ' + self.config_file) + + # Check if main.environment is not empty + if configuration['main']['environment'] == None: + raise Exception('main.environment key is empty in ' + self.config_file) + + # Check if main.mail is set + if 'mail' not in configuration['main']: + raise Exception('main.mail key is missing in ' + self.config_file) + + # Check if main.mail.enabled is set + if 'enabled' not in configuration['main']['mail']: + raise Exception('main.mail.enabled key is missing in ' + self.config_file) + + # Check if main.mail.enabled is set to True or False + if configuration['main']['mail']['enabled'] not in [True, False]: + raise Exception('main.mail.enabled key must be set to true or false in ' + self.config_file) + + # Check if main.mail.recipient is set + if 'recipient' not in configuration['main']['mail']: + raise Exception('main.mail.recipient key is missing in ' + self.config_file) + + # Check if modules is set + if 'modules' not in configuration: + raise Exception('modules key is missing in ' + self.config_file) + + # Check if modules.enabled is set + if 'enabled' not in configuration['modules']: + raise Exception('modules.enabled key is missing in ' + self.config_file) + + # Check if update is set + if 'update' not in configuration: + raise Exception('update key is missing in ' + self.config_file) + + # Check if update.method is set + if 'method' not in configuration['update']: + raise Exception('update.method key is missing in ' + self.config_file) + + # Check if update.method is not empty + if configuration['update']['method'] == None: + raise Exception('update.method key is empty in ' + self.config_file) + + # Check if update.exit_on_package_update_error is set + if 'exit_on_package_update_error' not in configuration['update']: + raise Exception('update.exit_on_package_update_error key is missing in ' + self.config_file) + + # Check if update.exit_on_package_update_error is set to True or False + if configuration['update']['exit_on_package_update_error'] not in [True, False]: + raise Exception('update.exit_on_package_update_error key must be set to true or false in ' + self.config_file) + + # Check if update.packages is set + if 'packages' not in configuration['update']: + raise Exception('update.packages key is missing in ' + self.config_file) + + # Check if update.packages.exclude section is set + if 'exclude' not in configuration['update']['packages']: + raise Exception('update.packages.exclude key is missing in ' + self.config_file) + + # Check if update.packages.exclude.always is set + if 'always' not in configuration['update']['packages']['exclude']: + raise Exception('update.packages.exclude.always key is missing in ' + self.config_file) + + # Check if update.packages.exclude.on_major_update is set + if 'on_major_update' not in configuration['update']['packages']['exclude']: + raise Exception('update.packages.exclude.on_major_update key is missing in ' + self.config_file) + + # Check if post_update.services section is set + if 'services' not in configuration['post_update']: + raise Exception('post_update.services key is missing in ' + self.config_file) + + # Check if post_update.services.restart is set in + if 'restart' not in configuration['post_update']['services']: + raise Exception('post_update.services.restart key is missing in ' + self.config_file) + + except Exception as e: + raise Exception('Fatal configuration file error: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Generate config files if not exist + # + #----------------------------------------------------------------------------------------------- + def generate_conf(self): + # If main config file does not exist, generate it + if not Path('/etc/linupdate/linupdate.yml').is_file(): + # Copy default configuration file + try: + shutil.copy2('/opt/linupdate/templates/linupdate.template.yml', '/etc/linupdate/linupdate.yml') + except Exception as e: + raise Exception('Could not generate configuration file /etc/linupdate/linupdate.yml: ' + str(e)) + + # If update config file does not exist, generate it + if not Path('/etc/linupdate/update.yml').is_file(): + # Copy default configuration file + try: + shutil.copy2('/opt/linupdate/templates/update.template.yml', '/etc/linupdate/update.yml') + except Exception as e: + raise Exception('Could not generate configuration file /etc/linupdate/update.yml: ' + str(e)) + + # TODO: to remove in some time + # If old linupdate (bash version) config file exists, migrate it + if Path('/etc/linupdate/linupdate.conf').is_file(): + self.migrate_conf() + + + #----------------------------------------------------------------------------------------------- + # + # Write linupdate configuration to config file + # + #----------------------------------------------------------------------------------------------- + def write_conf(self, configuration): + # Use custom Yaml class to keep the order of the keys + yaml = Yaml() + + main_config = { + 'main': { + **configuration['main'] + }, + 'modules': { + **configuration['modules'] + } + } + + update_config = { + 'update': { + **configuration['update'] + }, + 'post_update': { + **configuration['post_update'] + } + } + + # Write to main config file + try: + # TODO: When OS based on RHEL8 will not be used anymore, use 'sort_keys=False' to keep the order of the keys when writing the file + # e.g. yaml.dump(main_config, file, default_flow_style=False, sort_keys=False) + # But for now use the custom Yaml class to keep the order of the keys, and dict(main_config) to convert it back to a dict, because python3-yaml on RHEL8 does not support sort_keys=False + yaml.write(main_config, '/etc/linupdate/linupdate.yml') + except Exception as e: + raise Exception('Could not write configuration file /etc/linupdate/linupdate.yml: ' + str(e)) + + # Write to update config file + try: + yaml.write(update_config, '/etc/linupdate/update.yml') + except Exception as e: + raise Exception('Could not write configuration file /etc/linupdate/update.yml: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Print linupdate configuration to console (yaml format) + # + #----------------------------------------------------------------------------------------------- + def show_config(self): + try: + # Open main YAML config file: + with open('/etc/linupdate/linupdate.yml') as stream: + try: + # Read YAML and return configuration + main = yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception(str(e)) + + # Open update YAML config file: + with open('/etc/linupdate/update.yml') as stream: + try: + # Read YAML and return configuration + update = yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception(str(e)) + + print(Style.BRIGHT + 'Global configuration: ' + Style.RESET_ALL, end='\n\n') + + try: + print(yaml.dump(main, default_flow_style=False, sort_keys=False)) + except TypeError: + print(yaml.dump(main, default_flow_style=False)) + + print(Style.BRIGHT + 'Update configuration: ' + Style.RESET_ALL, end='\n\n') + + try: + print(yaml.dump(update, default_flow_style=False, sort_keys=False)) + except TypeError: + print(yaml.dump(update, default_flow_style=False)) + + # If there are modules config files, print them + if 'modules' in main: + if 'enabled' in main['modules']: + for module in main['modules']['enabled']: + try: + print(Style.BRIGHT + 'Module ' + module + ' configuration: ' + Style.RESET_ALL, end='\n\n') + + with open('/etc/linupdate/modules/' + module + '.yml') as stream: + try: + module_conf = yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception(str(e)) + + try: + print(yaml.dump(module_conf, default_flow_style=False, sort_keys=False)) + except TypeError: + print(yaml.dump(module_conf, default_flow_style=False)) + + except FileNotFoundError: + print('No configuration file found for module ' + module) + + except Exception as e: + raise Exception('Could not show configuration: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Return linupdate profile from config file + # + #----------------------------------------------------------------------------------------------- + def get_profile(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['main']['profile'] + + + #----------------------------------------------------------------------------------------------- + # + # Set linupdate profile in config file + # + #----------------------------------------------------------------------------------------------- + def set_profile(self, profile): + # Get current configuration + configuration = self.get_conf() + + # Set profile + configuration['main']['profile'] = profile + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Return linupdate environment from config file + # + #----------------------------------------------------------------------------------------------- + def get_environment(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['main']['environment'] + + + #----------------------------------------------------------------------------------------------- + # + # Set linupdate environment in config file + # + #----------------------------------------------------------------------------------------------- + def set_environment(self, environment): + # Get current configuration + configuration = self.get_conf() + + # Set environment + configuration['main']['environment'] = environment + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Enable or disable mail alert + # + #----------------------------------------------------------------------------------------------- + def set_mail_enable(self, enabled: bool): + # Get current configuration + configuration = self.get_conf() + + # Set environment + configuration['main']['mail']['enabled'] = enabled + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Get mail alert status + # + #----------------------------------------------------------------------------------------------- + def get_mail_enabled(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['main']['mail']['enabled'] + + + #----------------------------------------------------------------------------------------------- + # + # Get mail recipient(s) + # + #----------------------------------------------------------------------------------------------- + def set_mail_recipient(self, recipient: str = None): + # Get current configuration + configuration = self.get_conf() + + # If no recipient to set, set empty list + if not recipient: + configuration['main']['mail']['recipient'] = [] + + else: + # For each recipient, append it to the list if not already in + for item in recipient.split(","): + if item not in configuration['main']['mail']['recipient']: + # Append recipient + configuration['main']['mail']['recipient'].append(item) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Get mail recipient(s) + # + #----------------------------------------------------------------------------------------------- + def get_mail_recipient(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['main']['mail']['recipient'] + + + #----------------------------------------------------------------------------------------------- + # + # Get update method + # + #----------------------------------------------------------------------------------------------- + def get_update_method(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['update']['method'] + + + #----------------------------------------------------------------------------------------------- + # + # Set update method + # + #----------------------------------------------------------------------------------------------- + def set_update_method(self, method: str): + if method not in ['one_by_one', 'global']: + raise Exception('Invalid update method: ' + method) + + # Get current configuration + configuration = self.get_conf() + + # Set method + configuration['update']['method'] = method + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Set exit on package update error + # + #----------------------------------------------------------------------------------------------- + def set_exit_on_package_update_error(self, exit_on_package_update_error: bool): + # Get current configuration + configuration = self.get_conf() + + # Set method + configuration['update']['exit_on_package_update_error'] = exit_on_package_update_error + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Return linupdate packages exclude list from config file + # + #----------------------------------------------------------------------------------------------- + def get_exclusion(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['update']['packages']['exclude']['always'] + + + #----------------------------------------------------------------------------------------------- + # + # Set linupdate packages exclude list in config file + # + #----------------------------------------------------------------------------------------------- + def set_exclusion(self, exclude: str = None): + # Get current configuration + configuration = self.get_conf() + + # If no package to exclude, set empty list + if not exclude: + configuration['update']['packages']['exclude']['always'] = [] + + else: + # For each package to exclude, append it to the list if not already in + for item in exclude.split(","): + if item not in configuration['update']['packages']['exclude']['always']: + # Append exclude + configuration['update']['packages']['exclude']['always'].append(item) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Return linupdate packages exclude list on major update from config file + # + #----------------------------------------------------------------------------------------------- + def get_major_exclusion(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['update']['packages']['exclude']['on_major_update'] + + + #----------------------------------------------------------------------------------------------- + # + # Set linupdate packages exclude list on major update in config file + # + #----------------------------------------------------------------------------------------------- + def set_major_exclusion(self, exclude: str = None): + # Get current configuration + configuration = self.get_conf() + + # If no package to exclude, set empty list + if not exclude: + configuration['update']['packages']['exclude']['on_major_update'] = [] + + else: + # For each package to exclude, append it to the list if not already in + for item in exclude.split(","): + if item not in configuration['update']['packages']['exclude']['on_major_update']: + # Append exclude + configuration['update']['packages']['exclude']['on_major_update'].append(item) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Get services to restart + # + #----------------------------------------------------------------------------------------------- + def get_service_to_restart(self): + # Get current configuration + configuration = self.get_conf() + + return configuration['post_update']['services']['restart'] + + + #----------------------------------------------------------------------------------------------- + # + # Set services to restart + # + #----------------------------------------------------------------------------------------------- + def set_service_to_restart(self, services: str = None): + # Get current configuration + configuration = self.get_conf() + + # If no service to restart, set empty list + if not services: + configuration['post_update']['services']['restart'] = [] + + else: + # For each service to restart, append it to the list if not already in + for item in services.split(","): + if item not in configuration['post_update']['services']['restart']: + # Append service + configuration['post_update']['services']['restart'].append(item) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Append a module to the enabled list + # + #----------------------------------------------------------------------------------------------- + def append_module(self, module): + # Get current configuration + configuration = self.get_conf() + + # Add module to enabled list + configuration['modules']['enabled'].append(module) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Remove a module from the enabled list + # + #----------------------------------------------------------------------------------------------- + def remove_module(self, module): + # Get current configuration + configuration = self.get_conf() + + # Remove module from enabled list + configuration['modules']['enabled'].remove(module) + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Migration of old linupdate configuration file + # + #----------------------------------------------------------------------------------------------- + def migrate_conf(self): + # Old config file are like ini file + + print(' Detected old configuration file /etc/linupdate/linupdate.conf, migrating...') + + try: + # Open old config file + with open('/etc/linupdate/linupdate.conf', 'r') as file: + lines = file.readlines() + + for line in lines: + # If profile is set, set it in the new config file + if 'PROFILE=' in line: + profile = line.split('=')[1].replace('"', '').strip() + self.set_profile(profile) + + # If environment is set, set it in the new config file + if 'ENV=' in line: + environment = line.split('=')[1].replace('"', '').strip() + self.set_environment(environment) + + # If mail alert is enabled, set it in the new config file + if 'MAIL_ENABLED=' in line: + mail_alert = line.split('=')[1].replace('"', '').strip() + if (mail_alert in ['true', 'True']): + self.set_mail_enable(True) + else: + self.set_mail_enable(False) + + # If mail recipient is set, set it in the new config file + if 'MAIL_RECIPIENT=' in line: + mail_recipient = line.split('=')[1].replace('"', '').strip() + self.set_mail_recipient(mail_recipient) + + # If exclude major is set, set it in the new config file + if 'EXCLUDE_MAJOR=' in line: + major_exclusion = line.split('=')[1].replace('"', '').strip() + self.set_major_exclusion(major_exclusion) + + # If exclude is set, set it in the new config file + if 'EXCLUDE=' in line: + exclusion = line.split('=')[1].replace('"', '').strip() + self.set_exclusion(exclusion) + + # If services to restart are set, set them in the new config file + if 'SERVICE_RESTART=' in line: + services = line.split('=')[1].replace('"', '').strip() + self.set_service_to_restart(services) + + # Move old file + shutil.move('/etc/linupdate/linupdate.conf', '/etc/linupdate/linupdate.conf.migrated') + + except Exception as e: + raise Exception('Could not migrate configuration file /etc/linupdate/linupdate.conf: ' + str(e)) diff --git a/src/controllers/App/Service.py b/src/controllers/App/Service.py new file mode 100644 index 0000000..6a484d3 --- /dev/null +++ b/src/controllers/App/Service.py @@ -0,0 +1,185 @@ +# coding: utf-8 + +# Import libraries +import subprocess +import signal +import sys +import importlib +import subprocess +import time +from colorama import Fore, Style + +# Import classes +from src.controllers.Module.Module import Module + +class Service: + def __init__(self): + # Register signal handlers + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGINT, self.signal_handler) + + self.child_processes = [] + self.child_processes_started = [] + self.moduleController = Module() + + + #----------------------------------------------------------------------------------------------- + # + # Service main function + # + #----------------------------------------------------------------------------------------------- + def main(self): + try: + print("[linupdate] Hi, I'm linupdate service. I will start all enabled module agents and let them run in background. Stop me and I will stop all module agents.") + # Wait 3 seconds to let the above message to be read + time.sleep(3) + + while True: + # Check for terminated child processes (module agents) and remove them from the list + for child in self.child_processes[:]: + retcode = child['process'].poll() + + if retcode is not None: + # If the process has terminated with an error (exit 1), print a message + if retcode != 0: + print('[' + child['agent'] + '-agent] Terminated with return code ' + str(retcode) + ' :(') + print("[" + child['agent'] + "-agent] I'm dead for now but I will be resurrected soon, please wait or restart main service") + + # Remove child process from list + self.child_processes.remove(child) + + # Retrieve enabled modules + enabled_modules = self.moduleController.getEnabled() + + # For each enabled module, check if its agent is enabled + for module in enabled_modules: + # Convert module name to uppercase first letter + module_capitalize = module.capitalize() + + # Import python module config class + module_import_path = importlib.import_module('src.controllers.Module.' + module_capitalize + '.Config') + module_class = getattr(module_import_path, 'Config') + + # Instantiate module and get module configuration + my_module = module_class() + module_configuration = my_module.get_conf() + + # Check if module agent is enabled. If not, skip to next module + if not module_configuration['agent']['enabled']: + continue + + # Check if module agent process is already running, if so, skip to next module + if any(agent['agent'] == module for agent in self.child_processes): + continue + + # Only start module agent if it has not been started in the last 2 minutes + # This is to avoid restarting a failed module agent in a loop and consuming resources, as the service will restart it every 2 minutes + # An agent could fail to start or fail when executing a task, so we need to avoid restarting it in a loop + if any(agent['agent'] == module for agent in self.child_processes_started): + # Calculate the remaining time before the agent can be restarted + remaining_time = 120 - (time.time() - [agent['start_time'] for agent in self.child_processes_started if agent['agent'] == module][0]) + + if remaining_time < 120 and remaining_time >= 0: + # Print a message with the remaining time before the agent can be restarted (in seconds) + print('[linupdate] Delay in restarting the ' + module + ' agent: next start time is in ' + str(int(remaining_time)) + ' seconds') + continue + + # Start module agent + print('[linupdate] Starting ' + Fore.GREEN + module + Style.RESET_ALL + ' module agent') + + # Start module agent as a child process + process = subprocess.Popen( + ['/opt/linupdate/service.py', module], + stdout=sys.stdout, + stderr=sys.stderr + ) + + # Add child process to list + self.child_processes.append({ + 'agent': module, + 'process': process + }) + + # Add child process start timestamp to list, first remove it if it already exists (from a previous start) + if any(agent['agent'] == module for agent in self.child_processes_started): + self.child_processes_started = [agent for agent in self.child_processes_started if agent['agent'] != module] + + # Add the new start timestamp + self.child_processes_started.append({ + 'agent': module, + 'start_time': time.time() + }) + + time.sleep(5) + + except Exception as e: + print('[linupdate] General error: ' + str(e)) + exit(1) + + + #----------------------------------------------------------------------------------------------- + # + # Run a module agent as a child process + # + #----------------------------------------------------------------------------------------------- + def run_agent(self, module_name): + try: + # Convert module name to uppercase first letter + module_name_capitalize = module_name.capitalize() + + print("[" + module_name + "-agent] Hi, I'm " + Fore.GREEN + module_name + Style.RESET_ALL + ' module agent') + + # Import python module agent class + module_import_path = importlib.import_module('src.controllers.Module.' + module_name_capitalize + '.Agent') + my_module_agent_class = getattr(module_import_path, 'Agent')() + + # Run module agent main function + my_module_agent_class.main() + + except Exception as e: + print('[' + module_name + '-agent] Error: ' + str(e)) + exit(1) + + + #----------------------------------------------------------------------------------------------- + # + # Stop all child processes + # + #----------------------------------------------------------------------------------------------- + def stop_child_processes(self): + if not self.child_processes: + return + + # Stop all child processes + for child in self.child_processes: + # Retrieve agent name and process + agent = child['agent'] + process = child['process'] + + print('Stopping ' + agent + ' module agent') + + # Terminate process + process.terminate() + + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + + print('Stopped ' + child['agent'] + ' module agent') + + + #----------------------------------------------------------------------------------------------- + # + # Signal handler + # This function is called when the service receives a SIGTERM or SIGINT signal + # + #----------------------------------------------------------------------------------------------- + def signal_handler(self, sig, frame): + print('Linupdate service received signal ' + str(sig) + '. Stopping all child processes...') + + try: + self.stop_child_processes() + sys.exit(0) + except Exception as e: + print('Error while stopping child processes: ' + str(e)) diff --git a/src/controllers/App/Utils.py b/src/controllers/App/Utils.py new file mode 100644 index 0000000..dfc095d --- /dev/null +++ b/src/controllers/App/Utils.py @@ -0,0 +1,20 @@ +# coding: utf-8 + +# Import libraries +import json + +class Utils: + #----------------------------------------------------------------------------------------------- + # + # Return True if the string is a valid JSON + # + #----------------------------------------------------------------------------------------------- + def is_json(self, jsonString): + try: + json.loads(jsonString) + except json.decoder.JSONDecodeError: + return False + except ValueError as e: + return False + + return True diff --git a/src/controllers/App/__init__.py b/src/controllers/App/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/Args.py b/src/controllers/Args.py new file mode 100644 index 0000000..a6ffe01 --- /dev/null +++ b/src/controllers/Args.py @@ -0,0 +1,784 @@ +# coding: utf-8 + +# Import libraries +from tabulate import tabulate +from colorama import Fore, Style +import sys +import argparse + +# Import classes +from src.controllers.App.App import App +from src.controllers.App.Config import Config +from src.controllers.Module.Module import Module +from src.controllers.Exit import Exit +from src.controllers.ArgsException import ArgsException + +class Args: + + #----------------------------------------------------------------------------------------------- + # + # Pre-parse arguments + # + #----------------------------------------------------------------------------------------------- + def pre_parse(self): + # Default values + Args.from_agent = False + + # + # If --from-agent param has been set + # + if '--from-agent' in sys.argv: + Args.from_agent = True + + + #----------------------------------------------------------------------------------------------- + # + # Parse arguments + # + #----------------------------------------------------------------------------------------------- + def parse(self): + # Default values + Args.assume_yes = False + Args.check_updates = False + Args.ignore_exclude = False + Args.dist_upgrade = False + Args.keep_oldconf = True + + myApp = App() + myAppConfig = Config() + myModule = Module() + myExit = Exit() + + try: + # Parse arguments + parser = argparse.ArgumentParser(add_help=False) + + # Define valid arguments + # Help + parser.add_argument("--help", "-h", action="store_true", default="null") + # Version + parser.add_argument("--version", "-v", action="store_true", default="null") + # Show raw config + parser.add_argument("--show-config", "-sc", action="store_true", default="null") + + # Profile + parser.add_argument("--profile", "-p", action="store", nargs='?', default="null") + # Environment + parser.add_argument("--env", "-e", action="store", nargs='?', default="null") + # Mail alert enable + parser.add_argument("--mail-enable", action="store", nargs='?', default="null") + # Get mail recipient + parser.add_argument("--get-mail-recipient", action="store_true", default="null") + # Set mail recipient + parser.add_argument("--set-mail-recipient", action="store", nargs='?', default="null") + + # Dist upgrade + parser.add_argument("--dist-upgrade", "-du", action="store_true", default="null") + # Keep oldconf + parser.add_argument("--keep-oldconf", action="store_true", default="null") + # Force / assume-yes + parser.add_argument("--assume-yes", "-y", action="store_true", default="null") + # Check updates + parser.add_argument("--check-updates", "-cu", action="store_true", default="null") + # Ignore exclude + parser.add_argument("--ignore-exclude", "-ie", action="store_true", default="null") + # Get update method + parser.add_argument("--get-update-method", action="store", nargs='?', default="null") + # Set Update method + parser.add_argument("--set-update-method", action="store", nargs='?', default="null") + # Exit on package update error + parser.add_argument("--exit-on-package-update-error", action="store", nargs='?', default="null") + + # Get excluded packages + parser.add_argument("--get-exclude", action="store", nargs='?', default="null") + # Get excluded packages on major update + parser.add_argument("--get-exclude-major", action="store", nargs='?', default="null") + # Get services to restart after package update + parser.add_argument("--get-service-restart", action="store", nargs='?', default="null") + # Exclude + parser.add_argument("--exclude", action="store", nargs='?', default="null") + # Exclude on major update + parser.add_argument("--exclude-major", action="store", nargs='?', default="null") + # Services to restart after package update + parser.add_argument("--service-restart", action="store", nargs='?', default="null") + + # List modules + parser.add_argument("--mod-list", action="store_true", default="null") + # Module enable + parser.add_argument("--mod-enable", action="store", nargs='?', default="null") + # Module disable + parser.add_argument("--mod-disable", action="store", nargs='?', default="null") + + # Parse arguments + args, remaining_args = parser.parse_known_args() + + # If remaining_args arguments are passed + if remaining_args: + # If --mod-configure or -mc or --mod-exec or -me is in list (use regex) + if '--mod-configure' in remaining_args or '-mc' in remaining_args or '--mod-exec' in remaining_args or '-me' in remaining_args: + if '--mod-configure' in remaining_args: + param_name = '--mod-configure' + elif '-mc' in remaining_args: + param_name = '-mc' + elif '--mod-exec' in remaining_args: + param_name = '--mod-exec' + elif '-me' in remaining_args: + param_name = '-me' + + # Retrieve all arguments after --mod-configure (or -mc, etc.) + mod_index = remaining_args.index(param_name) + mod_args = remaining_args[mod_index + 1:] + + # If no arguments are passed after --mod-configure, print an error + if not mod_args: + raise ArgsException(param_name + ' requires additional arguments') + + # Else, pass the arguments to the module + else: + # Retrieve module name + mod_name = mod_args[0] + + # Retrieve all arguments after the module name + mod_args = mod_args[1:] + + # Check if module exists + if not myModule.exists(mod_name): + raise ArgsException('Module ' + mod_name + ' does not exist') + + # Configure module + try: + myModule.configure(mod_name, mod_args) + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not configure ' + mod_name + ' module: ' + str(e)) + + # If there are remaining arguments, print help and raise an exception + else: + self.help() + raise ArgsException('Unknown argument(s): ' + str(remaining_args)) + + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException(str(e)) + except Exception as e: + raise ArgsException(str(e)) + + try: + # + # If --help param has been set + # + if args.help != "null": + if args.help: + self.help() + myExit.clean_exit(0, False) + + # + # If --version param has been set + # + if args.version != "null": + if args.version: + print(' Current version: ' + Fore.GREEN + myApp.get_version() + Style.RESET_ALL, end='\n\n') + myExit.clean_exit(0, False) + + # + # If --show-config param has been set + # + if args.show_config != "null": + if args.show_config: + myAppConfig.show_config() + myExit.clean_exit(0, False) + + # + # If --profile param has been set + # + if args.profile != "null": + try: + # If a profile is set (not 'None'), change the app profile + if args.profile: + # Get current profile + currentProfile = myAppConfig.get_profile() + + # Print profile change + print(' Switching from profile ' + Fore.GREEN + currentProfile + Style.RESET_ALL + ' to ' + Fore.GREEN + args.profile + Style.RESET_ALL, end='\n\n') + + # Set new profile + myAppConfig.set_profile(args.profile) + + # Else print the current profile + else: + print(' Current profile: ' + Fore.GREEN + myAppConfig.get_profile() + Style.RESET_ALL, end='\n\n') + + myExit.clean_exit(0, False) + + except Exception as e: + raise ArgsException('could not switch profile: ' + str(e)) + + # + # If --env param has been set + # + if args.env != "null": + try: + # If a environment is set (not 'None'), change the app environment + if args.env: + # Get current environment + currentEnvironment = myAppConfig.get_environment() + + # Print environment change + print(' Switching from environment ' + Fore.GREEN + currentEnvironment + Style.RESET_ALL + ' to ' + Fore.GREEN + args.env + Style.RESET_ALL, end='\n\n') + + # Set new environment + myAppConfig.set_environment(args.env) + # Else print the current environment + else: + print(' Current environment: ' + Fore.GREEN + myAppConfig.get_environment() + Style.RESET_ALL, end='\n\n') + + myExit.clean_exit(0, False) + + except Exception as e: + raise ArgsException('could not switch environment: ' + str(e)) + + # + # If --mail-enable param has been set + # + if args.mail_enable != "null": + try: + if args.mail_enable == 'true': + myAppConfig.set_mail_enable(True) + print(' Mail sending ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + myAppConfig.set_mail_enable(False) + print(' Mail sending ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not configure mail: ' + str(e)) + + # + # If --get-mail-recipient param has been set + # + if args.get_mail_recipient != "null": + try: + print(' Current mail recipient(s): ' + Fore.GREEN) + + recipients = myAppConfig.get_mail_recipient() + + # If no recipient is set + if not recipients: + print(' ▪ None') + else: + for recipient in recipients: + print(' ▪ ' + recipient) + + print(Style.RESET_ALL, end='\n') + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not get mail recipient(s): ' + str(e)) + + # + # If --set-mail-recipient param has been set + # + if args.set_mail_recipient != "null": + try: + myAppConfig.set_mail_recipient(args.set_mail_recipient) + + print(' Mail recipient set to:' + Fore.GREEN) + + # If no recipient is set + if not args.set_mail_recipient: + print(' ▪ None') + else: + for item in args.set_mail_recipient.split(","): + print(' ▪ ' + item) + + print(Style.RESET_ALL, end='\n') + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not set mail recipient(s): ' + str(e)) + + # + # If --ignore-exclude param has been set + if args.ignore_exclude != "null": + Args.ignore_exclude = True + + # + # If --check-updates param has been set + # + if args.check_updates != "null": + Args.check_updates = True + + # + # If --dist-upgrade param has been set + # + if args.dist_upgrade != "null": + Args.dist_upgrade = True + + # + # If --keep-oldconf param has been set + # + if args.keep_oldconf != "null": + Args.keep_oldconf = True + + # + # If --assume-yes param has been set + # + if args.assume_yes != "null": + Args.assume_yes = True + + # + # If --get-update-method param has been set + # + if args.get_update_method != "null": + try: + update_method = myAppConfig.get_update_method() + print(' Current update method: ' + Fore.GREEN + update_method + Style.RESET_ALL, end='\n\n') + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not get update method: ' + str(e)) + + # + # If --set-update-method param has been set + # + if args.set_update_method != "null": + try: + myAppConfig.set_update_method(args.set_update_method) + print(' Update method set to ' + Fore.GREEN + args.set_update_method + Style.RESET_ALL, end='\n\n') + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not set update method: ' + str(e)) + + # + # If --exit-on-package-update-error param has been set + # + if args.exit_on_package_update_error != "null": + try: + if args.exit_on_package_update_error == 'true': + myAppConfig.set_exit_on_package_update_error(True) + print(' Exit on package update error ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + myAppConfig.set_exit_on_package_update_error(False) + print(' Exit on package update error ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not configure exit on package update error: ' + str(e)) + + # + # If --get-exclude param has been set + # + if args.get_exclude != "null": + try: + packages = myAppConfig.get_exclusion() + + print(' Currently excluded packages: ' + Fore.GREEN) + + # If no package is excluded + if not packages: + print(' ▪ None') + else: + for package in packages: + print(' ▪ ' + package) + + print(Style.RESET_ALL) + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not get excluded packages: ' + str(e)) + + # + # If --get-exclude-major param has been set + # + if args.get_exclude_major != "null": + try: + packages = myAppConfig.get_major_exclusion() + + print(' Currently excluded packages on major update: ' + Fore.GREEN) + + # If no package is excluded + if not packages: + print(' ▪ None') + else: + for package in packages: + print(' ▪ ' + package) + + print(Style.RESET_ALL) + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not get excluded packages on major update: ' + str(e)) + + # + # If --get-service-restart param has been set + # + if args.get_service_restart != "null": + try: + services = myAppConfig.get_service_to_restart() + + print(' Services to restart after package update: ' + Fore.GREEN) + + # If no service is set to restart + if not services: + print(' ▪ None') + else: + for service in services: + print(' ▪ ' + service) + + print(Style.RESET_ALL, end='\n') + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not get services to restart: ' + str(e)) + + # + # If --exclude param has been set + # + if args.exclude != "null": + try: + # Exclude packages + myAppConfig.set_exclusion(args.exclude) + + # Print excluded packages + packages = myAppConfig.get_exclusion() + + print(' Excluding packages: ' + Fore.GREEN) + + # If no package is excluded + if not packages: + print(' ▪ None') + else: + for package in packages: + print(' ▪ ' + package) + + print(Style.RESET_ALL) + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not exclude packages: ' + str(e)) + + # + # If --exclude-major param has been set + # + if args.exclude_major != "null": + try: + # Exclude packages on major update + myAppConfig.set_major_exclusion(args.exclude_major) + + # Print excluded packages + packages = myAppConfig.get_major_exclusion() + + print(' Excluding packages on major update: ' + Fore.GREEN) + + # If no package is excluded + if not packages: + print(' ▪ None') + else: + for package in packages: + print(' ▪ ' + package) + + print(Style.RESET_ALL) + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not exclude packages on major update: ' + str(e)) + + # + # If --service-restart param has been set + # + if args.service_restart != "null": + try: + # Set services to restart after package update + myAppConfig.set_service_to_restart(args.service_restart) + + # Print services to restart + services = myAppConfig.get_service_to_restart() + + print(' Setting services to restart after package update: ' + Fore.GREEN) + + for service in services: + print(' ▪ ' + service) + + # If no service is set to restart + if not services: + print(' ▪ None') + else: + for service in services: + print(' ▪ ' + service) + + print(Style.RESET_ALL) + + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not set services to restart after package update: ' + str(e)) + + # + # If --mod-list param has been set + # + if args.mod_list != "null": + myModule.list() + myExit.clean_exit(0, False) + + # + # If --mod-enable param has been set + # + if args.mod_enable != "null": + # If module to enable is set (not 'None'), enable the module + if args.mod_enable: + # Enable module + try: + myModule.enable(args.mod_enable) + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not enable module: ' + str(e)) + else: + raise ArgsException('Module name is required') + + # + # If --mod-disable param has been set + # + if args.mod_disable != "null": + # If module to disable is set (not 'None'), disable the module + if args.mod_disable: + # Disable module + try: + myModule.disable(args.mod_disable) + myExit.clean_exit(0, False) + except Exception as e: + raise ArgsException('Could not disable module: ' + str(e)) + else: + raise ArgsException('Module name is required') + + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException(str(e)) + except Exception as e: + raise ArgsException(str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Print help + # + #----------------------------------------------------------------------------------------------- + def help(self): + try: + table = [] + options = [ + { + 'args': [ + '--help', + '-h' + ], + 'description': 'Show help', + }, + { + 'args': [ + '--show-config', + '-sc' + ], + 'description': 'Show raw configuration', + }, + { + 'args': [ + '--version', + '-v' + ], + 'description': 'Show version', + }, + { + 'title': 'Global configuration options' + }, + { + 'args': [ + '--profile', + '-p' + ], + 'option': 'PROFILE', + 'description': 'Print current profile or set profile' + }, + { + 'args': [ + '--env', + '-e' + ], + 'option': 'ENVIRONMENT', + 'description': 'Print current environment or set environment' + }, + { + 'args': [ + '--mail-enable' + ], + 'option': 'true|false', + 'description': 'Enable or disable mail reports' + }, + { + 'args': [ + '--get-mail-recipient' + ], + 'description': 'Get current mail recipient(s)' + }, + { + 'args': [ + '--set-mail-recipient' + ], + 'option': 'EMAIL', + 'description': 'Set mail recipient(s) (separated by commas)' + }, + { + 'title': 'Update options' + }, + { + 'args': [ + '--dist-upgrade', + '-du' + ], + 'description': 'Enable distribution upgrade when updating packages (Debian based OS only)' + }, + { + 'args': [ + '--keep-oldconf' + ], + 'description': 'Keep old configuration files when updating packages (Debian based OS only)' + }, + { + 'args': [ + '--assume-yes', + '-y' + ], + 'description': 'Answer yes to all questions' + }, + { + 'args': [ + '--check-updates', + '-cu' + ], + 'description': 'Only check for updates and exit' + }, + { + 'args': [ + '--ignore-exclude', + '-ie' + ], + 'description': 'Ignore all package exclusions' + }, + { + 'args': [ + '--get-update-method', + ], + 'description': 'Get current update method' + }, + { + 'args': [ + '--set-update-method', + ], + 'option': 'one_by_one|global', + 'description': 'Set update method: one_by_one (update packages one by one, one apt command executed for each package) or global (update all packages at once, one single apt command executed for all packages)' + }, + { + 'args': [ + '--exit-on-package-update-error', + ], + 'option': 'true|false', + 'description': 'When update method is one_by_one, immediately exit if an error occurs during package update and do not update the remaining packages' + }, + { + 'title': 'Packages exclusion and services restart' + }, + { + 'args': [ + '--get-exclude' + ], + 'description': 'Get the current list of packages to exclude from update' + }, + { + 'args': [ + '--get-exclude-major' + ], + 'description': 'Get the current list of packages to exclude from update (if package has a major version update)' + }, + { + 'args': [ + '--get-service-restart' + ], + 'description': 'Get the current list of services to restart after package update' + }, + { + 'args': [ + '--exclude' + ], + 'option': 'PACKAGE', + 'description': 'Set packages to exclude from update (separated by commas)' + }, + { + 'args': [ + '--exclude-major' + ], + 'option': 'PACKAGE', + 'description': 'Set packages to exclude from update (if package has a major version update) (separated by commas)' + }, + { + 'args': [ + '--service-restart' + ], + 'option': 'SERVICE', + 'description': 'Set services to restart after package update (separated by commas)' + }, + { + 'title': 'Modules' + }, + { + 'args': [ + '--mod-list' + ], + 'description': 'List available modules' + }, + { + 'args': [ + '--mod-enable' + ], + 'option': 'MODULE', + 'description': 'Enable a module' + }, + { + 'args': [ + '--mod-disable' + ], + 'option': 'MODULE', + 'description': 'Disable a module' + }, + ] + + # Add options to table + for option in options: + # If option is a title, just print it + if 'title' in option: + table.append(['', Style.BRIGHT + '\n' + option['title'] + '\n' + Style.RESET_ALL, '']) + continue + + # If option has multiple arguments, join them + if len(option['args']) > 1: + args_str = ', '.join(option['args']) + else: + args_str = option['args'][0] + + if 'option' in option: + args_str += Style.DIM + ' [' + option['option'] + ']' + Style.RESET_ALL + + table.append(['', args_str, option['description']]) + + + print(' Available options:', end='\n\n') + + # Print table + print(tabulate(table, headers=["", "Name", "Description"], tablefmt="simple"), end='\n\n') + + print(' Usage: linupdate [OPTIONS]', end='\n\n') + + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException('Printing help error: ' + str(e)) + except Exception as e: + raise ArgsException('Printing help error: ' + str(e)) diff --git a/src/controllers/ArgsException.py b/src/controllers/ArgsException.py new file mode 100644 index 0000000..4d5c196 --- /dev/null +++ b/src/controllers/ArgsException.py @@ -0,0 +1,9 @@ +# coding: utf-8 + +#----------------------------------------------------------------------------------------------- +# +# Custom exception class for arguments parsing errors +# +#----------------------------------------------------------------------------------------------- +class ArgsException(Exception): + pass diff --git a/src/controllers/Exit.py b/src/controllers/Exit.py new file mode 100644 index 0000000..9769173 --- /dev/null +++ b/src/controllers/Exit.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +# Import libraries +from colorama import Fore, Style + +# Import classes +from src.controllers.App.App import App +from src.controllers.App.Config import Config +from src.controllers.Mail import Mail + +class Exit: + #----------------------------------------------------------------------------------------------- + # + # Clean and exit + # + #----------------------------------------------------------------------------------------------- + def clean_exit(self, exit_code = 0, send_mail: bool = True, logfile: str = None): + my_app = App() + my_config = Config() + my_mail = Mail() + + if send_mail is True: + # Try to get mail settings + # It could fail if the config file is not found or if the + # mail section is not defined (e.g. first execution) + # So if it fails to retrieve configuration, just don't send the mail + try: + mail_enabled = my_config.get_mail_enabled() + mail_recipient = my_config.get_mail_recipient() + except Exception as e: + send_mail = False + mail_enabled = False + mail_recipient = None + + # Send mail unless send_mail is False + # (in some case mail is not needed, like when exiting at update confirmation) + if send_mail is True: + # Check if mail is enabled and recipient is set + if (mail_enabled and mail_recipient): + # Define mail subject depending on exit code + if exit_code == 0: + subject = '[ OK ] Packages update completed' + + if exit_code == 1: + subject = '[ ERROR ] Packages update failed' + + print('\n Sending email report:') + + try: + my_mail.send(subject, 'See report below or attached file.', mail_recipient, logfile) + print(' ' + Fore.GREEN + '✔' + Style.RESET_ALL + ' Email sent') + except Exception as e: + print(' ▪ ' + Fore.YELLOW + 'Error: ' + str(e) + Style.RESET_ALL) + # If mail fails, exit with error code + exit_code = 1 + + # Remove lock + my_app.remove_lock() + + # Final exit + exit(exit_code) diff --git a/src/controllers/HttpRequest.py b/src/controllers/HttpRequest.py new file mode 100644 index 0000000..45a2388 --- /dev/null +++ b/src/controllers/HttpRequest.py @@ -0,0 +1,112 @@ +# coding: utf-8 + +# Import libraries +from colorama import Fore, Style +import requests +import json + +# Import classes +from src.controllers.App.Utils import Utils + +class HttpRequest: + def __init__(self): + self.quiet = False + + #----------------------------------------------------------------------------------------------- + # + # GET request + # + #----------------------------------------------------------------------------------------------- + def get(self, url: str, id: str, token: str, connectionTimeout: int = 5, readTimeout: int = 3): + # If an Id and a token are provided, add them to the URL + if id != "" and token != "": + response = requests.get(url, + headers = {'Authorization': 'Host ' + id + ':' + token}, + timeout = (connectionTimeout, readTimeout)) + else: + response = requests.get(url, timeout = (connectionTimeout, readTimeout)) + + # Parse response and return results if 200 + return self.request_parse_result(response) + + + #----------------------------------------------------------------------------------------------- + # + # POST request with API key + # + #----------------------------------------------------------------------------------------------- + def post_token(self, url: str, apiKey: str, data, connectionTimeout: int = 5, readTimeout: int = 3): + # Send POST request to URL with API key + response = requests.post(url, + data = json.dumps(data), + headers = {'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json'}, + timeout = (connectionTimeout, readTimeout)) + + # Parse response and return results if 200 + return self.request_parse_result(response) + + + #----------------------------------------------------------------------------------------------- + # + # PUT request with Id and token + # + #----------------------------------------------------------------------------------------------- + def put(self, url: str, id: str, token: str, data, connectionTimeout: int = 5, readTimeout: int = 3): + # Send PUT request to URL with Id and token + response = requests.put(url, + data = json.dumps(data), + headers = {'Authorization': 'Host ' + id + ':' + token}, + timeout = (connectionTimeout, readTimeout)) + + # Parse response and return results if 200 + return self.request_parse_result(response) + + + #----------------------------------------------------------------------------------------------- + # + # DELETE request with Id and token + # + #----------------------------------------------------------------------------------------------- + def delete(self, url: str, id: str, token: str, connectionTimeout: int = 5, readTimeout: int = 3): + # Send DELETE request to URL with Id and token + response = requests.delete(url, + headers = {'Authorization': 'Host ' + id + ':' + token}, + timeout = (connectionTimeout, readTimeout)) + + # Parse response and return results if 200 + return self.request_parse_result(response) + + + #----------------------------------------------------------------------------------------------- + # + # Parse request result + # + #----------------------------------------------------------------------------------------------- + def request_parse_result(self, response): + myUtils = Utils() + + # Check is response is OK (200) + try: + response.raise_for_status() + + # Print response message if not quiet + if not self.quiet: + # If response is a JSON with a 'message' key, then print it + if myUtils.is_json(response.text) and 'message' in response.json(): + for message in response.json()['message']: + print(' ' + Fore.GREEN + '✔' + Style.RESET_ALL + ' ' + message) + + # If response is a JSON with a 'results' key, return it + if myUtils.is_json(response.text) and 'results' in response.json(): + return response.json()['results'] + + # Else return response + return response + except requests.exceptions.HTTPError as e: + # If response is a JSON with a 'message_error' key, return it + if myUtils.is_json(response.text) and 'message_error' in response.json(): + for message in response.json()['message_error']: + print(' ' + Fore.YELLOW + '✕' + Style.RESET_ALL + ' ' + message) + raise Exception() + else: + raise Exception('HTTP request error: ' + str(e)) diff --git a/src/controllers/Log.py b/src/controllers/Log.py new file mode 100644 index 0000000..b036bff --- /dev/null +++ b/src/controllers/Log.py @@ -0,0 +1,34 @@ +# coding: utf-8 + +import traceback +import sys + +# https://stackoverflow.com/a/57008553 + +# Context manager that copies stdout and any exceptions to a log file +class Log(object): + def __init__(self, filename): + self.file = open(filename, 'w') + self.stdout = sys.stdout + + def __enter__(self): + sys.stdout = self + + def __exit__(self, exc_type, exc_value, tb): + sys.stdout = self.stdout + if exc_type is not None: + self.file.write(traceback.format_exc()) + self.file.close() + + def write(self, data): + self.file.write(data) + self.stdout.write(data) + self.file.flush() + + def flush(self): + self.file.flush() + self.stdout.flush() + + def getContent(self): + return self.stdout + \ No newline at end of file diff --git a/src/controllers/Mail.py b/src/controllers/Mail.py new file mode 100644 index 0000000..cd5ec14 --- /dev/null +++ b/src/controllers/Mail.py @@ -0,0 +1,58 @@ +# coding: utf-8 + +# Import libraries +import re +import smtplib +import socket +from email.message import EmailMessage +from email.headerregistry import Address + +class Mail(): + #----------------------------------------------------------------------------------------------- + # + # Send email + # + #----------------------------------------------------------------------------------------------- + def send(self, subject: str, body_content: str, recipient: list, logfile = None): + msg = EmailMessage() + + # If logfile is set, then clean it from ANSI escape codes + if logfile: + # Read logfile content + with open(logfile, 'r') as f: + attach_content = f.read() + + # Get logfile real filename + attachment = logfile.split('/')[-1] + + # Replace ANSI escape codes + ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') + attach_content = ansi_escape.sub('', attach_content) + + # Define email content and headers + msg['Subject'] = subject + # debug only + # msg['From'] = Address('Linupdate', 'noreply', 'example.com') + msg['From'] = Address('Linupdate', 'noreply', socket.gethostname()) + msg['To'] = ','.join(recipient) + + # Retrieve HTML mail template + with open('/opt/linupdate/templates/mail/mail.template.html') as f: + template = f.read() + # Replace values in template + template = template.replace('__CONTENT__', body_content) + template = template.replace('__PRE_CONTENT__', attach_content) + + # Add HTML body + msg.add_alternative(template, subtype='html') + + # Add attachment if there is + if attach_content: + bs = attach_content.encode('utf-8') + msg.add_attachment(bs, maintype='text', subtype='plain', filename=attachment) + + # Send the message via our own SMTP server + s = smtplib.SMTP('localhost') + s.send_message(msg) + s.quit() + diff --git a/src/controllers/Module/Module.py b/src/controllers/Module/Module.py new file mode 100644 index 0000000..c1e52c8 --- /dev/null +++ b/src/controllers/Module/Module.py @@ -0,0 +1,231 @@ +# coding: utf-8 + +# Import libraries +import os +import importlib +import shutil +from pathlib import Path +from colorama import Fore, Style + +# Import classes +from src.controllers.App.Config import Config +from src.controllers.Exit import Exit + +class Module: + def __init__(self): + self.configController = Config() + self.exitController = Exit() + self.loadedModules = [] + + #----------------------------------------------------------------------------------------------- + # + # Return list of enabled modules + # + #----------------------------------------------------------------------------------------------- + def getEnabled(self): + return self.configController.get_conf()['modules']['enabled'] + + + #----------------------------------------------------------------------------------------------- + # + # List available modules + # + #----------------------------------------------------------------------------------------------- + def list(self): + # List all modules + print(' Available modules:') + + for module in os.listdir('/opt/linupdate/src/controllers/Module'): + # Ignore cache files + if module == '__pycache__': + continue + + # Ignore non directories + if not os.path.isdir('/opt/linupdate/src/controllers/Module/' + module): + continue + + print(Fore.GREEN + ' ▪ ' + module.lower()) + + print(Style.RESET_ALL, end='\n') + + + #----------------------------------------------------------------------------------------------- + # + # Enable a module + # + #----------------------------------------------------------------------------------------------- + def enable(self, module): + # Retrieve configuration + configuration = self.configController.get_conf() + + # Loop through modules (remove duplicates) + for mod in list(dict.fromkeys(module.split(','))): + # Check if module exists + if not self.exists(mod): + raise Exception('Module ' + mod + ' does not exist') + + # Continue if module is already enabled + if mod not in configuration['modules']['enabled']: + # Copy module default configuration file if it does not exist + if not Path('/etc/linupdate/modules/' + mod + '.yml').is_file(): + try: + shutil.copy2('/opt/linupdate/templates/modules/' + mod + '.template.yml', '/etc/linupdate/modules/' + mod + '.yml') + except Exception as e: + raise Exception('Could not generate module ' + mod + ' configuration file /etc/linupdate/modules/' + mod + '.yml: ' + str(e)) + + # Add enabled module in configuration + self.configController.append_module(mod) + + # Print enabled module + print(' Module ' + mod + Fore.GREEN + ' enabled' + Style.RESET_ALL) + + print('\n') + + + #----------------------------------------------------------------------------------------------- + # + # Disable a module + # + #----------------------------------------------------------------------------------------------- + def disable(self, module): + # Retrieve configuration + configuration = self.configController.get_conf() + + # Loop through modules (remove duplicates) + for mod in list(dict.fromkeys(module.split(','))): + # Check if module exists + if not self.exists(mod): + raise Exception('Module ' + mod + ' does not exist') + + # Disable module + if mod in configuration['modules']['enabled']: + self.configController.remove_module(mod) + + # Print disabled modules + print(' Module ' + mod + Fore.YELLOW + ' disabled' + Style.RESET_ALL) + + print('\n') + + + #----------------------------------------------------------------------------------------------- + # + # Configure a module + # + #----------------------------------------------------------------------------------------------- + def configure(self, module, args): + # Check if module exists + if not self.exists(module): + raise Exception('Module ' + module + ' does not exist') + + # Convert module name tu uppercase first letter + moduleName = module.capitalize() + + # Import python module class + moduleImportPath = importlib.import_module('src.controllers.Module.'+ moduleName + '.' + moduleName) + moduleClass = getattr(moduleImportPath, moduleName) + + # Instanciate module and call module load method + myModule = moduleClass() + myModule.load() + myModule.main(args) + + + #----------------------------------------------------------------------------------------------- + # + # Return True if module exists + # + #----------------------------------------------------------------------------------------------- + def exists(self, module): + # Check if module class file exists + if not os.path.exists('/opt/linupdate/src/controllers/Module/' + module.capitalize() + '/' + module.capitalize() + '.py'): + return False + + return True + + + #----------------------------------------------------------------------------------------------- + # + # Load enabled modules + # + #----------------------------------------------------------------------------------------------- + def load(self): + # Retrieve configuration + configuration = self.configController.get_conf() + + # Quit if no modules are enabled + if not configuration['modules']['enabled']: + return + + print(' Loading modules') + + # Loop through modules + for module in configuration['modules']['enabled']: + try: + # Convert module name tu uppercase first letter + moduleName = module.capitalize() + + # Import python module class + moduleImportPath = importlib.import_module('src.controllers.Module.'+ moduleName + '.' + moduleName) + moduleClass = getattr(moduleImportPath, moduleName) + + # Instanciate module and call module load method + myModule = moduleClass() + myModule.load() + + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + module + ' module loaded ') + + # Add module to the list of loaded modules + self.loadedModules.append(module) + + except Exception as e: + raise Exception('Could not load module ' + module + ': ' + str(e) + Style.RESET_ALL) + + + #----------------------------------------------------------------------------------------------- + # + # Execute modules pre-update actions (loaded modules only) + # + #----------------------------------------------------------------------------------------------- + def pre(self): + for module in self.loadedModules: + try: + print('\n Executing ' + Fore.YELLOW + module + Style.RESET_ALL + ' pre-update actions') + # Convert module name to uppercase first letter + moduleName = module.capitalize() + + # Import python module class + moduleImportPath = importlib.import_module('src.controllers.Module.'+ moduleName + '.' + moduleName) + moduleClass = getattr(moduleImportPath, moduleName) + + # Instanciate module and call module pre method + myModule = moduleClass() + myModule.pre() + + except Exception as e: + print(Fore.YELLOW + str(e) + Style.RESET_ALL) + raise Exception('Error while executing pre-update actions for module ' + module) + + + #----------------------------------------------------------------------------------------------- + # + # Execute modules post-update actions + # + #----------------------------------------------------------------------------------------------- + def post(self, updateSummary): + for module in self.loadedModules: + try: + print('\n Executing ' + Fore.YELLOW + module + Style.RESET_ALL + ' post-update actions') + # Convert module name to uppercase first letter + moduleName = module.capitalize() + + # Import python module class + moduleImportPath = importlib.import_module('src.controllers.Module.'+ moduleName + '.' + moduleName) + moduleClass = getattr(moduleImportPath, moduleName) + + # Instanciate module and call module post method + myModule = moduleClass() + myModule.post(updateSummary) + + except Exception as e: + print(Fore.YELLOW + str(e) + Style.RESET_ALL) + raise Exception('Error while executing post-update actions for module ' + module) diff --git a/src/controllers/Module/Reposerver/Agent.py b/src/controllers/Module/Reposerver/Agent.py new file mode 100644 index 0000000..4ada234 --- /dev/null +++ b/src/controllers/Module/Reposerver/Agent.py @@ -0,0 +1,331 @@ +# coding: utf-8 + +# Import libraries +from colorama import Fore, Style +import time +import pyinotify +import threading +import websocket +import json +import sys +from pathlib import Path + +# Import classes +from src.controllers.Module.Module import Module +from src.controllers.Module.Reposerver.Status import Status +from src.controllers.Module.Reposerver.Config import Config +from src.controllers.Package.Package import Package + +class Agent: + def __init__(self): + self.moduleController = Module() + self.configController = Config() + self.reposerverStatusController = Status() + self.packageController = Package() + + #----------------------------------------------------------------------------------------------- + # + # General checks for running the agent + # + #----------------------------------------------------------------------------------------------- + def run_general_checks(self): + enabled_modules = self.moduleController.getEnabled() + + # Checking that reposerver module is enabled, if not quit (but with error 0 because it could have been disabled on purpose) + if 'reposerver' not in enabled_modules: + print('[reposerver-agent] Reposerver module is disabled, quitting.') + sys.exit(0) + + # Retrieving configuration + self.configuration = self.configController.get_conf() + + # Checking that reposerver URL is set, if not quit + if not self.configuration['reposerver']['url']: + print('[reposerver-agent] Reposerver URL is not set. Quitting.') + sys.exit(1) + + # Check that this host has an auth id and token + if not self.configuration['client']['auth']['id'] or not self.configuration['client']['auth']['token']: + print('[reposerver-agent] No authentication id and token set, is this host registered to a Repomanager server? Quitting.') + sys.exit(1) + + # Checking that reposerver agent is enabled, if not quit (but with error 0 because it could have been disabled on purpose) + if not self.configuration['agent']['enabled']: + print('[reposerver-agent] Reposerver agent is disabled. Quitting.') + sys.exit(0) + + # Checking that a log file exists for yum/dnf or apt + if Path('/var/log/yum.log').is_file(): + self.log_file = '/var/log/yum.log' + elif Path('/var/log/dnf.log').is_file(): + self.log_file = '/var/log/dnf.log' + elif Path('/var/log/apt/history.log').is_file(): + self.log_file = '/var/log/apt/history.log' + else: + raise Exception('no log file found for yum/dnf or apt') + + + #----------------------------------------------------------------------------------------------- + # + # Run functions when an inotify event is detected + # + #----------------------------------------------------------------------------------------------- + def on_inotify_change(self, ev): + # If latest event was less than 120 seconds ago, then do not send again the history + if self.last_inotify_event_time and time.time() - self.last_inotify_event_time < 120: + return + + # Define new last event time + self.last_inotify_event_time = time.time() + + # /var/log/dnf.log issue: wait before sending the history because it might have + # been triggered by another history sending (from a request from the Repomanager server for example) as + # even the 'dnf history' command is logged in the dnf.log file + # So just wait a bit to don't send both history at the same time... + if self.log_file == '/var/log/dnf.log': + time.sleep(15) + + # Send the history + print('[reposerver-agent] New event has been detected in ' + self.log_file + ' - sending history to the Repomanager server.') + self.reposerverStatusController.send_packages_history() + + + #----------------------------------------------------------------------------------------------- + # + # Run inotify process to scan for package events + # + #----------------------------------------------------------------------------------------------- + def run_inotify_package_event(self): + print('[reposerver-agent] Starting package event monitoring from ' + self.log_file) + + try: + # Set inotify process as running to prevent multiple processes from running + self.inotify_is_running = True + self.last_inotify_event_time = None + + watch_manager = pyinotify.WatchManager() + # TODO: to test when there are multiple events at once in the log file + # quiet=False => raise Exception + # watch_manager.add_watch(self.log_file, pyinotify.IN_MODIFY, self.on_inotify_change) + watch_manager.add_watch(self.log_file, pyinotify.IN_CLOSE_WRITE, self.on_inotify_change, quiet=False) + notifier = pyinotify.Notifier(watch_manager) + notifier.loop() + + # If an exception is raised, then set inotify process as not running and store the exception to + # be read by the main function (cannot raise an exception to be read by the main function, it does not work, so store it instead) + except pyinotify.WatchManagerError as e: + self.inotify_is_running = False + self.inotify_exception = str(e) + except Exception as e: + self.inotify_is_running = False + self.inotify_exception = str(e) + + + #----------------------------------------------------------------------------------------------- + # + # On message received from the websocket + # + #----------------------------------------------------------------------------------------------- + def websocket_on_message(self, ws, message): + # Decode JSON message + message = json.loads(message) + request_id = None + summary = None + + # If the message contains 'request' + if 'request' in message: + try: + # Retrieve request Id if any (authenticate request does not have an id) + if 'request-id' in message: + request_id = message['request-id'] + + # Case the request is 'authenticate', then authenticate to the reposerver + if message['request'] == 'authenticate': + print('[reposerver-agent] Authenticating to the reposerver') + + id = self.configuration['client']['auth']['id'] + token = self.configuration['client']['auth']['token'] + + # Send a response to authenticate to the reposerver, with id and token + self.websocket.send(json.dumps({'response-to-request': {'request': 'authenticate', 'auth-id': id, 'token': token}})) + + # Case the request is 'request-general-infos', then send general informations to the reposerver + elif message['request'] == 'request-general-infos': + print('[reposerver-agent] Reposerver requested general informations') + self.reposerverStatusController.send_general_info() + + # Case the request is 'request-packages-infos', then send packages informations to the reposerver + elif message['request'] == 'request-packages-infos': + print('[reposerver-agent] Reposerver requested packages informations') + self.reposerverStatusController.send_packages_info() + + # Case the request is 'update-all-packages', then update all packages + elif message['request'] == 'update-all-packages': + print('[reposerver-agent] Reposerver requested all packages update') + self.packageController.update() + # Send a summary to the reposerver, with the summary of the update (number of packages updated or failed) + summary = self.packageController.summary + else: + raise Exception('unknown request sent by reposerver: ' + message['request']) + + # If there was a request id, then send a response to reposerver to make the request as completed + if request_id: + # If there is a summary to send, then send it + if summary: + self.websocket.send(json.dumps({'response-to-request': {'request-id': request_id, 'status': 'completed', 'summary': summary}})) + else: + self.websocket.send(json.dumps({'response-to-request': {'request-id': request_id, 'status': 'completed'}})) + + # If request failed + except Exception as e: + print('[reposerver-agent] Error: ' + str(e)) + + # If there was a request id, then send a response to reposerver to make the request as failed + if request_id: + self.websocket.send(json.dumps({'response-to-request': {'request-id': request_id, 'status': 'failed', 'error': str(e)}})) + + # If the message contains 'info' + if 'info' in message: + print('[reposerver-agent] Received info message from reposerver: ' + message['info']) + + # If the message contains 'error' + if 'error' in message: + print('[reposerver-agent] Received error message from reposerver: ' + message['error']) + + + #----------------------------------------------------------------------------------------------- + # + # On error from the websocket + # + #----------------------------------------------------------------------------------------------- + def websocket_on_error(self, ws, error): + raise Exception('Websocket error: ' + str(error)) + + + #----------------------------------------------------------------------------------------------- + # + # On websocket connection closed + # + #----------------------------------------------------------------------------------------------- + def websocket_on_close(self, ws, close_status_code, close_msg): + print('[reposerver-agent] Reposerver websocket connection closed with status code: ' + str(close_status_code) + ' and message: ' + close_msg) + raise Exception('reposerver websocket connection closed') + + + #----------------------------------------------------------------------------------------------- + # + # On websocket connection opened + # + #----------------------------------------------------------------------------------------------- + def websocket_on_open(self, ws): + print('[reposerver-agent] Opening connection with reposerver') + + + #----------------------------------------------------------------------------------------------- + # + # Start websocket client + # + #----------------------------------------------------------------------------------------------- + def websocket_client(self): + try: + # Replace http by ws, or https by wss + reposerver_ws_url = self.configuration['reposerver']['url'].replace('http', 'ws').replace('https', 'wss') + + # Set websocket process as running to prevent multiple processes from running + self.websocket_is_running = True + + # Set to True for debugging + websocket.enableTrace(False) + + # Open websocket connection + # Using lambda to pass arguments to the functions, this is necessary for older versions of python websocket + self.websocket = websocket.WebSocketApp(reposerver_ws_url + '/ws', + on_open=lambda ws: self.websocket_on_open(ws), + on_message=lambda ws, message: self.websocket_on_message(ws, message), + on_error=lambda ws, error: self.websocket_on_error(ws, error), + on_close=lambda ws, close_status_code, close_msg: self.websocket_on_close(ws, close_status_code, close_msg)) + + # Clean version but not working with older versions of python websocket: + # Open websocket connection + # self.websocket = websocket.WebSocketApp(reposerver_ws_url + '/ws', + # on_open=self.websocket_on_open, + # on_message=self.websocket_on_message, + # on_error=self.websocket_on_error, + # on_close=self.websocket_on_close) + + # Run websocket + # self.websocket.on_open = self.websocket_on_open + self.websocket.run_forever() + + except KeyboardInterrupt: + self.websocket_is_running = False + self.websocket_exception = str(e) + except Exception as e: + self.websocket_is_running = False + self.websocket_exception = str(e) + + + #----------------------------------------------------------------------------------------------- + # + # Reposerver agent main function + # + #----------------------------------------------------------------------------------------------- + def main(self): + counter = 0 + self.child_processes = [] + self.child_processes_started = [] + self.inotify_is_running = False + self.inotify_exception = None + self.websocket_is_running = False + self.websocket_exception = None + + # Checking that all the necessary elements are present for the agent execution + self.run_general_checks() + + # Executing regular tasks + while True: + # Checking that all the necessary elements are present for the agent execution. + # This is checked every time in case that a configuration change has been made in the configuration file + self.run_general_checks() + + # Regulary sending data to the Repomanager server (every hour) + # 3600 / 5sec (sleep 5) = 720 + if counter == 0 or counter == 720: + # Sending full status + print('[reposerver-agent] Periodically sending informations about this host to the repomanager server') + self.reposerverStatusController.send_general_info() + self.reposerverStatusController.send_packages_info() + + # Reset counter + counter = 0 + + # If no inotify process is running, then execute it in background + if not self.inotify_is_running: + try: + # If there was an exception in the last inotify process, then raise it + if self.inotify_exception: + raise Exception(self.inotify_exception) + + thread = threading.Thread(target=self.run_inotify_package_event) + thread.daemon = True + thread.start() + except Exception as e: + raise Exception('package event monitoring failed: ' + str(e)) + + # If agent listening is enabled, open websocket + if self.configuration['agent']['listen']['enabled']: + if not self.websocket_is_running: + try: + # If there was an exception in the last websocket process, then raise it + if self.websocket_exception: + raise Exception(self.websocket_exception) + + thread = threading.Thread(target=self.websocket_client) + thread.daemon = True + thread.start() + except Exception as e: + raise Exception('reposerver websocket connection failed: ' + str(e)) + + time.sleep(5) + + counter+=1 diff --git a/src/controllers/Module/Reposerver/Args.py b/src/controllers/Module/Reposerver/Args.py new file mode 100644 index 0000000..520c267 --- /dev/null +++ b/src/controllers/Module/Reposerver/Args.py @@ -0,0 +1,394 @@ +# coding: utf-8 + +# Import libraries +from colorama import Fore, Style +from tabulate import tabulate +import argparse + +# Import classes +from src.controllers.Exit import Exit +from src.controllers.Module.Reposerver.Config import Config +from src.controllers.Module.Reposerver.Status import Status +from src.controllers.ArgsException import ArgsException + +class Args: + def __init__(self): + self.exitController = Exit() + self.configController = Config() + + #----------------------------------------------------------------------------------------------- + # + # Parse arguments + # + #----------------------------------------------------------------------------------------------- + def parse(self, module_args): + try: + # Parse arguments + parser = argparse.ArgumentParser(add_help=False) + + # Define valid arguments + # Help + parser.add_argument("--help", "-h", action="store_true", default="null") + # URL + parser.add_argument("--url", action="store", nargs='?', default="null") + # API key + parser.add_argument("--api-key", action="store", nargs='?', default="null") + # IP + parser.add_argument("--ip", action="store", nargs='?', default="null") + + # Enable or disable packages configuration update + parser.add_argument("--get-packages-conf-from-reposerver", action="store", nargs='?', default="null") + # Enable or disable repos update + parser.add_argument("--get-repos-from-reposerver", action="store", nargs='?', default="null") + # Enable or disable the removing of existing repos + parser.add_argument("--remove-existing-repos", action="store", nargs='?', default="null") + + # Agent enable + parser.add_argument("--agent-enable", action="store", nargs='?', default="null") + # Agent listen enable + parser.add_argument("--agent-listen-enable", action="store", nargs='?', default="null") + + # Register to reposerver + parser.add_argument("--register", action="store_true", default="null") + # Unregister from server + parser.add_argument("--unregister", action="store_true", default="null") + + # Retrieve profile packages configuration from reposerver + parser.add_argument("--get-profile-packages-conf", action="store_true", default="null") + # Retrieve profile repositories from reposerver + parser.add_argument("--get-profile-repos", action="store_true", default="null") + + # Send general info + parser.add_argument("--send-general-info", action="store_true", default="null") + # Send packages status + parser.add_argument("--send-packages-info", action="store_true", default="null") + # Send all info + parser.add_argument("--send-all-info", action="store_true", default="null") + + # If no arguments are passed, print help + if not module_args: + self.help() + self.exitController.clean_exit(0, False) + + # Else, parse arguments + args, remaining_args = parser.parse_known_args(module_args) + + # If there are remaining arguments, print help and raise an exception + if remaining_args: + self.help() + raise ArgsException('Unknown argument(s): ' + str(remaining_args)) + + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException(str(e)) + except Exception as e: + raise ArgsException(str(e)) + + try: + # + # If --url param has been set + # + if args.url != "null": + # If a URL is set (not 'None'), change the app URL + if args.url: + # Set new URL + self.configController.setUrl(args.url) + + # Print URL change + print(' Reposerver URL set to ' + Fore.GREEN + args.url + Style.RESET_ALL, end='\n\n') + # Else print the current URL + else: + print(' Current reposerver URL: ' + Fore.GREEN + self.configController.getUrl() + Style.RESET_ALL, end='\n\n') + self.exitController.clean_exit(0, False) + + # + # If --api-key param has been set + # + if args.api_key != "null": + Args.api_key = args.api_key + + # + # If --ip param has been set + # + if args.ip != "null": + Args.ip = args.ip + + # + # If --agent-enable param has been set + # + if args.agent_enable != "null": + if args.agent_enable == 'true': + self.configController.set_agent_enable(True) + else: + self.configController.set_agent_enable(False) + + self.exitController.clean_exit(0, False) + + # + # If --agent-listen-enable param has been set + # + if args.agent_listen_enable != "null": + if args.agent_listen_enable == 'true': + self.configController.set_agent_listen(True) + else: + self.configController.set_agent_listen(False) + + self.exitController.clean_exit(0, False) + + # + # If --get-packages-conf-from-reposerver param has been set + # + if args.get_packages_conf_from_reposerver != "null": + if args.get_packages_conf_from_reposerver == 'true': + self.configController.set_get_packages_conf_from_reposerver(True) + else: + self.configController.set_get_packages_conf_from_reposerver(False) + self.exitController.clean_exit(0, False) + + # + # If --get-repos-from-reposerver param has been set + # + if args.get_repos_from_reposerver != "null": + if args.get_repos_from_reposerver == 'true': + self.configController.set_get_repos_from_reposerver(True) + else: + self.configController.set_get_repos_from_reposerver(False) + self.exitController.clean_exit(0, False) + + # + # If --remove-existing-repos param has been set + # + if args.remove_existing_repos != "null": + if args.remove_existing_repos == 'true': + self.configController.set_remove_existing_repos(True) + else: + self.configController.set_remove_existing_repos(False) + self.exitController.clean_exit(0, False) + + # + # If --register param has been set + # + if args.register != "null" and args.register: + # Register to the URL with the API key and IP (could be "null" if not set) + self.configController.register(args.api_key, args.ip) + self.exitController.clean_exit(0, False) + + # + # If --unregister param has been set + # + if args.unregister != "null" and args.unregister: + # Unregister from the reposerver + self.configController.unregister() + self.exitController.clean_exit(0, False) + + # + # If --get-profile-packages-conf param has been set + # + if args.get_profile_packages_conf != "null" and args.get_profile_packages_conf: + # Get profile packages configuration + self.configController.get_profile_packages_conf() + self.exitController.clean_exit(0, False) + + # + # If --get-profile-repos param has been set + # + if args.get_profile_repos != "null" and args.get_profile_repos: + # Get profile repositories + self.configController.get_profile_repos() + self.exitController.clean_exit(0, False) + + # + # If --send-general-info param has been set + # + if args.send_general_info != "null" and args.send_general_info: + # Send general info + status = Status() + status.send_general_info() + self.exitController.clean_exit(0, False) + + # + # If --send-packages-info param has been set + # + if args.send_packages_info != "null" and args.send_packages_info: + # Send packages info + status = Status() + status.send_packages_info() + self.exitController.clean_exit(0, False) + + # + # If --send-all-info param has been set + # + if args.send_all_info != "null" and args.send_all_info: + # Send full status including general status, available packages status, installed packages status and full history + status = Status() + status.send_general_info() + status.send_packages_info() + self.exitController.clean_exit(0, False) + + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException(str(e)) + except Exception as e: + raise ArgsException(str(e)) + + #----------------------------------------------------------------------------------------------- + # + # Print help + # + #----------------------------------------------------------------------------------------------- + def help(self): + try: + table = [] + options = [ + { + 'args': [ + '--help', + '-h' + ], + 'description': 'Show reposerver module help', + }, + { + 'title': 'Configuring reposerver' + }, + { + 'args': [ + '--url', + ], + 'option': 'URL', + 'description': 'Specify target reposerver URL', + }, + { + 'args': [ + '--api-key', + ], + 'option': 'APIKEY', + 'description': 'Specify API key to authenticate to the reposerver', + }, + { + 'args': [ + '--ip', + ], + 'option': 'IP', + 'description': 'Specify an alternative local IP address to use to authenticate to the reposerver (default: will use the public IP address)', + }, + { + 'args': [ + '--register', + ], + 'description': 'Register this host to the reposerver (--api-key required)' + }, + { + 'args': [ + '--unregister', + ], + 'description': 'Unregister this host from the reposerver' + }, + { + 'title': 'Configuring retrieval from reposerver (using configured profile)' + }, + { + 'args': [ + '--get-packages-conf-from-reposerver', + ], + 'option': 'true|false', + 'description': 'If enabled, packages exclusions will be retrieved from the reposerver', + }, + { + 'args': [ + '--get-repos-from-reposerver', + ], + 'option': 'true|false', + 'description': 'If enabled, repositories configuration will be retrieved from the reposerver', + }, + { + 'args': [ + '--remove-existing-repos', + ], + 'option': 'true|false', + 'description': 'If enabled, existing repositories will be removed before adding the new ones', + }, + { + 'title': 'Retrieving data from reposerver' + }, + { + 'args': [ + '--get-profile-packages-conf', + ], + 'description': 'Get profile packages configuration from reposerver' + }, + { + 'args': [ + '--get-profile-repos', + ], + 'description': 'Get profile repositories from reposerver' + }, + { + 'title': 'Sending data to reposerver' + }, + { + 'args': [ + '--send-general-info', + ], + 'description': 'Send host\'s general informations (OS, version, kernel..) to the reposerver' + }, + { + 'args': [ + '--send-packages-info', + ], + 'description': 'Send this host\'s packages informations to the reposerver (available package updates, installed packages)' + }, + { + 'args': [ + '--send-all-info', + ], + 'description': 'Send all of the previous informations to the reposerver' + }, + { + 'args': [ + '--agent-enable', + ], + 'option': 'true|false', + 'description': 'Enable reposerver module agent. This agent will regularly send informations about this host to reposerver (global informations, packages informations...)', + }, + { + 'args': [ + '--agent-listen-enable', + ], + 'option': 'true|false', + 'description': 'Enable or disable agent listening for requests coming from the reposerver', + } + ] + + # Add options to table + for option in options: + # If option is a title, just print it + if 'title' in option: + table.append(['', Style.BRIGHT + '\n' + option['title'] + '\n' + Style.RESET_ALL, '']) + continue + + # If option has multiple arguments, join them + if len(option['args']) > 1: + args_str = ', '.join(option['args']) + else: + args_str = option['args'][0] + + if 'option' in option: + args_str += Style.DIM + ' [' + option['option'] + ']' + Style.RESET_ALL + + table.append(['', args_str, option['description']]) + + + print(' Available options:', end='\n\n') + + # Print table + print(tabulate(table, headers=["", "Name", "Description"], tablefmt="simple"), end='\n\n') + + print(' Usage: linupdate --mod-configure reposerver [OPTIONS]', end='\n\n') + # Catch exceptions + # Either ArgsException or Exception, it will always raise an ArgsException to the main script, this to avoid sending an email when an argument error occurs + except ArgsException as e: + raise ArgsException('Printing help error: ' + str(e)) + except Exception as e: + raise ArgsException('Printing help error: ' + str(e)) diff --git a/src/controllers/Module/Reposerver/Config.py b/src/controllers/Module/Reposerver/Config.py new file mode 100644 index 0000000..3fabf72 --- /dev/null +++ b/src/controllers/Module/Reposerver/Config.py @@ -0,0 +1,668 @@ +# coding: utf-8 + +# Import libraries +from pathlib import Path +from colorama import Fore, Style +import yaml +import ipaddress +import shutil +import socket + +# Import classes +from src.controllers.System import System +from src.controllers.App.Config import Config as appConfig +from src.controllers.Package.Package import Package +from src.controllers.HttpRequest import HttpRequest +from src.controllers.Yaml import Yaml + +class Config: + def __init__(self): + self.config_file = '/etc/linupdate/modules/reposerver.yml' + self.systemController = System() + self.appConfigController = appConfig() + self.packageController = Package() + self.httpRequestController = HttpRequest() + + #----------------------------------------------------------------------------------------------- + # + # Generate reposerver config file if not exist + # + #----------------------------------------------------------------------------------------------- + def generate_conf(self): + # Generate configuration file if not exists + if not Path(self.config_file).is_file(): + # Copy default configuration file + try: + shutil.copy2('/opt/linupdate/templates/modules/reposerver.template.yml', '/etc/linupdate/modules/reposerver.yml') + except Exception as e: + raise Exception('error while generating reposerver configuration file ' + self.config_file + ': ' + str(e)) + + # TODO: to remove in some time + # If old linupdate (bash version) config file exists, migrate it + if Path('/etc/linupdate/modules/reposerver.conf').is_file(): + self.migrate_conf() + + + #----------------------------------------------------------------------------------------------- + # + # Return current reposerver URL + # + #----------------------------------------------------------------------------------------------- + def getUrl(self): + # Get current configuration + configuration = self.get_conf() + + # Check if url exists in configuration and is not empty + if 'url' not in configuration['reposerver']: + raise Exception('reposerver URL not found in ' + self.config_file) + + if configuration['reposerver']['url'] == '': + raise Exception('no reposerver URL set. Please set a URL with --url option') + + # Return URL + return configuration['reposerver']['url'] + + + #----------------------------------------------------------------------------------------------- + # + # Set reposerver URL + # + #----------------------------------------------------------------------------------------------- + def setUrl(self, url): + # Check that url is valid (start with http(s)://) + if not url.startswith('http://') and not url.startswith('https://'): + raise Exception('reposerver URL must start with http:// or https://') + + # Get current configuration + configuration = self.get_conf() + + # Remove final slash if exists + if url.endswith('/'): + url = url[:-1] + + # Set url + configuration['reposerver']['url'] = url + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Return reposerver configuration + # + #----------------------------------------------------------------------------------------------- + def get_conf(self): + # Checking that a configuration file exists for reposerver module + if not Path(self.config_file).is_file(): + raise Exception('reposerver configuration file ' + self.config_file + ' does not exist') + + # Open YAML config file + with open(self.config_file, 'r') as stream: + try: + # Read YAML and return profile + return yaml.safe_load(stream) + + except yaml.YAMLError as e: + raise Exception('error while reading reposerver configuration file ' + self.config_file + ': ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Write reposerver configuration to config file + # + #----------------------------------------------------------------------------------------------- + def write_conf(self, configuration): + yaml = Yaml() + + try: + yaml.write(configuration, self.config_file) + except Exception as e: + raise Exception('error while writing to reposerver configuration file ' + self.config_file + ': ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Check if the config file exists and if it contains the required parameters + # + #----------------------------------------------------------------------------------------------- + def check_conf(self): + if not Path(self.config_file).is_file(): + raise Exception('reposerver module configuration file not found ' + self.config_file) + + # Retrieve configuration + configuration = self.get_conf() + + # Check if reposerver is set + if 'reposerver' not in configuration: + raise Exception('reposerver key not found in ' + self.config_file) + + # Check if reposerver.url is set + if 'url' not in configuration['reposerver']: + raise Exception('reposerver.url key not found in ' + self.config_file) + + # Check if client is set + if 'client' not in configuration: + raise Exception('client key not found in ' + self.config_file) + + # Check if client.auth is set + if 'auth' not in configuration['client']: + raise Exception('client.auth key not found in ' + self.config_file) + + # Check if client.auth.id is set + if 'id' not in configuration['client']['auth']: + raise Exception('client.auth.id key not found in ' + self.config_file) + + # Check if client.auth.token is set + if 'token' not in configuration['client']['auth']: + raise Exception('client.auth.token key not found in ' + self.config_file) + + # Check if client.get_packages_conf_from_reposerver is set + if 'get_packages_conf_from_reposerver' not in configuration['client']: + raise Exception('client.get_packages_conf_from_reposerver key not found in ' + self.config_file) + + # Check if client.get_packages_conf_from_reposerver.enabled is set + if 'enabled' not in configuration['client']['get_packages_conf_from_reposerver']: + raise Exception('client.get_packages_conf_from_reposerver.enabled key not found in ' + self.config_file) + + # Check if client.get_packages_conf_from_reposerver.enabled is set (True or False) + if configuration['client']['get_packages_conf_from_reposerver']['enabled'] not in [True, False]: + raise Exception('client.get_packages_conf_from_reposerver.enabled key must be set to true or false') + + # Check if client.get_repos_from_reposerver is set + if 'get_repos_from_reposerver' not in configuration['client']: + raise Exception('client.get_repos_from_reposerver key not found in ' + self.config_file) + + # Check if client.get_repos_from_reposerver.enabled is set + if 'enabled' not in configuration['client']['get_repos_from_reposerver']: + raise Exception('client.get_repos_from_reposerver.enabled key not found in ' + self.config_file) + + # Check if client.get_repos_from_reposerver.enabled is set (True or False) + if configuration['client']['get_repos_from_reposerver']['enabled'] not in [True, False]: + raise Exception('client.get_repos_from_reposerver.enabled key must be set to true or false') + + # Check if client.get_repos_from_reposerver.remove_existing_repos is set + if 'remove_existing_repos' not in configuration['client']['get_repos_from_reposerver']: + raise Exception('client.get_repos_from_reposerver.remove_existing_repos key not found in ' + self.config_file) + + # Check if client.get_repos_from_reposerver.remove_existing_repos is set (True or False) + if configuration['client']['get_repos_from_reposerver']['remove_existing_repos'] not in [True, False]: + raise Exception('client.get_repos_from_reposerver.remove_existing_repos key must be set to true or false') + + # Check if agent is set + if 'agent' not in configuration: + raise Exception('agent key not found in ' + self.config_file) + + # Check if agent.enabled is set and is set (True or False) + if 'enabled' not in configuration['agent']: + raise Exception('agent.enabled key not found in ' + self.config_file) + + # Check if agent.enabled is set and is set (True or False) + if configuration['agent']['enabled'] not in [True, False]: + raise Exception('agent.enabled key must be set to true or false') + + # Check if agent.listen is set + if 'listen' not in configuration['agent']: + raise Exception('agent.listen key not found in ' + self.config_file) + + # Check if agent.listen.enabled is set + if 'enabled' not in configuration['agent']['listen']: + raise Exception('agent.listen.enabled key not found in ' + self.config_file) + + # Check if agent.listen.enabled is set (True or False) + if configuration['agent']['listen']['enabled'] not in [True, False]: + raise Exception('agent.listen.enabled key must be set to true or false') + + + #----------------------------------------------------------------------------------------------- + # + # Register to reposerver + # + #----------------------------------------------------------------------------------------------- + def register(self, api_key: str, ip: str): + # Get Reposerver URL + url = self.getUrl() + + # Check if URL is not null + if url == '': + raise Exception('you must configure the target reposerver URL [--url ]') + + print(' Registering to ' + url + ':') + + # Check if API key is not null + if api_key == 'null': + raise Exception('you must specify an API key from a Repomanager user account [--register --api-key ]') + + # If no IP has been specified (null), then retrieve the public IP of the host + if ip == 'null': + try: + ip = self.httpRequestController.get('https://api.ipify.org', '', '', 2).text + except Exception as e: + raise Exception('failed to retrieve public IP from https://api.ipify.org (resource might be temporarily unavailable): ' + str(e)) + + # Check that the IP is valid + try: + ipaddress.ip_address(ip) + except ValueError: + raise Exception('invalid IP address ' + ip) + + # Register to server using API key and IP (POST) + data = { + 'ip': ip, + 'hostname': socket.gethostname() + } + + try: + results = self.httpRequestController.post_token(url + '/api/v2/host/registering', api_key, data) + except Exception as e: + raise Exception('Registering failed') + + # If registration is successful, the server will return an Id and a token, set Id and token in configuration + self.setId(results['id']) + self.setToken(results['token']) + + + #----------------------------------------------------------------------------------------------- + # + # Unregister from reposerver + # + #----------------------------------------------------------------------------------------------- + def unregister(self): + # Get Reposerver URL + url = self.getUrl() + + # Check if URL is not null + if url == '': + raise Exception('you must configure the target Reposerver URL [--url ]') + + print(' Unregistering from ' + Fore.GREEN + url + Style.RESET_ALL + ':') + + # Get Id and token from configuration + id = self.getId() + token = self.getToken() + + # Check if Id and token are not null + if id == '': + raise Exception('no auth Id found in configuration') + + if token == '': + raise Exception('no auth token found in configuration') + + try: + # Unregister from server using Id and token (DELETE) + self.httpRequestController.delete(url + '/api/v2/host/registering', id, token) + except Exception as e: + raise Exception('Unregistering failed') + + + #----------------------------------------------------------------------------------------------- + # + # Enable or disable configuration update from reposerver + # + #----------------------------------------------------------------------------------------------- + def set_get_packages_conf_from_reposerver(self, value: bool): + # Get current configuration + configuration = self.get_conf() + + # Set get_packages_conf_from_reposerver + configuration['client']['get_packages_conf_from_reposerver']['enabled'] = value + + # Write config file + self.write_conf(configuration) + + if value == True: + print(' Retrieving packages configuration from reposerver is ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + print(' Retrieving packages configuration from reposerver is ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + + + #----------------------------------------------------------------------------------------------- + # + # Enable or disable repositories configuration update from reposerver + # + #----------------------------------------------------------------------------------------------- + def set_get_repos_from_reposerver(self, value: bool): + # Get current configuration + configuration = self.get_conf() + + # Set get_repos_from_reposerver + configuration['client']['get_repos_from_reposerver']['enabled'] = value + + # Write config file + self.write_conf(configuration) + + if value == True: + print(' Retrieving repositories configuration from reposerver is ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + print(' Retrieving repositories configuration from reposerver is ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + + + #----------------------------------------------------------------------------------------------- + # + # Set remove existing repositories + # + #----------------------------------------------------------------------------------------------- + def set_remove_existing_repos(self, value: bool): + # Get current configuration + configuration = self.get_conf() + + # Set remove_existing_repos + configuration['client']['get_repos_from_reposerver']['remove_existing_repos'] = value + + # Write config file + self.write_conf(configuration) + + if value == True: + print(' Removing existing repositories is ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + print(' Removing existing repositories is ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + + + #----------------------------------------------------------------------------------------------- + # + # Get authentication id + # + #----------------------------------------------------------------------------------------------- + def getId(self): + # Get current configuration + configuration = self.get_conf() + + # Return Id + return configuration['client']['auth']['id'] + + + #----------------------------------------------------------------------------------------------- + # + # Set authentication id + # + #----------------------------------------------------------------------------------------------- + def setId(self, id: str): + # Get current configuration + configuration = self.get_conf() + + # Set Id + configuration['client']['auth']['id'] = id + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Get authentication token + # + #----------------------------------------------------------------------------------------------- + def getToken(self): + # Get current configuration + configuration = self.get_conf() + + # Return Token + return configuration['client']['auth']['token'] + + + #----------------------------------------------------------------------------------------------- + # + # Set authentication token + # + #----------------------------------------------------------------------------------------------- + def setToken(self, token: str): + # Get current configuration + configuration = self.get_conf() + + # Set Token + configuration['client']['auth']['token'] = token + + # Write config file + self.write_conf(configuration) + + + #----------------------------------------------------------------------------------------------- + # + # Get profile packages configuration (exclusions) from reposerver + # + #----------------------------------------------------------------------------------------------- + def get_profile_packages_conf(self): + # Get current configuration + configuration = self.get_conf() + + # Get reposerver URL + url = self.getUrl() + + # Get current profile, auth Id and token + profile = self.appConfigController.get_profile() + id = self.getId() + token = self.getToken() + + print(' ▪ Getting ' + Fore.YELLOW + profile + Style.RESET_ALL + ' profile packages configuration:', end=' ') + + # Check if getting profile packages configuration from reposerver is enabled + if configuration['client']['get_packages_conf_from_reposerver']['enabled'] == False: + print(Fore.YELLOW + 'Disabled' + Style.RESET_ALL) + return + + # Check if profile is not empty + if not profile: + raise Exception('no profile set. Please set a profile with --profile option') + + # Check if Id and token are not empty + if not id or not token: + raise Exception('no auth Id or token found in configuration') + + # Retrieve configuration from reposerver + results = self.httpRequestController.get(url + '/api/v2/profile/' + profile + '/excludes', id, token, 2) + + # Parse results + + # Packages to exclude no matter the version + if results[0]['Package_exclude'] != "null": + # First, clear the exclude list + self.appConfigController.set_exclusion() + + # Then, set the new exclude list + self.appConfigController.set_exclusion(results[0]['Package_exclude']) + + # Packages to exclude on major version + if results[0]['Package_exclude_major'] != "null": + # First, clear the exclude major list + self.appConfigController.set_major_exclusion() + + # Then, set the new exclude major list + self.appConfigController.set_major_exclusion(results[0]['Package_exclude_major']) + + # Service to restart after an update + if results[0]['Service_restart'] != "null": + # First clear the services to restart + self.appConfigController.set_service_to_restart() + + # Then set the new services to restart + self.appConfigController.set_service_to_restart(results[0]['Service_restart']) + + print('[' + Fore.GREEN + ' OK ' + Style.RESET_ALL + ']') + + + #----------------------------------------------------------------------------------------------- + # + # Get profile repositories configuration from reposerver + # + #----------------------------------------------------------------------------------------------- + def get_profile_repos(self): + # Get current configuration + configuration = self.get_conf() + + # Get reposerver URL + url = self.getUrl() + + # Get current profile, auth Id and token + profile = self.appConfigController.get_profile() + env = self.appConfigController.get_environment() + id = self.getId() + token = self.getToken() + + print(' ▪ Getting ' + Fore.YELLOW + profile + Style.RESET_ALL + ' profile repositories:', end=' ') + + # Check if getting profile packages configuration from reposerver is enabled + if configuration['client']['get_repos_from_reposerver']['enabled'] == False: + print(Fore.YELLOW + 'Disabled' + Style.RESET_ALL) + return + + # Check if profile is not empty + if not profile: + raise Exception('no profile set. Please set a profile with --profile option') + + # Check if environment is not empty + if not env: + raise Exception('no environment set. Please set an environment with --env option') + + # Check if Id and token are not empty + if not id or not token: + raise Exception('no auth Id or token found in configuration') + + # Retrieve configuration from reposerver + results = self.httpRequestController.get(url + '/api/v2/profile/' + profile + '/repos', id, token, 2) + + # Parse results + + # Quit if no results + if not results: + print(Fore.YELLOW + 'No repositories configured ' + Style.RESET_ALL) + return + + # Remove current repositories files if enabled + if configuration['client']['get_repos_from_reposerver']['remove_existing_repos']: + # Debian + if self.systemController.get_os_family() == 'Debian': + # Clear /etc/apt/sources.list + with open('/etc/apt/sources.list', 'w') as file: + file.write('') + + # Delete all files in /etc/apt/sources.list.d + for file in Path('/etc/apt/sources.list.d/').glob('*.list'): + file.unlink() + + # Redhat + if self.systemController.get_os_family() == 'Redhat': + # Delete all files in /etc/yum.repos.d + for file in Path('/etc/yum.repos.d/').glob('*.repo'): + file.unlink() + + # Create each repo file + for result in results: + # Depending on the OS family, the repo files are stored in different directories + + # Debian + if self.systemController.get_os_family() == 'Debian': + reposRoot = '/etc/apt/sources.list.d' + + # Redhat + if self.systemController.get_os_family() == 'Redhat': + reposRoot = '/etc/yum.repos.d' + + # Insert description at the top of the file, then content + # Replace __ENV__ with current environment on the fly + with open(reposRoot + '/' + result['filename'], 'w') as file: + # Insert description + file.write('# ' + result['description'] + '\n' + result['content'].replace('__ENV__', env) + '\n') + + # Set permissions + Path(reposRoot + '/' + result['filename']).chmod(0o660) + + # Clear cache + self.packageController.clear_cache() + + print('[' + Fore.GREEN + ' OK ' + Style.RESET_ALL + ']') + + + #----------------------------------------------------------------------------------------------- + # + # Enable or disable agent + # + #----------------------------------------------------------------------------------------------- + def set_agent_enable(self, value: bool): + try: + # Get current configuration + configuration = self.get_conf() + + # Set agent enable + configuration['agent']['enabled'] = value + + # Write config file + self.write_conf(configuration) + + if value: + print(' Reposerver agent ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + print(' Reposerver agent ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + except Exception as e: + raise Exception('could not set agent enable to ' + str(value) + ': ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Enable or disable agent listening + # + #----------------------------------------------------------------------------------------------- + def set_agent_listen(self, value: bool): + try: + # Get current configuration + configuration = self.get_conf() + + # Set agent listening enable + configuration['agent']['listen']['enabled'] = value + + # Write config file + self.write_conf(configuration) + + if value: + print(' Agent listening ' + Fore.GREEN + 'enabled' + Style.RESET_ALL, end='\n\n') + else: + print(' Agent listening ' + Fore.YELLOW + 'disabled' + Style.RESET_ALL, end='\n\n') + except Exception as e: + raise Exception('could not set agent listening enable to ' + str(value) + ': ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Migration of old reposerver configuration file + # + #----------------------------------------------------------------------------------------------- + def migrate_conf(self): + # Old config file are like ini file + + print(' Detected old configuration file /etc/linupdate/modules/reposerver.conf, migrating...') + + try: + # Open old config file + with open('/etc/linupdate/modules/reposerver.conf', 'r') as file: + lines = file.readlines() + + for line in lines: + # If url is set + if 'URL' in line: + url = line.split('=')[1].replace('"', '').strip() + self.setUrl(url) + + # If id is set + if 'ID' in line: + id = line.split('=')[1].replace('"', '').strip() + self.setId(id) + + # If token is set + if 'TOKEN' in line: + token = line.split('=')[1].replace('"', '').strip() + self.setToken(token) + + # If get_packages_conf_from_reposerver is set + if 'GET_PROFILE_PKG_CONF_FROM_REPOSERVER' in line: + get_packages_conf_from_reposerver = line.split('=')[1].replace('"', '').strip() + self.set_get_packages_conf_from_reposerver(True if get_packages_conf_from_reposerver == 'true' else False) + + # If get_repos_from_reposerver is set + if 'GET_PROFILE_REPOS_FROM_REPOSERVER' in line: + get_repos_from_reposerver = line.split('=')[1].replace('"', '').strip() + self.set_get_repos_from_reposerver(True if get_repos_from_reposerver == 'true' else False) + + # Move old file + Path('/etc/linupdate/modules/reposerver.conf').rename('/etc/linupdate/modules/reposerver.conf.migrated') + + except Exception as e: + raise Exception('error while migrating old reposerver configuration file: ' + str(e)) diff --git a/src/controllers/Module/Reposerver/Reposerver.py b/src/controllers/Module/Reposerver/Reposerver.py new file mode 100644 index 0000000..da0247e --- /dev/null +++ b/src/controllers/Module/Reposerver/Reposerver.py @@ -0,0 +1,75 @@ +# coding: utf-8 + +# Import classes +from src.controllers.Module.Reposerver.Config import Config as Config +from src.controllers.Module.Reposerver.Status import Status +from src.controllers.Module.Reposerver.Args import Args + +class Reposerver: + def __init__(self): + self.configController = Config() + self.argsController = Args() + + + #----------------------------------------------------------------------------------------------- + # + # Load Reposerver module + # + #----------------------------------------------------------------------------------------------- + def load(self): + # Note: no need of try / except block here, as it is already handled in the Module load() function + + # Generate config file if not exist + self.configController.generate_conf() + + # Check config file + self.configController.check_conf() + + + #----------------------------------------------------------------------------------------------- + # + # Execute pre-update actions + # + #----------------------------------------------------------------------------------------------- + def pre(self): + # Note: no need of try / except block here, as it is already handled in the Module pre() function + + # Retrieve profile configuration from reposerver + self.configController.get_profile_packages_conf() + + # Retrieve profile repositories from reposerver + self.configController.get_profile_repos() + + + #----------------------------------------------------------------------------------------------- + # + # Execute post-update actions + # + #----------------------------------------------------------------------------------------------- + def post(self, updateSummary): + # Note: no need of try / except block here, as it is already handled in the Module pre() function + + # Quit if there was no packages updates + if updateSummary['update']['status'] == 'nothing-to-do': + print(' ▪ Nothing to do as no packages have been updated') + return + + # Generaly "*-release" packages on Redhat/CentOS are resetting .repo files. So it is better to retrieve them again from the reposerver + self.configController.get_profile_repos() + + # Send last 4 packages history entries to the reposerver + status = Status() + status.send_packages_history() + + + #----------------------------------------------------------------------------------------------- + # + # Reposerver main function + # + #----------------------------------------------------------------------------------------------- + def main(self, module_args): + try: + # Parse reposerver arguments + self.argsController.parse(module_args) + except Exception as e: + raise Exception(str(e)) diff --git a/src/controllers/Module/Reposerver/Status.py b/src/controllers/Module/Reposerver/Status.py new file mode 100644 index 0000000..1c65954 --- /dev/null +++ b/src/controllers/Module/Reposerver/Status.py @@ -0,0 +1,237 @@ +# coding: utf-8 + +# Import libraries +from colorama import Fore, Style +import socket + +# Import classes +from src.controllers.System import System +from src.controllers.App.App import App +from src.controllers.App.Config import Config +from src.controllers.Module.Reposerver.Config import Config as ReposerverConfig +from src.controllers.Exit import Exit +from src.controllers.Package.Package import Package +from src.controllers.HttpRequest import HttpRequest + +class Status: + def __init__(self): + self.systemController = System() + self.appController = App() + self.configController = Config() + self.reposerverConfigController = ReposerverConfig() + self.httpRequestController = HttpRequest() + self.packageController = Package() + self.exitController = Exit() + + + #----------------------------------------------------------------------------------------------- + # + # Send general status + # + #----------------------------------------------------------------------------------------------- + def send_general_info(self): + try: + # Retrieve URL, ID and token + url = self.reposerverConfigController.getUrl() + id = self.reposerverConfigController.getId() + token = self.reposerverConfigController.getToken() + + data = { + 'hostname': socket.gethostname(), + 'os_family': self.systemController.get_os_family(), + 'os': self.systemController.get_os_name(), + 'os_version': self.systemController.get_os_version(), + 'type': self.systemController.get_virtualization(), + 'kernel': self.systemController.get_kernel(), + 'arch': self.systemController.get_arch(), + 'profile': self.configController.get_profile(), + 'env': self.configController.get_environment(), + 'agent_status': self.appController.get_agent_status(), + 'linupdate_version': self.appController.get_version(), + 'reboot_required': str(self.systemController.reboot_required()).lower() # Convert True/False to 'true'/'false' + } + + print('\n Sending general informations to ' + Fore.YELLOW + url + Style.RESET_ALL + ':') + + except Exception as e: + raise Exception('could not build general status data: ' + str(e)) + + try: + self.httpRequestController.quiet = False + self.httpRequestController.put(url + '/api/v2/host/status', id, token, data) + except Exception as e: + raise Exception('error while sending general status to reposerver: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Send all packages status + # + #----------------------------------------------------------------------------------------------- + def send_packages_info(self): + try: + # Send all status + self.send_packages_history() + self.sendAvailablePackagesStatus() + self.sendInstalledPackagesStatus() + except Exception as e: + raise Exception('error while sending packages status to reposerver: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Send list of available packages + # + #----------------------------------------------------------------------------------------------- + def sendAvailablePackagesStatus(self): + # Retrieve URL, ID and token + url = self.reposerverConfigController.getUrl() + id = self.reposerverConfigController.getId() + token = self.reposerverConfigController.getToken() + + available_packages = 'none' + + print('\n Building available packages list...') + + try: + # Retrieve available packages + packages = self.packageController.get_available_packages(True) + + if len(packages) > 0: + available_packages = '' + + for package in packages: + name = package['name'] + available_version = package['available_version'] + + # Ignore package if name is empty + if name == '': + continue + + # Redhat only + if self.systemController.get_os_family() == 'Redhat': + # Remove epoch if it is equal to 0 + if available_version.startswith('0:'): + available_version = available_version[2:] + + # Add package name, its available version to the available_packages string + available_packages += name + '|' + available_version + ',' + + # Remove last comma + available_packages = available_packages[:-1] + + # Convert to JSON + data = { + 'available_packages': available_packages + } + + except Exception as e: + # Raise an exception to be caught in the main function + raise Exception('error while retrieving available packages: ' + str(e)) + + # Send available packages to Reposerver + print(' Sending available packages to ' + Fore.YELLOW + url + Style.RESET_ALL + ':') + + self.httpRequestController.quiet = False + self.httpRequestController.put(url + '/api/v2/host/packages/available', id, token, data) + + + #----------------------------------------------------------------------------------------------- + # + # Send list of installed packages + # + #----------------------------------------------------------------------------------------------- + def sendInstalledPackagesStatus(self): + # Retrieve URL, ID and token + url = self.reposerverConfigController.getUrl() + id = self.reposerverConfigController.getId() + token = self.reposerverConfigController.getToken() + + installed_packages = '' + + print('\n Building installed packages list...') + + try: + # Retrieve installed packages + packages = self.packageController.get_installed_packages() + + if len(packages) > 0: + for package in packages: + name = package['name'] + version = package['version'] + + # Ignore package if name or version is empty + if name == '' or version == '': + continue + + # Add package name, its available version to the installed_packages string + installed_packages += name + '|' + version + ',' + + # Remove last comma + installed_packages = installed_packages[:-1] + + # Convert to JSON + data = { + 'installed_packages': installed_packages + } + + except Exception as e: + # Raise an exception to set status to 'error' + raise Exception('error while retrieving installed packages: ' + str(e)) + + # Send installed packages to Reposerver + print(' Sending installed packages to ' + Fore.YELLOW + url + Style.RESET_ALL + ':') + + self.httpRequestController.quiet = False + self.httpRequestController.put(url + '/api/v2/host/packages/installed', id, token, data) + + + #----------------------------------------------------------------------------------------------- + # + # Send packages history (installed, removed, upgraded, downgraded, etc.) + # + #----------------------------------------------------------------------------------------------- + def send_packages_history(self, entries_limit: int = 999999): + # Retrieve URL, ID and token + url = self.reposerverConfigController.getUrl() + id = self.reposerverConfigController.getId() + token = self.reposerverConfigController.getToken() + + # History parsing will start from the oldest to the newest + history_order = 'oldest' + + print('\n Building packages history...') + + # If limit is set (not the default 999999), history parsing will start from the newest to the oldest + if entries_limit != 999999: + history_order = 'newest' + + try: + # Retrieve history Ids or files + history_entries = self.packageController.get_history(history_order) + except Exception as e: + raise Exception('error while retrieving history: ' + str(e)) + + # If there is no item (would be strange), exit + if len(history_entries) == 0: + print(' no history found') + return + + # Parse history files / Ids + try: + events = {} + events['events'] = self.packageController.parse_history(history_entries, entries_limit) + + # debug only: print pretty json + # import json + # r = json.dumps(events) + # json_object = json.loads(r) + # json_formatted_str = json.dumps(json_object, indent=2) + # print(json_formatted_str) + except Exception as e: + raise Exception('could not parse packages history: ' + str(e)) + + print(' Sending packages events to ' + Fore.YELLOW + url + Style.RESET_ALL + ':') + + self.httpRequestController.quiet = False + self.httpRequestController.put(url + '/api/v2/host/packages/event', id, token, events) diff --git a/src/controllers/Module/Reposerver/__init__.py b/src/controllers/Module/Reposerver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/Package/Apt.py b/src/controllers/Package/Apt.py new file mode 100644 index 0000000..9792c1f --- /dev/null +++ b/src/controllers/Package/Apt.py @@ -0,0 +1,553 @@ +# coding: utf-8 + +# Import libraries +import apt +import subprocess +import glob +import os +import re +import sys +from colorama import Fore, Style + +class Apt: + def __init__(self): + # Create an instance of the apt cache + self.aptcache = apt.Cache() + + # self.aptcache.update() + self.aptcache.open(None) + + # Total count of success and failed package updates + self.summary = { + 'update': { + 'success': { + 'count': 0, + 'packages': [] + }, + 'failed': { + 'count': 0, + 'packages': [] + } + } + } + + # Define some default options + self.dist_upgrade = False + self.keep_oldconf = True + + + #----------------------------------------------------------------------------------------------- + # + # Return list of installed apt packages, sorted by name + # + #----------------------------------------------------------------------------------------------- + def get_installed_packages(self): + list = [] + + try: + # Loop through all installed packages + for pkg in self.aptcache: + # If the package is installed, add it to the list of installed packages + if pkg.is_installed: + list.append({ + 'name': pkg.name, + 'version': pkg.installed.version, + }) + + # Sort the list by package name + list.sort(key=lambda x: x['name']) + + except Exception as e: + raise Exception('could not get installed packages: ' + str(e)) + + return list + + + #----------------------------------------------------------------------------------------------- + # + # Return list of available apt packages, sorted by name + # + #----------------------------------------------------------------------------------------------- + def get_available_packages(self): + try: + list = [] + + # Simulate an upgrade + # self.aptcache.upgrade() + + # Loop through all packages marked for upgrade + for pkg in self.aptcache.get_changes(): + # If the package is upgradable, add it to the list of available packages + if pkg.is_upgradable: + myPackage = { + 'name': pkg.name, + 'current_version': pkg.installed.version, + 'available_version': pkg.candidate.version + } + + list.append(myPackage) + + # Sort the list by package name + list.sort(key=lambda x: x['name']) + + except Exception as e: + raise Exception('could not get available packages: ' + str(e)) + + return list + + + #----------------------------------------------------------------------------------------------- + # + # Clear apt cache + # + #----------------------------------------------------------------------------------------------- + def clear_cache(self): + result = subprocess.run( + ["apt", "clean", "all"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + raise Exception('could not clear apt cache: ' + result.stderr) + + + #----------------------------------------------------------------------------------------------- + # + # Update apt cache + # + #----------------------------------------------------------------------------------------------- + def update_cache(self, dist_upgrade: bool = False): + try: + if dist_upgrade: + self.aptcache.upgrade(True) + else: + self.aptcache.upgrade() + + except Exception as e: + raise Exception('could not update apt cache: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Get list of excluded packages (hold) + # + #----------------------------------------------------------------------------------------------- + def get_exclusion(self): + result = subprocess.run( + ["apt-mark", "showhold"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + raise Exception('could not get excluded (holded) packages list:' + result.stderr) + + return result.stdout.splitlines() + + + #----------------------------------------------------------------------------------------------- + # + # Exclude (hold) specified package + # + #----------------------------------------------------------------------------------------------- + def exclude(self, package): + result = subprocess.run( + ["apt-mark", "hold", package], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + raise Exception('could not exclude ' + package + ' package from update: ' + result.stderr) + + + #----------------------------------------------------------------------------------------------- + # + # Remove all package exclusions (unhold) + # + #----------------------------------------------------------------------------------------------- + def remove_all_exclusions(self): + # Retrieve the list of packages on hold + list = self.get_exclusion() + + # Quit if there are no packages on hold + if list == '': + return + + # Unhold all packages + for package in list: + result = subprocess.run( + ["apt-mark", "unhold", package], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + raise Exception('could not unhold packages: ' + package) + + + #----------------------------------------------------------------------------------------------- + # + # Update packages + # + #----------------------------------------------------------------------------------------------- + def update(self, packagesList, update_method: str = 'one_by_one', exit_on_package_update_error: bool = True): + # If update_method is 'one_by_one', update packages one by one (one command per package) + if update_method == 'one_by_one': + # Loop through the list of packages to update + for pkg in packagesList: + # If the package is excluded, ignore it + if pkg['excluded']: + continue + + print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') + + # Before updating, check If package is already in the latest version, if so, skip it + # It means that it has been updated previously by another package, probably because it was a dependency + # Get the current version of the package from apt cache. Use a new temporary apt cache to be sure it is up to date + try: + temp_apt_cache = apt.Cache() + temp_apt_cache.open(None) + + # If version in cache is the same the target version, skip the update + if temp_apt_cache[pkg['name']].installed.version == pkg['available_version']: + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated by a previous package).') + + # Mark the package as already updated + self.summary['update']['success']['count'] += 1 + continue + except Exception as e: + raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + str(e)) + + # If --keep-oldconf is True, then keep the old configuration file + if self.keep_oldconf: + cmd = [ + 'apt-get', 'install', pkg['name'], '-y', + '-o', 'Dpkg::Options::=--force-confdef', + '-o', 'Dpkg::Options::=--force-confold' + ] + else: + cmd = ['apt-get', 'install', pkg['name'], '-y'] + + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) + + # Print lines as they are read + for line in popen.stdout: + # Deal with carriage return + parts = line.split('\r') + # for part in parts[:-1]: + # sys.stdout.write('\r' + ' | ' + part.strip() + '\n') + # sys.stdout.flush() + buffer = parts[-1] + sys.stdout.write('\r' + ' | ' + buffer.strip() + '\n') + sys.stdout.flush() + + # Wait for the command to finish + popen.wait() + + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # Add the package to the list of failed packages + self.summary['update']['failed']['count'] += 1 + + # If error is critical, raise an exception + if (exit_on_package_update_error == True): + raise Exception('Error while updating ' + pkg['name'] + '.') + + # Else print an error message and continue to the next package + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') + continue + + # Close the pipe + popen.stdout.close() + + # If command succeeded, increment the success counter + self.summary['update']['success']['count'] += 1 + + # Print a success message + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') + + # If update_method is 'global', update all packages at once (one command) + if update_method == 'global': + # If --keep-oldconf is True, then keep the old configuration file + if self.keep_oldconf: + cmd = [ + 'apt-get', 'upgrade', '-y', + '-o', 'Dpkg::Options::=--force-confdef', + '-o', 'Dpkg::Options::=--force-confold' + ] + else: + cmd = ['apt-get', 'upgrade', '-y'] + + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + + # Print lines as they are read + for line in popen.stdout: + # Deal with carriage return + parts = line.split('\r') + # for part in parts[:-1]: + # sys.stdout.write('\r' + ' | ' + part.strip() + '\n') + # sys.stdout.flush() + buffer = parts[-1] + sys.stdout.write('\r' + ' | ' + buffer.strip() + '\n') + sys.stdout.flush() + + # Wait for the command to finish + popen.wait() + + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # If error is critical, raise an exception + if (exit_on_package_update_error == True): + raise Exception('Error while updating packages.') + + # Else print an error message + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error.') + + else: + # Print a success message + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + 'Done.') + + # Close the pipe + popen.stdout.close() + + + #----------------------------------------------------------------------------------------------- + # + # Return apt history log files sorted by modification time + # + #----------------------------------------------------------------------------------------------- + def get_history(self, order): + try: + files = sorted(glob.glob('/var/log/apt/history.log*'), key=os.path.getmtime) + + # If order is newest, then sort by date in ascending order + if order == 'newest': + files.reverse() + except Exception as e: + raise Exception('could not get apt history log files: ' + str(e)) + + return files + + + #----------------------------------------------------------------------------------------------- + # + # Parse all apt history log files and return a list of events (JSON) + # + #----------------------------------------------------------------------------------------------- + def parse_history(self, history_files: list, entries_limit: int): + # Initialize a limit counter which will be incremented until it reaches the entries_limit + limit_counter = 0 + + # Initialize a list of events + events = [] + + # Parse each apt history files + for history_file in history_files: + # Retrieve all Start-Date in the history file + result = subprocess.run( + ['zgrep "^Start-Date:*" ' + history_file], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True, # Alias of 'text = True' + shell = True + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could not retrieve Start-Date from ' + history_file + ': ' + result.stderr) + + # Split the result into a list + start_dates = result.stdout.strip().split('\n') + + for start_date in start_dates: + # Reset all variables for each event + installed_packages = '' + upgraded_packages = '' + removed_packages = '' + purged_packages = '' + downgraded_packages = '' + reinstalled_packages = '' + installed_packages_json = '' + upgraded_packages_json = '' + removed_packages_json = '' + purged_packages_json = '' + downgraded_packages_json = '' + reinstalled_packages_json = '' + + # Quit if the limit of entries to send has been reached + if limit_counter > entries_limit: + break + + # Retrieve the event block : from the start date (START_DATE) to the next empty line + # If the file is compressed, we must use zcat to read it + if history_file.endswith('.gz'): + result = subprocess.run( + ['zcat ' + history_file + ' | sed -n "/' + start_date + '/,/^$/p"'], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True, # Alias of 'text = True' + shell = True + ) + + # If the file is not compressed, we can use sed directly + else: + result = subprocess.run( + ['sed -n "/' + start_date + '/,/^$/p" ' + history_file], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True, # Alias of 'text = True' + shell = True + ) + + if result.returncode != 0: + raise Exception('could not retrieve event for ' + start_date + ' in ' + history_file + ': ' + result.stderr) + + # Retrieve event block lines + event = result.stdout.strip() + + # From the retrieved event block, we can get the start date and time and the end date and time + # If the event block does not contain a Start-Date or an End-Date, ignore the event (the event might be + # incomplete because the log file was being written at the same time) + if not re.search(r'^Start-Date: (.+)', event): + continue + if not re.search(r'End-Date: (.+)', event): + continue + + date_start = re.search(r'^Start-Date: (.+)', event).group(1).split()[0].strip() + time_start = re.search(r'^Start-Date: (.+)', event).group(1).split()[1].strip() + date_end = re.search(r'End-Date: (.+)', event).group(1).split()[0].strip() + time_end = re.search(r'End-Date: (.+)', event).group(1).split()[1].strip() + + # Retrieve the event command, ignore event if no command found + if not re.search(r'Commandline: (.+)', event): + continue + + command = re.search(r'Commandline: (.+)', event).group(1).strip() + + # Retrieve packages installed, removed, upgraded, downgraded, etc. + if re.search(r'Install: (.+)', event): + installed_packages = re.search(r'Install: (.+)', event).group(1).strip() + if re.search(r'Upgrade: (.+)', event): + upgraded_packages = re.search(r'Upgrade: (.+)', event).group(1).strip() + if re.search(r'Remove: (.+)', event): + removed_packages = re.search(r'Remove: (.+)', event).group(1).strip() + if re.search(r'Purge: (.+)', event): + purged_packages = re.search(r'Purge: (.+)', event).group(1).strip() + if re.search(r'Downgrade: (.+)', event): + downgraded_packages = re.search(r'Downgrade: (.+)', event).group(1).strip() + if re.search(r'Reinstall: (.+)', event): + reinstalled_packages = re.search(r'Reinstall: (.+)', event).group(1).strip() + + # Parse packages lists and convert them to JSON + if installed_packages != '': + installed_packages_json = self.parse_packages_line_to_json(installed_packages, 'install') + + if upgraded_packages != '': + upgraded_packages_json = self.parse_packages_line_to_json(upgraded_packages, 'upgrade') + + if removed_packages != '': + removed_packages_json = self.parse_packages_line_to_json(removed_packages, 'remove') + + if purged_packages != '': + purged_packages_json = self.parse_packages_line_to_json(purged_packages, 'purge') + + if downgraded_packages != '': + downgraded_packages_json = self.parse_packages_line_to_json(downgraded_packages, 'downgrade') + + if reinstalled_packages != '': + reinstalled_packages_json = self.parse_packages_line_to_json(reinstalled_packages, 'reinstall') + + # Create the event JSON object + event = { + 'date_start': date_start, + 'time_start': time_start, + 'date_end': date_end, + 'time_end': time_end, + 'command': command + } + + if installed_packages_json != '': + event['installed'] = installed_packages_json + + if upgraded_packages_json != '': + event['upgraded'] = upgraded_packages_json + + if removed_packages_json != '': + event['removed'] = removed_packages_json + + if purged_packages_json != '': + event['purged'] = purged_packages_json + + if downgraded_packages_json != '': + event['downgraded'] = downgraded_packages_json + + if reinstalled_packages_json != '': + event['reinstalled'] = reinstalled_packages_json + + # Add the event to the list of events + events.append(event) + + limit_counter += 1 + + return events + + + #----------------------------------------------------------------------------------------------- + # + # Parse a string of one or multiple package(s) into a list of JSON objects + # + #----------------------------------------------------------------------------------------------- + def parse_packages_line_to_json(self, packages: str, action: str): + packages_json = [] + + # If there is more than one package on the same line + # e.g. + # libc6-i386:amd64 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8), libc6:amd64 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8), libc6:i386 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8), libc-dev-bin:amd64 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8), libc6-dbg:amd64 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8), libc6-dev:amd64 (2.35-0ubuntu3.7, 2.35-0ubuntu3.8) + if re.search(r"\),", packages): + # Split all the packages from the same line into a list + packages = re.sub(r"\),", "\n", packages).split('\n') + + # Else if there is only one package on the same line, just split the line into a list + else: + packages = packages.split('\n') + + # For all packages in the list, retrieve the name and the version + for package in packages: + # First, remove extra spaces + package = package.strip() + + # Then, split the package into name and version + name = package.split(' ')[0] + + # Depending on the action, the version to retrieve is on a different position + if action == 'install' or action == 'remove' or action == 'purge' or action == 'reinstall': + version = package.split(' ')[1] + if action == 'upgrade' or action == 'downgrade': + version = package.split(' ')[2] + + # Remove parenthesis, commas, colons and spaces from name and version + for char in ['(', ')', ',', ' ']: + name = name.replace(char, '') + version = version.replace(char, '') + + # Also remove architecture from name + for arch in [':amd64', ':i386', ':all', ':arm64', ':armhf', ':armel', ':ppc64el', ':s390x', ':mips', ':mips64el', ':mipsel', ':powerpc', ':powerpcspe', ':riscv64', ':s390', ':sparc', ':sparc64']: + if arch in name: + name = name.replace(arch, '') + + # Add the package to the list of packages + packages_json.append({ + 'name': name, + 'version': version + }) + + # Return the list of packages as JSON + return packages_json diff --git a/src/controllers/Package/Dnf.py b/src/controllers/Package/Dnf.py new file mode 100644 index 0000000..dd7e8fe --- /dev/null +++ b/src/controllers/Package/Dnf.py @@ -0,0 +1,637 @@ +# coding: utf-8 + +# Import libraries +import os +import subprocess +import time +import re +from colorama import Fore, Style +from dateutil import parser as dateutil_parser +import configparser + +class Dnf: + def __init__(self): + # Total count of success and failed package updates + self.summary = { + 'update': { + 'success': { + 'count': 0, + 'packages': [] + }, + 'failed': { + 'count': 0, + 'packages': [] + } + } + } + + #----------------------------------------------------------------------------------------------- + # + # Return list of installed apt packages, sorted by name + # + #----------------------------------------------------------------------------------------------- + def get_installed_packages(self): + list = [] + + # Get list of installed packages + # e.g. dnf repoquery -q -a --qf="%{name} %{version}-%{release}.%{arch} %{repoid}" --upgrades + result = subprocess.run( + ["dnf", "repoquery", "--installed", "-a", "--qf=%{name} %{epoch}:%{version}-%{release}.%{arch}"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could not get installed packages: ' + result.stderr) + + try: + # Split all lines and parse them + for line in result.stdout.split('\n'): + if line == '': + continue + + # Split package name and version + # e.g: zlib-devel 0:1.2.11-41.el9.x86_64 + package = line.split() + + name = package[0] + version = package[1] + + # Remove epoch if it is equal to 0 + if version.startswith('0:'): + version = version[2:] + + list.append({ + 'name': name, + 'version': version + }) + + # Sort the list by package name + list.sort(key=lambda x: x['name']) + + except Exception as e: + raise Exception('could not get installed packages: ' + str(e)) + + return list + + + #----------------------------------------------------------------------------------------------- + # + # Return list of available dnf packages, sorted by name + # + #----------------------------------------------------------------------------------------------- + def get_available_packages(self): + list = [] + + # Get list of packages to update sorted by name + # e.g. dnf repoquery --upgrades --latest-limit 1 -q -a --qf="%{name} %{version}-%{release}.%{arch} %{repoid}" + result = subprocess.run( + ["dnf", "repoquery", "--upgrades", "--latest-limit", "1", "-a", "--qf=%{name} %{version}-%{release}.%{arch} %{repoid}"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could not retrieve available packages list: ' + result.stderr) + + # Split all lines and parse them + for line in result.stdout.split('\n'): + if line == '': + continue + + package = line.split(' ') + + # Retrieve current version with dnf + # e.g. rpm -q --qf="%{version}-%{release}.%{arch}" + result = subprocess.run( + ["rpm", "-q", "--qf=%{version}-%{release}.%{arch}", package[0]], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could not retrieve current version of package ' + package[0] + ': ' + result.stderr) + + current_version = result.stdout.strip() + + list.append({ + 'name': package[0], + 'current_version': current_version, + 'available_version': package[1], + 'repository': package[2] + }) + + return list + + + #----------------------------------------------------------------------------------------------- + # + # Clear dnf cache + # + #----------------------------------------------------------------------------------------------- + def clear_cache(self): + # Check if dnf lock is present + self.check_lock + + result = subprocess.run( + ["dnf", "clean", "all"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('Error while clearing dnf cache: ' + result.stderr) + + + #----------------------------------------------------------------------------------------------- + # + # Update dnf cache + # + #----------------------------------------------------------------------------------------------- + def update_cache(self, useless_dist_upgrade: bool = False): + # Useless because dnf update command already updates the cache + return + + + #----------------------------------------------------------------------------------------------- + # + # Get list of excluded packages + # + #----------------------------------------------------------------------------------------------- + def get_exclusion(self): + # Get dnf.conf file content + try: + # Parse the content of dnf.conf, it's like a ini file + dnf_config = configparser.ConfigParser() + dnf_config.read('/etc/dnf/dnf.conf') + except Exception as e: + raise Exception('could not retrieve /etc/dnf/dnf.conf content: ' + str(e)) + + # If exclude is not present in the file, return an empty list + if not dnf_config.has_option('main', 'exclude'): + return [] + + # Else return the list of excluded packages + return dnf_config.get('main', 'exclude').split(' ') + + + #----------------------------------------------------------------------------------------------- + # + # Exclude specified package + # + #----------------------------------------------------------------------------------------------- + def exclude(self, package): + # Get dnf.conf file content + try: + # Parse the content of dnf.conf, it's like a ini file + dnf_config = configparser.ConfigParser() + dnf_config.read('/etc/dnf/dnf.conf') + except Exception as e: + raise Exception('could not retrieve /etc/dnf/dnf.conf content: ' + str(e)) + + # If exclude is not present in the file, add it + if not dnf_config.has_option('main', 'exclude'): + dnf_config.set('main', 'exclude', '') + + # Add the package to the list of excluded packages + dnf_config.set('main', 'exclude', dnf_config.get('main', 'exclude') + ' ' + package) + + # Write the new content to the file + try: + with open('/etc/dnf/dnf.conf', 'w') as configfile: + dnf_config.write(configfile) + except Exception as e: + raise Exception('could not write to /etc/dnf/dnf.conf: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Remove all package exclusions + # + #----------------------------------------------------------------------------------------------- + def remove_all_exclusions(self): + # Get dnf.conf file content + try: + # Parse the content of dnf.conf, it's like a ini file + dnf_config = configparser.ConfigParser() + dnf_config.read('/etc/dnf/dnf.conf') + except Exception as e: + raise Exception('could not retrieve /etc/dnf/dnf.conf content: ' + str(e)) + + # Remove the exclude option from the file + if dnf_config.has_option('main', 'exclude'): + dnf_config.remove_option('main', 'exclude') + + # Write the new content to the file + try: + with open('/etc/dnf/dnf.conf', 'w') as configfile: + dnf_config.write(configfile) + except Exception as e: + raise Exception('could not write to /etc/dnf/dnf.conf: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Update packages + # + #----------------------------------------------------------------------------------------------- + def update(self, packagesList, update_method: str = 'one_by_one', exit_on_package_update_error: bool = True): + # If update_method is 'one_by_one', update packages one by one (one command per package) + if update_method == 'one_by_one': + # Loop through the list of packages to update + for pkg in packagesList: + # If the package is excluded, ignore it + if pkg['excluded']: + continue + + print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') + + # Before updating, check If package is already in the latest version, if so, skip it + # It means that it has been updated previously by another package, probably because it was a dependency + # Get the current version of the package with dnf + # e.g. dnf repoquery --installed --qf="%{version}-%{release}.%{arch}" wget + result = subprocess.run( + ["dnf", "repoquery", "--installed", "--qf=%{version}-%{release}.%{arch}", pkg['name']], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + result.stderr) + + # Retrieve current version + current_version = result.stdout.strip() + + # If current version is the same the target version, skip the update + if current_version == pkg['available_version']: + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated by a previous package).') + + # Mark the package as already updated + self.summary['update']['success']['count'] += 1 + continue + + # Define the command to update the package + cmd = ['dnf', 'update', pkg['name'], '-y'] + + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + + # Print lines as they are read + for line in popen.stdout: + line = line.replace('\r', '') + print(' | ' + line, end='') + + # Wait for the command to finish + popen.wait() + + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # Add the package to the list of failed packages + self.summary['update']['failed']['count'] += 1 + + # If error is critical, raise an exception to quit + if (exit_on_package_update_error == True): + raise Exception('Error while updating ' + pkg['name'] + '.') + + # Else continue to the next package + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') + continue + + # Close the pipe + popen.stdout.close() + + # If command succeeded, increment the success counter + self.summary['update']['success']['count'] += 1 + + # Print a success message + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') + + # If update_method is 'global', update all packages at once (one command) + if update_method == 'global': + # Define the command to update all packages + cmd = ['dnf', 'update', '-y'] + + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + + # Print lines as they are read + for line in popen.stdout: + line = line.replace('\r', '') + print(' | ' + line, end='') + + # Wait for the command to finish + popen.wait() + + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # If error is critical, raise an exception to quit + if (exit_on_package_update_error == True): + raise Exception('Error while updating packages.') + + # Else print an error message + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error.') + else: + # Print a success message + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + 'Done.') + + # Close the pipe + popen.stdout.close() + + + #----------------------------------------------------------------------------------------------- + # + # Wait for DNF lock to be released + # + #----------------------------------------------------------------------------------------------- + def check_lock(self): + if os.path.isfile('/var/run/dnf.pid'): + print(' Waiting for dnf lock...', end=' ') + + while os.path.isfile('/var/run/dnf.pid'): + time.sleep(2) + + + #----------------------------------------------------------------------------------------------- + # + # Return dnf history Ids sorted by modification time + # + #----------------------------------------------------------------------------------------------- + def get_history(self, order): + # Get history IDs + result = subprocess.run( + ["dnf history list | tail -n +3 | awk '{print $1}'"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True, # Alias of 'text = True' + shell = True + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could nt retrieve dnf history: ' + result.stderr) + + # Retrieve history IDs + ids = result.stdout.splitlines() + + # If order is oldest, then sort by date in ascending order + if order == 'oldest': + ids.reverse() + + return ids + + + #----------------------------------------------------------------------------------------------- + # + # Parse all dnf history IDs and return a list of events (JSON) + # + #----------------------------------------------------------------------------------------------- + def parse_history(self, ids: list, entries_limit: int): + # Initialize a limit counter which will be incremented until it reaches the entries_limit + limit_counter = 0 + + # Initialize a list of events + events = [] + + # Parse each ids + for id in ids: + # If id is not a number, skip it, might be a parsing error + if not id.isnumeric(): + continue + + installed_packages_json = [] + installed_dependencies_json = [] + upgraded_packages_json = [] + removed_packages_json = [] + downgraded_packages_json = [] + reinstalled_packages_json = [] + + # Quit if the limit of entries to send has been reached + if limit_counter > entries_limit: + break + + # Retrieve informations from dnf history + # Force the locale to en_US.UTF-8 to avoid parsing issues + result = subprocess.run( + ["dnf", "history", "info", id], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True, # Alias of 'text = True' + env = {'LC_ALL': 'en_US.UTF-8'} + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('could not retrieve dnf history for id ' + id + ': ' + result.stderr) + + # Retrieve event + event = result.stdout + + # Remove '**' is present in the event string + # ** means that the transaction did not complete successfully + event = event.replace('**', '') + + # Skip if cannot retrieve event date and time + if not re.search(r'^Begin time(.+)', event, re.MULTILINE): + raise Exception('error parsing dnf event id #' + id + ': could not retrieve event date and time') + + # Skip if cannot retrieve command line + if not re.search(r'^Command Line(.+)', event, re.MULTILINE): + raise Exception('error parsing dnf event id #' + id + ': could not retrieve command line') + + # Skip if cannot retrieve packages altered + if not re.search(r'Packages Altered.*', event, re.DOTALL): + raise Exception('error parsing dnf event id #' + id + ': could not find any packages altered in the event') + + # Retrieve event date and time + date_time = re.search(r'^Begin time(.+)', event, re.MULTILINE).group(0).strip() + # Remove extra spaces and 'Begin time : ' string + date_time = str(date_time.replace(' ', '').replace('Begin time : ', '')) + + # Retrieve command line + command = re.search(r'^Command Line(.+)', event, re.MULTILINE).group(0).strip() + command = str(command.replace(' ', '').replace('Command Line :', '')).strip() + + # Retrieve packages altered + packages_altered = re.search(r'Packages Altered.*', event, re.DOTALL).group(0).strip() + packages_altered = re.sub(r' +', ' ', packages_altered) + + # Parsing and formatting + + # Convert date to %Y-%m-%d format + date_time_parsed = dateutil_parser.parse(date_time) + date = date_time_parsed.strftime('%Y-%m-%d') + time = date_time_parsed.strftime('%H:%M:%S') + + # Skip if there is no lines containing 'Install', 'Dep-Install', 'Upgraded', 'Upgrade', 'Obsoleting', 'Erase', 'Removed', 'Downgrade', 'Reinstall' + # Note: on CentOS7, it was 'Update' and 'Updated' instead of 'Upgrade' and 'Upgraded' + if not re.search(r'^ +(Install|Dep-Install|Upgraded|Upgrade|Obsoleting|Erase|Removed|Downgrade|Reinstall) .*', packages_altered, re.MULTILINE): + raise Exception('error parsing dnf event id #' + id + ': could not find any operation lines in the event') + + # For each lines of packages_altered + for line in packages_altered.split('\n'): + package_and_version = '' + package_name = '' + package_version = '' + repository = '' + operation= '' + + # If line starts with Install + if re.search(r'^ +Install .*', line): + package_and_version = re.search(r'^ +Install (.+)', line).group(0).strip().replace('Install ', '') + operation = 'install' + + # If line starts with Dep-Install + elif re.search(r'^ +Dep-Install .*', line): + package_and_version = re.search(r'^ +Dep-Install (.+)', line).group(0).strip().replace('Dep-Install ', '') + operation = 'dep-install' + + # If line starts with Upgrade + elif re.search(r'^ +Upgrade .*', line): + package_and_version = re.search(r'^ +Upgrade (.+)', line).group(0).strip().replace('Upgrade ', '') + operation = 'upgrade' + + # If line starts with Update + # elif re.search(r'^ +Update .*', line): + # package_and_version = re.search(r'^ +Update (.+)', line).group(0).strip().replace('Update ', '') + + # If line starts with Obsoleting + elif re.search(r'^ +Obsoleting .*', line): + package_and_version = re.search(r'^ +Obsoleting (.+)', line).group(0).strip().replace('Obsoleting ', '') + operation = 'obsoleting' + + # If line starts with Erase + # elif re.search(r'^ +Erase .*', line): + # package_and_version = re.search(r'^ +Erase (.+)', line).group(0).strip().replace('Erase ', '') + # operation = 'erase' + + # If line starts with Removed + elif re.search(r'^ +Removed .*', line): + package_and_version = re.search(r'^ +Removed (.+)', line).group(0).strip().replace('Removed ', '') + operation = 'remove' + + # If line starts with Downgrade + elif re.search(r'^ +Downgrade .*', line): + package_and_version = re.search(r'^ +Downgrade (.+)', line).group(0).strip().replace('Downgrade ', '') + operation = 'downgrade' + + # If line starts with Reinstall + elif re.search(r'^ +Reinstall .*', line): + package_and_version = re.search(r'^ +Reinstall (.+)', line).group(0).strip().replace('Reinstall ', '') + operation = 'reinstall' + + else: + continue + + # Remove extra spaces + package_and_version = package_and_version.strip() + + # Skip if package_and_version is empty + if package_and_version == '': + raise Exception('error parsing dnf event id #' + id + ': cannot retrieve package and version for line:\n' + line) + + # Skip if string starts with '@' + if package_and_version.startswith('@'): + continue + + # Retrieve package name, version and repository from package_and_version + package_name = re.sub(r'-[0-9].*', '', package_and_version).strip() + package_version_and_repository = re.sub(r'^-', '', package_and_version.replace(package_name, '')).strip() + + # Retrieve repository and package version + package_version = package_version_and_repository.split()[0].strip() + repository = package_version_and_repository.split()[1].strip() + + # Raise exception if package_name or package_version is empty + if package_name == '': + raise Exception('error parsing dnf event id #' + id + ': cannot retrieve package name for line:\n' + line) + + if package_version == '': + raise Exception('error parsing dnf event id #' + id + ': cannot retrieve package version for line:\n' + line) + + # Add package to the corresponding list + if operation == 'install': + installed_packages_json.append({ + 'name': package_name, + 'version': package_version, + 'repo': repository + }) + + if operation == 'dep-install': + installed_dependencies_json.append({ + 'name': package_name, + 'version': package_version, + 'repo': repository + }) + + if operation == 'upgrade': + upgraded_packages_json.append({ + 'name': package_name, + 'version': package_version, + 'repo': repository + }) + + if operation == 'remove': + removed_packages_json.append({ + 'name': package_name, + 'version': package_version, + }) + + if operation == 'downgrade': + downgraded_packages_json.append({ + 'name': package_name, + 'version': package_version, + }) + + if operation == 'reinstall': + reinstalled_packages_json.append({ + 'name': package_name, + 'version': package_version, + 'repo': repository + }) + + # TODO + # if operation == 'obsoleting': + + # Create the event JSON object + event = { + 'date_start': date, + 'time_start': time, + 'date_end': '', + 'time_end': '', + 'command': command, + } + + if installed_packages_json != '': + event['installed'] = installed_packages_json + + if installed_dependencies_json != '': + event['dep_installed'] = installed_dependencies_json + + if upgraded_packages_json != '': + event['upgraded'] = upgraded_packages_json + + if removed_packages_json != '': + event['removed'] = removed_packages_json + + if downgraded_packages_json != '': + event['downgraded'] = downgraded_packages_json + + if reinstalled_packages_json != '': + event['reinstalled'] = reinstalled_packages_json + + # Add the event to the list of events + events.append(event) + + limit_counter += 1 + + return events diff --git a/src/controllers/Package/Package.py b/src/controllers/Package/Package.py new file mode 100644 index 0000000..fa7e8e5 --- /dev/null +++ b/src/controllers/Package/Package.py @@ -0,0 +1,308 @@ +# coding: utf-8 + +# https://github.com/excid3/python-apt/blob/master/doc/examples/inst.py + +# Import libraries +from tabulate import tabulate +from colorama import Fore, Style +import re + +# Import classes +from src.controllers.System import System +from src.controllers.App.Config import Config +from src.controllers.Exit import Exit + +class Package: + def __init__(self): + self.systemController = System() + self.appConfigController = Config() + self.exitController = Exit() + + # Import libraries depending on the OS family + + # If Debian, import apt + if (self.systemController.get_os_family() == 'Debian'): + from src.controllers.Package.Apt import Apt + self.myPackageManagerController = Apt() + + # If Redhat, import yum + if (self.systemController.get_os_family() == 'Redhat'): + from src.controllers.Package.Dnf import Dnf + self.myPackageManagerController = Dnf() + + #----------------------------------------------------------------------------------------------- + # + # Check for package exclusions + # + #----------------------------------------------------------------------------------------------- + def exclude(self, ignore_exclude): + try: + # Create a new empty list of packages to update + packagesToUpdateList = [] + + # Retrieve the list of packages to exclude from the config file + configuration = self.appConfigController.get_conf() + excludeAlways = configuration['update']['packages']['exclude']['always'] + excludeOnMajorUpdate = configuration['update']['packages']['exclude']['on_major_update'] + + # Loop through the list of packages to update + for package in self.packagesToUpdateList: + excluded = False + + # Check for exclusions and exclude packages only if the ignore_exclude parameter is False + if not ignore_exclude: + # If the package is in the list of packages to exclude (on major update), check if the available version is a major update + if excludeOnMajorUpdate: + # There can be regex in the excludeOnMajorUpdate list (e.g. apache.*), so we need to convert it to a regex pattern + # https://www.geeksforgeeks.org/python-check-if-string-matches-regex-list/ + regex = '(?:% s)' % '|'.join(excludeOnMajorUpdate) + + # Check if the package name matches the regex pattern + if re.match(regex, package['name']): + # Retrieve the first digit of the current and available versions + # If the first digit is different then it is a major update, exclude the package + if package['current_version'].split('.')[0] != package['available_version'].split('.')[0]: + self.myPackageManagerController.exclude(package['name']) + excluded = True + + # If the package is in the list of packages to exclude (always), exclude it + if excludeAlways: + # There can be regex in the excludeAlways list (e.g. apache.*), so we need to convert it to a regex pattern + # https://www.geeksforgeeks.org/python-check-if-string-matches-regex-list/ + regex = '(?:% s)' % '|'.join(excludeAlways) + + # Check if the package name matches the regex pattern + if re.match(regex, package['name']): + self.myPackageManagerController.exclude(package['name']) + excluded = True + + # Add the package to the list of packages to update + packagesToUpdateList.append({ + 'name': package['name'], + 'current_version': package['current_version'], + 'available_version': package['available_version'], + 'excluded': excluded + }) + + # Replace the list of packages to update with the new list + self.packagesToUpdateList = packagesToUpdateList + + del configuration, excludeAlways, excludeOnMajorUpdate, packagesToUpdateList + except Exception as e: + raise Exception('error while excluding packages: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Remove all package exclusions + # + #----------------------------------------------------------------------------------------------- + def remove_all_exclusions(self): + try: + # Remove all exclusions + self.myPackageManagerController.remove_all_exclusions() + except Exception as e: + raise Exception('could not remove all package exclusions: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Get installed packages + # + #----------------------------------------------------------------------------------------------- + def get_installed_packages(self): + try: + # Get a list of installed packages + return self.myPackageManagerController.get_installed_packages() + + except Exception as e: + raise Exception('error while getting installed packages: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Get available packages + # + #----------------------------------------------------------------------------------------------- + def get_available_packages(self, dist_upgrade: bool = False): + try: + # First, clear package manager cache + self.myPackageManagerController.update_cache(dist_upgrade) + + # Get a list of available packages + return self.myPackageManagerController.get_available_packages() + + except Exception as e: + raise Exception('error while retrieving available packages: ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Update packages + # + #----------------------------------------------------------------------------------------------- + def update(self, assume_yes: bool = False, ignore_exclude: bool = False, check_updates: bool = False, dist_upgrade: bool = False, keep_oldconf: bool = True): + # Package update summary + self.summary = { + 'update': { + 'status': 'running', + 'success': { + 'count': 0, + 'packages': [] + }, + 'failed': { + 'count': 0, + 'packages': [] + }, + } + } + + try: + # Retrieve configuration + configuration = self.appConfigController.get_conf() + + # Retrieve the update method + update_method = configuration['update']['method'] + + # Retrieve the exit_on_package_update_error option + exit_on_package_update_error = configuration['update']['exit_on_package_update_error'] + + # Remove all exclusions before starting (could be some left from a previous run that failed) + self.remove_all_exclusions() + + # Retrieve available packages, + # passing the dist_upgrade parameter (which will, with apt, update the list of available packages including packages such as the kernel) + self.packagesToUpdateList = self.get_available_packages(dist_upgrade) + + # Check for package exclusions + self.exclude(ignore_exclude) + + # Count packages to update and packages excluded + self.packagesToUpdateCount = 0 + self.packagesExcludedCount = 0 + + for package in self.packagesToUpdateList: + if 'excluded' in package and package['excluded']: + self.packagesExcludedCount += 1 + else: + self.packagesToUpdateCount += 1 + + # Print the number of packages to update + print('\n ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages will be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' will be excluded \n') + + # Convert the list of packages to a table + table = [] + for package in self.packagesToUpdateList: + # If package is excluded + if 'excluded' in package and package['excluded']: + installOrExclude = Fore.YELLOW + '✕ (excluded)' + Style.RESET_ALL + else: + installOrExclude = Fore.GREEN + '✔' + Style.RESET_ALL + + table.append(['', package['name'], package['current_version'], package['available_version'], installOrExclude]) + + # Print the table list of packages to update + # Check prettytable for table with width control https://pypi.org/project/prettytable/ + print(tabulate(table, headers=["", "Package", "Current version", "Available version", "Install decision"], tablefmt="simple"), end='\n\n') + + # If there are no packages to update + if self.packagesToUpdateCount == 0: + print(Fore.GREEN + ' No package updates \n' + Style.RESET_ALL) + self.summary['update']['status'] = 'nothing-to-do' + + # Remove all exclusions before exiting + self.remove_all_exclusions() + + # Quit if --check-updates param has been specified + if check_updates == True: + # Remove all exclusions before exiting + self.remove_all_exclusions() + self.exitController.clean_exit(0, False) + + # Qui here if there was no packages to update + if self.packagesToUpdateCount == 0: + return + + # If --assume-yes param has not been specified, then ask for confirmation before installing the printed packages update list + if not assume_yes: + # Ask for confirmation + print('\n ' + Fore.YELLOW + 'Update now [y/N]' + Style.RESET_ALL, end=' ') + + answer = input() + + # Quit if the answer is not 'y' + if answer.lower() != 'y': + print(Fore.YELLOW + ' Cancelled' + Style.RESET_ALL) + # Remove all exclusions before exiting + self.remove_all_exclusions() + self.exitController.clean_exit(0, False) + + print('\n Updating packages...') + + # Execute the packages update + self.myPackageManagerController.dist_upgrade = dist_upgrade + self.myPackageManagerController.keep_oldconf = keep_oldconf + self.myPackageManagerController.update(self.packagesToUpdateList, update_method, exit_on_package_update_error) + + # Update the summary status + self.summary['update']['status'] = 'done' + + except Exception as e: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + str(e)) + print('\n' + Fore.RED + ' Packages update failed: ' + Style.RESET_ALL) + self.summary['update']['status'] = 'failed' + finally: + # Remove all exclusions + self.remove_all_exclusions() + + # If update method is 'one_by_one', it will be possible to print precise information about the number of packages updated and failed + if update_method == 'one_by_one': + # Update the summary with the number of packages updated and failed + self.summary['update']['success']['count'] = self.myPackageManagerController.summary['update']['success']['count'] + self.summary['update']['failed']['count'] = self.myPackageManagerController.summary['update']['failed']['count'] + + # Print the number of packages updated and failed + # If there was a failed package, print the number in red + if self.summary['update']['failed']['count'] > 0: + print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + Fore.RED + str(self.summary['update']['failed']['count']) + Style.RESET_ALL + ' packages failed' + Style.RESET_ALL) + else: + print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + str(self.summary['update']['failed']['count']) + ' packages failed' + Style.RESET_ALL) + + # If update method is 'global', just print success or failure + if update_method == 'global': + # If there was a failed package, print the number in red + if self.summary['update']['status'] == 'done': + print('\n ' + Fore.GREEN + 'All packages updated' + Style.RESET_ALL) + else: + print('\n ' + Fore.RED + 'Some packages failed to update' + Style.RESET_ALL) + + # If there was a failed package update and the package update error is critical (set to true), then raise an exception to exit + if exit_on_package_update_error == True and self.summary['update']['failed']['count'] > 0: + raise Exception('Critical error: package update failed') + + + #----------------------------------------------------------------------------------------------- + # + # Return history items (log file or history Ids) for a specific order + # + #----------------------------------------------------------------------------------------------- + def get_history(self, order): + return self.myPackageManagerController.get_history(order) + + + #----------------------------------------------------------------------------------------------- + # + # Parse history entries + # + #----------------------------------------------------------------------------------------------- + def parse_history(self, entries, entries_limit): + return self.myPackageManagerController.parse_history(entries, entries_limit) + + + #----------------------------------------------------------------------------------------------- + # + # Clear package manager cache + # + #----------------------------------------------------------------------------------------------- + def clear_cache(self): + self.myPackageManagerController.clear_cache() diff --git a/src/controllers/Service/Service.py b/src/controllers/Service/Service.py new file mode 100644 index 0000000..9ef9190 --- /dev/null +++ b/src/controllers/Service/Service.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +# Import libraries +import subprocess +import re +from colorama import Fore, Style + +# Import classes +from src.controllers.App.Config import Config + +class Service: + #----------------------------------------------------------------------------------------------- + # + # Restart services + # + #----------------------------------------------------------------------------------------------- + def restart(self, update_summary: list): + # Retrieve services to restart + services = Config().get_service_to_restart() + + # Retrieve updated packages list from update summary + updated_packages = update_summary['update']['success']['count'] + + # Quit if no packages were updated + if updated_packages == 0: + return + + print('\n Restarting services') + + # If no services to restart, skip + if not services: + print(' ▪ No services to restart') + return + + # Restart services + for service in services: + # Check if there is a condition to restart the service (got a : in the service name) + if ':' in service: + # Split service name and package name + service, package = service.split(':') + + # Check if the package is in the list of updated packages + regex = '(?:% s)' % '|'.join(updated_packages) + + # If the package is not in the list of updated packages, skip the service + if not re.match(regex, package): + continue + + print(' ▪ Restarting ' + Fore.YELLOW + service + Style.RESET_ALL + ':', end=' ') + + # Check if service is active + result = subprocess.run( + ["systemctl", "is-active", service], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # If service is unknown or inactive, skip it + if result.returncode != 0: + print('service does not exist or is not active') + continue + + # Restart service + result = subprocess.run( + ["systemctl", "restart", service, "--quiet"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # If service failed to restart, print error message + if result.returncode != 0: + print(Fore.RED + 'failed with error: ' + Style.RESET_ALL + result.stderr) + continue + + print(Fore.GREEN + 'done' + Style.RESET_ALL) diff --git a/src/controllers/Service/__init__.py b/src/controllers/Service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/System.py b/src/controllers/System.py new file mode 100644 index 0000000..e71f90f --- /dev/null +++ b/src/controllers/System.py @@ -0,0 +1,123 @@ +# coding: utf-8 + +# Import libraries +import os +import platform +import distro +import subprocess + +class System: + #----------------------------------------------------------------------------------------------- + # + # Return true if current user is root or sudo + # + #----------------------------------------------------------------------------------------------- + def is_root(self): + if os.geteuid() == 0: + return True + + return False + + + #----------------------------------------------------------------------------------------------- + # + # Check if the program is running on a supported Linux distribution + # + #----------------------------------------------------------------------------------------------- + def check(self): + # Check if the program is running on Linux + if (platform.system() != 'Linux'): + raise Exception('This program only works on Linux') + + # Check if the program is running on a supported Linux distribution (will raise an exception if not supported) + self.get_os_family() + + + #----------------------------------------------------------------------------------------------- + # + # Return the OS family + # + #----------------------------------------------------------------------------------------------- + def get_os_family(self): + if (distro.name() in ['Debian GNU/Linux', 'Debian', 'Ubuntu', 'Kubuntu', 'Xubuntu', 'Linux Mint']): + return 'Debian' + + if (distro.name() in ['Red Hat Enterprise Linux', 'CentOS Linux', 'CentOS Stream', 'Fedora', 'Alma Linux', 'Rocky Linux', 'Oracle Linux Server']): + return 'Redhat' + + raise Exception('This program does not support your Linux distribution "' + distro.name() + '" yet.') + + + #----------------------------------------------------------------------------------------------- + # + # Return the OS name + # + #----------------------------------------------------------------------------------------------- + def get_os_name(self): + return distro.name() + + + #----------------------------------------------------------------------------------------------- + # + # Return the OS version + # + #----------------------------------------------------------------------------------------------- + def get_os_version(self): + return distro.version() + + + #----------------------------------------------------------------------------------------------- + # + # Return the kernel version + # + #----------------------------------------------------------------------------------------------- + def get_kernel(self): + return platform.release() + + + #----------------------------------------------------------------------------------------------- + # + # Return the architecture + # + #----------------------------------------------------------------------------------------------- + def get_arch(self): + return platform.machine() + + + #----------------------------------------------------------------------------------------------- + # + # Return the virtualization type + # + #----------------------------------------------------------------------------------------------- + def get_virtualization(self): + # Detect virtualization type + if os.path.isfile("/usr/sbin/virt-what"): + virt = os.popen('/usr/sbin/virt-what').read().replace('\n', ' ') + if not virt: + virt = "Bare-metal" + + return virt + + + #----------------------------------------------------------------------------------------------- + # + # Return True if a reboot is required + # + #----------------------------------------------------------------------------------------------- + def reboot_required(self): + if self.get_os_family() == 'Debian' and os.path.isfile('/var/run/reboot-required'): + return True + + if self.get_os_family() == 'Redhat' and os.path.isfile('/usr/bin/needs-restarting'): + result = subprocess.run( + ["/usr/bin/needs-restarting", "-r"], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + if result.returncode != 0: + return True + + return False + \ No newline at end of file diff --git a/src/controllers/Yaml.py b/src/controllers/Yaml.py new file mode 100644 index 0000000..38c9545 --- /dev/null +++ b/src/controllers/Yaml.py @@ -0,0 +1,27 @@ +# coding: utf-8 + +# Import libraries +import yaml + +# Custom class to handle yaml files and keep the order of the keys when writing the file +# Modern version of python3-yaml comes with 'sort_keys=False' option to keep the order of the keys, but +# this is not available on RHEL8 based OS (python3-yaml is too old) +# So this class is a workaround to keep the order of the keys when writing the file + +# Yaml inherits from dict +class Yaml(): + #----------------------------------------------------------------------------------------------- + # + # Write data to a yaml file + # + #----------------------------------------------------------------------------------------------- + def write(self, data, file: str): + try: + with open(file, 'w') as yaml_file: + try: + yaml.dump(data, yaml_file, default_flow_style=False, sort_keys=False) + + except TypeError: + yaml.dump(data, yaml_file, default_flow_style=False) + except Exception as e: + raise Exception(e) diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/linupdate.template.yml b/templates/linupdate.template.yml new file mode 100644 index 0000000..3210a7e --- /dev/null +++ b/templates/linupdate.template.yml @@ -0,0 +1,8 @@ +main: + environment: prod + mail: + enabled: false + recipient: [] + profile: Host +modules: + enabled: [] diff --git a/templates/mail/mail.template.html b/templates/mail/mail.template.html new file mode 100644 index 0000000..f8104cd --- /dev/null +++ b/templates/mail/mail.template.html @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
Linux update report
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
__CONTENT__
+
__PRE_CONTENT__
+
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
Linupdate - a deb/rpm package updater tool
Github
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/templates/modules/reposerver.template.yml b/templates/modules/reposerver.template.yml new file mode 100644 index 0000000..a97ef45 --- /dev/null +++ b/templates/modules/reposerver.template.yml @@ -0,0 +1,15 @@ +agent: + enabled: false + listen: + enabled: true +client: + auth: + id: '' + token: '' + get_packages_conf_from_reposerver: + enabled: true + get_repos_from_reposerver: + enabled: true + remove_existing_repos: true +reposerver: + url: '' diff --git a/templates/update.template.yml b/templates/update.template.yml new file mode 100644 index 0000000..426abcd --- /dev/null +++ b/templates/update.template.yml @@ -0,0 +1,10 @@ +update: + exit_on_package_update_error: true + method: one_by_one + packages: + exclude: + always: [] + on_major_update: [] +post_update: + services: + restart: [] diff --git a/version b/version index 3c3ae88..56fea8a 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.2.13 \ No newline at end of file +3.0.0 \ No newline at end of file