diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 00000000..b8cf09dd --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,26 @@ +on: + push: + pull_request: + workflow_dispatch: + +name: 'Lint and Test' + +jobs: + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + check_together: 'yes' + env: + SHELLCHECK_OPTS: -e SC2154 + + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/test-and-upload-to-testpypi.yml b/.github/workflows/test-and-upload-to-testpypi.yml deleted file mode 100644 index d73bc96e..00000000 --- a/.github/workflows/test-and-upload-to-testpypi.yml +++ /dev/null @@ -1,58 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Test & Upload to TestPyPI - -# Controls when the action will run. -on: - # Triggers the workflow on push to the master branch - push: - branches: [ main ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - # Sets up python3 - - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - # Installs and upgrades pip, installs other dependencies and installs the package from setup.py - - name: "Installs and upgrades pip, installs other dependencies and installs the package from setup.py" - run: | - # Upgrade pip - python3 -m pip install --upgrade pip - # Install build deps - python3 -m pip install setuptools wheel twine - # If requirements.txt exists, install from it - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - # Install the package from setup.py - python3 setup.py install - - # Tests with unittest - - name: Test with unittest - run: | - cd tests - python3 -m unittest discover - cd .. - - # Upload to TestPyPI - - name: Build and Upload to TestPyPI - run: | - python3 setup.py sdist bdist_wheel - python3 -m twine upload dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TWINE_TEST_TOKEN }} - TWINE_REPOSITORY: testpypi diff --git a/.github/workflows/upload-to-pip.yml b/.github/workflows/upload-to-pip.yml deleted file mode 100644 index ddd77d85..00000000 --- a/.github/workflows/upload-to-pip.yml +++ /dev/null @@ -1,43 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Upload to PIP - -# Controls when the action will run. -on: - # Triggers the workflow when a release is created - release: - types: [created] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "upload" - upload: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - # Sets up python - - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - # Install dependencies - - name: "Installs dependencies" - run: | - python3 -m pip install --upgrade pip - python3 -m pip install setuptools wheel twine - - # Build and upload to PyPI - - name: "Builds and uploads to PyPI" - run: | - python3 setup.py sdist bdist_wheel - python3 -m twine upload dist/* - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TWINE_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f17f07b3..00000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -__pycache__/ -build/ -dist/ -*.egg-info/ -*.egg -venv/ -.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..cce767d4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +# See https://pre-commit.com for more information +repos: +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.10 + hooks: + - id: remove-tabs + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: check-merge-conflict + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-docstring-first + - id: check-symlinks + - id: destroyed-symlinks + - id: fix-byte-order-marker diff --git a/README.md b/README.md index d90d398b..93789fbe 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,129 @@ # truetool + A easy tool for frequently used TrueNAS SCALE CLI utilities. -Previously known as "trueupdate" -## How to install +## Table of contents: +* [Synopsis](#synopsis) +* [Arguments](#arguments) +* [How to Install](#how-to-install) +* [How to Update](#how-to-update) +* [Creating a Cron Job](#creating-a-cron-job) +* [Additional Information](#additional-information) -run `pip install truetool` +
-Please be aware you will need to reinstall after every SCALE update +## Synopsis + +TrueTool is a commandline tool, designed to enable some features of TrueNAS SCALE that are either not-enabled by default or not-available in the Web-GUI. +It also offers a few handy shortcuts for commonly required chores, like: Enabling Apt or Helm + +## Arguments + +| Flag | Example | Parameter | Description | +|----------------- |------------------------ |----------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| --delete-backup | --delete-backup | None | Opens a menu to delete backups
_Useful if you need to delete old system backups or backups from other scripts_ | +| --restore | --restore | None | Restore TrueTool specific `ix-applications dataset` snapshot | +| --mount | --mount | None | Initiates mounting feature
Choose between unmounting and mounting PVC data | +| --dns | --dns | None | list all of your applications DNS names and their web ports +| --list-backups | --list-backups | None | Prints a list of backups available +| --helm-enable | --helm-enable | None | Enables Helm command access on SCALE +| --apt-enable | --apt-enable | None | Enables Apt command access on SCALE +| --no-color | --no-color | None | Disables showing colors in terminal output, usefull for SCALE Email output +| -U | -U | None | Update applications, ignoring major version changes | +| -u | -u | None | Update applications, do NOT update if there was a major version change | +| -b | -b 14 | Integer | Backup `ix-appliactions` dataset
_Creates backups up to the number you've chosen_ | +| -i | -i nextcloud -i sonarr | String | Applications listed will be ignored during updating
_List one application after another as shown in the example_ | +| -v | -v | None | Verbose Output
_Look at the bottom of this page for an example_ | +| -t | -t 150 | Integer | Set a custom timeout to be used with either:
`-m`
_Time the script will wait for application to be "STOPPED"_
or
`-(u\|U)`
_Time the script will wait for application to be either "STOPPED" or "ACTIVE"_ | +| -s | -s | None | Sync Catalogs prior to updating | +| -p | -p | None | Prune old/unused docker images | + + +
+
+ + +## How to Install + +### Create a Scripts Dataset + +In this example we created a `scripts` dataset on the Truenas SCALE system, feel free to use another folder. + +### Open a Terminal + +**Change Directory to your scripts folder** +``` +cd /mnt/pool/scripts +``` + +**Git Clone truetool** +``` +git clone https://github.com/truecharts/truetool.git +``` + +**Change Directory to truetool folder** +``` +cd truetool +``` + +From here, you can just run truetool with `bash truetool.sh -ARGUMENTS` + +
## How to Update -run `pip install --upgrade truetool` +TrueTool updates itself automatically. -## How to use +
-running `truetool` should be a good start. +### Update with your Cron Job -Additional options are available: +Here, we will update the script prior to running it, incase there is a bugfix, or any new additions to the script -### Help +**Cron Job Command** +``` +bash /mnt/pool/scripts/truetool/truetool.sh -b 14 -rsup +``` -- `truetool -h` for the CLI help page +
+
+ +## Creating a Cron Job + +1. TrueNAS SCALE GUI +2. System Settings +3. Advanced +4. Cron Jobs + 1. Click Add + +| Name | Value | Reason | +|------------------------ |------------------------------------------------------------------------------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Description` | TrueTool Update apps | This is up to you, put whatever you think is a good description in here | +| `Command` | `bash /PATH/TO/truetool_DIRECTORY/truetool.sh --no-color -b 14 -sup` | This is the command you will be running on your schedule, example: `bash /mnt/speed/scripts/truetool/truetool.sh -b 14 -rsup` | +| `Run As User` | `root` | Running the script as `root` is REQUIRED. You cannot access all of the kubernetes functions without this user. | +| `Schedule` | Up to you, example: `0400` | Again up to you | +| `Hide Standard Output` | `False` or Unticked | It's best to keep an eye on updates and enable this to recieve email reports | +| `Hide Standard Error` | `False` or Unticked | We definately want to see what errors occured during updating | +| `Enabled` | `True` or Ticked | This will Enable the script to run on your schedule | -### Update -- `truetool -u` or ` truetool --update` updates TrueNAS SCALE Apps +
+
+ +### Additional Information -- `truetool --catalog CATALOGNAME` where CATALOGNAME is the name of the catalog you want to process in caps -- `truetool --versioning SCHEME` where SCHEME is the highest semver version you want to process. options: `patch`, `minor` and `major` -- `truetool -a` or ` truetool --all` updates both active (running) and non-active (stuck or stopped) Apps +#### TrueTool vs HeavyScript +TrueTool and HeavyScript are based, in essence, based on the the original (python based) TrueUpdate and TrueTool. +Then Support-Manager for TrueCharts, HeavyBullets8, ported this to Bash and started adding some additional logic and options for tasks we frequently needed our users to do, such as mounting PVC's. -### Backup -- `truetool -b` or ` truetool --backup` backup the complete Apps system prior to updates. Deletes old backups prior, number of old backups can be set, 14 by default -- `truetool -r` or ` truetool --restore` restores a specific backup by name -- `truetool -d` or ` truetool --delete` deletes a specific backup by name +After a month or so, the TrueCharts Team officially started refactoring this expanded bash-port. Due to personal reasons, HeavyBullets by then decided to seperate from TrueCharts after merging the TrueCharts refactor into his own work. The beauty of OpenSource. -### Other +From this point onwards the HeavyScript and TrueTool diverged a bit. +We internally review changes within our staff team, to verify we somewhat stick to best-practices. This means, in some cases, we decided not to port certain features from HeavyScript and did decide to add features we think are usefull and safe. +But this also means we can give guarantees TrueTool works optimally with our Catalog of TrueNAS SCALE Apps, as well as official Apps. -- `truetool -s` or ` truetool --sync` to sync the catalogs before running updates -- `truetool -p` or ` truetool --prune` to prune (remove) old docker images after running auto-update - -### Important note - -Please use the above arguments seperatly, combining them might not work as you would expect. -So use: `truetool -u -b -p -s -a` -not: `truetool -ubpsa` \ No newline at end of file +Users from HeavyScript can safely start using TrueTool, as we've made precautions to ensure the backups take over smoothly. +We, however, do *not* advice using HeavyScript with TrueCharts Apps. Not because it's a bad App, but because we offer an alternative that is validated by our Staff. diff --git a/includes/backup.sh b/includes/backup.sh new file mode 100755 index 00000000..1f412c6c --- /dev/null +++ b/includes/backup.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +## Simple shortcut to just list the backups without promts and such +listBackups(){ +echo -e "${BWhite}Backup Listing Tool${Color_Off}" +clear -x && echo "pulling all restore points.." +list_backups=$(cli -c 'app kubernetes list_backups' | grep -v system-update | sort -t '_' -Vr -k2,7 | tr -d " \t\r" | awk -F '|' '{print $2}' | nl | column -t) +[[ -z "$list_backups" ]] && echo -e "${IRed}No restore points available${Color_Off}" && exit || echo "Detected Backups:" && echo "$list_backups" +} +export -f listBackups + +## Lists backups, except system-created backups, and promts which one to delete +deleteBackup(){ +echo -e "${BWhite}Backup Deletion Tool${Color_Off}" +clear -x && echo "pulling all restore points.." +list_delete_backups=$(cli -c 'app kubernetes list_delete_backups' | grep -v system-update | sort -t '_' -Vr -k2,7 | tr -d " \t\r" | awk -F '|' '{print $2}' | nl | column -t) +clear -x +# shellcheck disable=SC2015 +[[ -z "$list_delete_backups" ]] && echo -e "${IRed}No restore points available${Color_Off}" && exit || { title; echo -e "Choose a restore point to delete\nThese may be out of order if they are not TrueTool backups" ; } +# shellcheck disable=SC2015 +echo "$list_delete_backups" && read -rt 600 -p "Please type a number: " selection && restore_point=$(echo "$list_delete_backups" | grep ^"$selection " | awk '{print $2}') +[[ -z "$selection" ]] && echo "${IRed}Your selection cannot be empty${Color_Off}" && exit #Check for valid selection. If none, kill script +[[ -z "$restore_point" ]] && echo "Invalid Selection: $selection, was not an option" && exit #Check for valid selection. If none, kill script +echo -e "\nWARNING:\nYou CANNOT go back after deleting your restore point" || { echo "${IRed}FAILED${Color_Off}"; exit; } +# shellcheck disable=SC2015 +echo -e "\n\nYou have chosen:\n$restore_point\n\nWould you like to continue?" && echo -e "1 Yes\n2 No" && read -rt 120 -p "Please type a number: " yesno || { echo "${IRed}FAILED${Color_Off}"; exit; } +if [[ $yesno == "1" ]]; then + echo -e "\nDeleting $restore_point" && cli -c 'app kubernetes delete_backup backup_name=''"'"$restore_point"'"' &>/dev/null && echo -e "${IGreen}Sucessfully deleted${Color_Off}" || echo -e "${IRed}Deletion FAILED${Color_Off}" +elif [[ $yesno == "2" ]]; then + echo "You've chosen NO, killing script." +else + echo -e "${IRed}Invalid Selection${Color_Off}" +fi +} +export -f deleteBackup + +## Creates backups and deletes backups if a "backups to keep"-count is exceeded. +# backups-to-keep takes only heavyscript and truetool created backups into account, as other backups aren't guaranteed to be sorted correctly +backup(){ +echo -e "${BWhite}Backup Tool${Color_Off}" +echo -e "\nNumber of backups was set to $number_of_backups" +date=$(date '+%Y_%m_%d_%H_%M_%S') +[[ "$verbose" == "true" ]] && cli -c 'app kubernetes backup_chart_releases backup_name=''"'TrueTool_"$date"'"' +[[ -z "$verbose" ]] && echo -e "\nNew Backup Name:" && cli -c 'app kubernetes backup_chart_releases backup_name=''"'TrueTool_"$date"'"' | tail -n 1 +mapfile -t list_create_backups < <(cli -c 'app kubernetes list_create_backups' | grep 'HeavyScript\|TrueTool_' | sort -t '_' -Vr -k2,7 | awk -F '|' '{print $2}'| tr -d " \t\r") +# shellcheck disable=SC2309 +if [[ ${#list_create_backups[@]} -gt "number_of_backups" ]]; then + echo -e "\nDeleting the oldest backup(s) for exceeding limit:" + overflow=$(( ${#list_create_backups[@]} - "$number_of_backups" )) + mapfile -t list_overflow < <(cli -c 'app kubernetes list_create_backups' | grep "TrueTool_" | sort -t '_' -V -k2,7 | awk -F '|' '{print $2}'| tr -d " \t\r" | head -n "$overflow") + for i in "${list_overflow[@]}" + do + cli -c 'app kubernetes delete_backup backup_name=''"'"$i"'"' &> /dev/null || echo "${IRed}FAILED${Color_Off} to delete $i" + echo "$i" + done +fi +} +export -f backup + +## Lists available backup and prompts the users to select a backup to restore +restore(){ +echo -e "${BWhite}Backup Restoration Tool${Color_Off}" +clear -x && echo "pulling restore points.." +list_restore_backups=$(cli -c 'app kubernetes list_restore_backups' | grep "TrueTool_" | sort -t '_' -Vr -k2,7 | tr -d " \t\r" | awk -F '|' '{print $2}' | nl | column -t) +clear -x +# shellcheck disable=SC2015 +[[ -z "$list_restore_backups" ]] && echo "No TrueTool restore points available" && exit || { title; echo "Choose a restore point" ; } +echo "$list_restore_backups" && read -rt 600 -p "Please type a number: " selection && restore_point=$(echo "$list_restore_backups" | grep ^"$selection " | awk '{print $2}') +[[ -z "$selection" ]] && echo "Your selection cannot be empty" && exit #Check for valid selection. If none, kill script +[[ -z "$restore_point" ]] && echo "Invalid Selection: $selection, was not an option" && exit #Check for valid selection. If none, kill script +echo -e "\nWARNING:\nThis is NOT guranteed to work\nThis is ONLY supposed to be used as a LAST RESORT\nConsider rolling back your applications instead if possible" || { echo "${IRed}FAILED${Color_Off}"; exit; } +# shellcheck disable=SC2015 +echo -e "\n\nYou have chosen:\n$restore_point\n\nWould you like to continue?" && echo -e "1 Yes\n2 No" && read -rt 120 -p "Please type a number: " yesno || { echo "${IRed}FAILED${Color_Off}"; exit; } +if [[ $yesno == "1" ]]; then + echo -e "\nStarting Backup, this will take a ${BWhite}LONG${Color_Off} time." && cli -c 'app kubernetes restore_backup backup_name=''"'"$restore_point"'"' || echo "Restore ${IRed}FAILED${Color_Off}" +elif [[ $yesno == "2" ]]; then + echo "You've chosen NO, killing script. Good luck." +else + echo -e "${IRed}Invalid Selection${Color_Off}" +fi +} +export -f restore diff --git a/includes/chores.sh b/includes/chores.sh new file mode 100755 index 00000000..df6d3a73 --- /dev/null +++ b/includes/chores.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +helmEnable(){ +echo -e "${BWhite}Enabling Helm${Color_Off}" +export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && echo -e "${IGreen}Helm Enabled${Color_Off}"|| echo -e "${IRed}Helm Enable FAILED${Color_Off}" +} +export -f helmEnable + +aptEnable(){ +echo -e "${BWhite}Enabling Apt-Commands${Color_Off}" +chmod +x /usr/bin/apt* && echo -e "${IGreen}APT enabled${Color_Off}"|| echo -e "${IRed}APT Enable FAILED${Color_Off}" +} +export -f aptEnable + +# Prune unused docker images to prevent dataset/snapshot bloat related slowdowns on SCALE +prune(){ +echo -e "${BWhite}Docker Prune${Color_Off}" +echo "Pruning Docker Images..." +docker image prune -af | grep "^Total" && echo -e "${IGreen}Docker Prune Successfull${Color_Off}" || echo "Docker Prune ${IRed}FAILED${Color_Off}" + +# TODO Switch to middleware prune on next release +# midclt call container.prune '{"remove_unused_images": true, "remove_stopped_containers": true}' &> /dev/null && echo "Docker Prune completed"|| echo "Docker Prune ${IRed}FAILED${Color_Off}" +} +export -f prune + +# +sync(){ +echo -e "${BWhite}Starting Catalog Sync...${Color_Off}" +cli -c 'app catalog sync_all' &> /dev/null && echo -e "${IGreen}Catalog sync complete${Color_Off}" || echo -e "${IRed}Catalog Sync Failed${Color_Off}" +} +export -f sync diff --git a/includes/colors.sh b/includes/colors.sh new file mode 100755 index 00000000..a1f8cbdd --- /dev/null +++ b/includes/colors.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# shellcheck disable=SC2034 + +# Reset +Color_Off='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Underline +UBlack='\033[4;30m' # Black +URed='\033[4;31m' # Red +UGreen='\033[4;32m' # Green +UYellow='\033[4;33m' # Yellow +UBlue='\033[4;34m' # Blue +UPurple='\033[4;35m' # Purple +UCyan='\033[4;36m' # Cyan +UWhite='\033[4;37m' # White + +# High Intensity +IBlack='\033[0;90m' # Black +IRed='\033[0;91m' # Red +IGreen='\033[0;92m' # Green +IYellow='\033[0;93m' # Yellow +IBlue='\033[0;94m' # Blue +IPurple='\033[0;95m' # Purple +ICyan='\033[0;96m' # Cyan +IWhite='\033[0;97m' # White + + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + +noColor(){ +# Reset +Color_Off="" + +# Regular Colors +Black="" +Red="" +Green="" +Yellow="" +Blue="" +Purple="" +Cyan="" +White="" + +# Bold +BBlack="" +BRed="" +BGreen="" +BYellow="" +BBlue="" +BPurple="" +BCyan="" +BWhite="" + +# Underline +UBlack="" +URed="" +UGreen="" +UYellow="" +UBlue="" +UPurple="" +UCyan="" +UWhite="" + +# High Intensity +IBlack="" +IRed="" +IGreen="" +IYellow="" +IBlue="" +IPurple="" +ICyan="" +IWhite="" + + +# Bold High Intensity +BIBlack="" +BIRed="" +BIGreen="" +BIYellow="" +BIBlue="" +BIPurple="" +BICyan="" +BIWhite="" + } + export -f noColor diff --git a/includes/dns.sh b/includes/dns.sh new file mode 100755 index 00000000..0e12e90a --- /dev/null +++ b/includes/dns.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +dns(){ + echo -e "${BWhite}Service DNS Names Tool${Color_Off}" +clear -x +echo "Generating Internal Service DNS Names..." +#ignored dependency pods, may need to add more in the future. +dep_ignore="\-cronjob\-|^kube-system|\ssvclb|NAME|\-memcached\-.[^custom\-app]|\-postgresql\-.[^custom\-app]|\-redis\-.[^custom\-app]|\-mariadb\-.[^custom\-app]|\-promtail\-.[^custom\-app]" + +# Pulling pod names +mapfile -t main < <(k3s kubectl get pods -A | grep -Ev "$dep_ignore" | sort) + +# Pulling all ports +all_ports=$(k3s kubectl get service -A) + +clear -x +count=0 +for i in "${main[@]}" +do + [[ count -le 0 ]] && echo -e "\n" && ((count++)) + appName=$(echo "$i" | awk '{print $2}' | sed 's/-[^-]*-[^-]*$//' | sed 's/-0//') + ixName=$(echo "$i" | awk '{print $1}') + port=$(echo "$all_ports" | grep -E "\s$appName\s" | awk '{print $6}' | grep -Eo "^[[:digit:]]+{1}") + [[ -n "$port" ]] && echo -e "$appName.$ixName.svc.cluster.local $port" +done | uniq | nl -b t | sed 's/\s\s\s$/- -------- ----/' | column -t -R 1 -N "#,DNS_Name,Port" -L +} +export -f dns diff --git a/includes/help.sh b/includes/help.sh new file mode 100755 index 00000000..5b778c75 --- /dev/null +++ b/includes/help.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +help(){ +[[ $help == "true" ]] && clear -x +echo "" +echo -e "${BWhite}Basic Utilities${Color_Off}" +echo "--mount | Initiates mounting feature, choose between unmounting and mounting PVC data" +echo "--restore | Opens a menu to restore a \"truetool\" backup that was taken on your \"ix-applications\" dataset" +echo "--delete-backup | Opens a menu to delete backups on your system" +echo "--list-backups | Prints a list of backups available" +echo "--helm-enable | Enables Helm command access on SCALE" +echo "--apt-enable | Enables Apt command access on SCALE" +echo "--dns | list all of your applications DNS names and their web ports" +echo +echo -e "${BWhite}Update Options${Color_Off}" +echo "-U | Update all applications, ignores versions" +echo "-u | Update all applications, does not update Major releases" +echo "-b | Back-up your ix-applications dataset, specify a number after -b" +echo "-i | Add application to ignore list, one by one, see example below." +echo "-v | verbose output" +echo "-t | Set a custom timeout in seconds when checking if either an App or Mountpoint correctly Started, Stopped or (un)Mounted. Defaults to 500 seconds" +echo "-s | sync catalog" +echo "-p | Prune unused/old docker images" +echo +echo -e "${BWhite}Examples${Color_Off}" +echo "bash truetool.sh -b 14 -i portainer -i arch -i sonarr -i radarr -t 600 -vrsUp" +echo "bash /mnt/tank/scripts/truetool.sh -t 150 --mount" +echo "bash /mnt/tank/scripts/truetool.sh --dns" +echo "bash /mnt/tank/scripts/truetool.sh --restore" +echo "bash /mnt/tank/scripts/truetool.sh --delete-backup" +echo +exit +} +export -f help diff --git a/includes/mount.sh b/includes/mount.sh new file mode 100755 index 00000000..7d5a6019 --- /dev/null +++ b/includes/mount.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +mountPVC(){ +echo -e "${BWhite}PVC Mounting Tool${Color_Off}" +clear -x +title +echo -e "1 Mount\n2 Unmount All" && read -rt 600 -p "Please type a number: " selection +[[ -z "$selection" ]] && echo "Your selection cannot be empty" && exit #Check for valid selection. If none, kill script +if [[ $selection == "1" ]]; then + list=$(k3s kubectl get pvc -A | sort -u | awk '{print NR-1, "\t" $1 "\t" $2 "\t" $4}' | column -t | sed "s/^0/ /") + echo "$list" && read -rt 120 -p "Please type a number: " selection + [[ -z "$selection" ]] && echo "Your selection cannot be empty" && exit #Check for valid selection. If none, kill script + app=$(echo -e "$list" | grep ^"$selection " | awk '{print $2}' | cut -c 4- ) + [[ -z "$app" ]] && echo "Invalid Selection: $selection, was not an option" && exit #Check for valid selection. If none, kill script + pvc=$(echo -e "$list" | grep ^"$selection ") + status=$(cli -m csv -c 'app chart_release query name,status' | grep -E "^$app\b" | awk -F ',' '{print $2}'| tr -d " \t\n\r") + if [[ "$status" != "STOPPED" ]]; then + [[ -z $timeout ]] && echo -e "\nDefault Timeout: 500" && timeout=500 || echo -e "\nCustom Timeout: $timeout" + SECONDS=0 && echo -e "\nScaling down $app" && midclt call chart.release.scale "$app" '{"replica_count": 0}' &> /dev/null + else + echo -e "\n$app is already stopped" + fi + while [[ "$SECONDS" -le "$timeout" && "$status" != "STOPPED" ]] + do + status=$(cli -m csv -c 'app chart_release query name,status' | grep -E "^$app\b" | awk -F ',' '{print $2}'| tr -d " \t\n\r") + echo -e "Waiting $((timeout-SECONDS)) more seconds for $app to be STOPPED" && sleep 5 + done + data_name=$(echo "$pvc" | awk '{print $3}') + volume_name=$(echo "$pvc" | awk '{print $4}') + full_path=$(zfs list | grep "$volume_name" | awk '{print $1}') + echo -e "\nMounting\n$full_path\nTo\n/mnt/truetool/$data_name" && zfs set mountpoint="/truetool/$data_name" "$full_path" && echo -e "Mounted, Use the Unmount All option to unmount\n" + exit +elif [[ $selection == "2" ]]; then + mapfile -t unmount_array < <(basename -a /mnt/truetool/* | sed "s/*//") + [[ -z ${unmount_array[*]} ]] && echo "Theres nothing to unmount" && exit + for i in "${unmount_array[@]}" + do + main=$(k3s kubectl get pvc -A | grep -E "\s$i\s" | awk '{print $1, $2, $4}') + app=$(echo "$main" | awk '{print $1}' | cut -c 4-) + pvc=$(echo "$main" | awk '{print $3}') + mapfile -t path < <(find /mnt/*/ix-applications/releases/"$app"/volumes/ -maxdepth 0 | cut -c 6-) + if [[ "${#path[@]}" -gt 1 ]]; then #if there is another app with the same name on another pool, use the current pools application, since the other instance is probably old, or unused. + echo "$i is a name used on more than one pool.. attempting to use your current kubernetes apps pool" + pool=$(cli -c 'app kubernetes config' | grep -E "dataset\s\|" | awk -F '|' '{print $3}' | awk -F '/' '{print $1}' | tr -d " \t\n\r") + full_path=$(find /mnt/"$pool"/ix-applications/releases/"$app"/volumes/ -maxdepth 0 | cut -c 6-) + zfs set mountpoint=legacy "$full_path""$pvc" && echo "$i unmounted" && rmdir /mnt/truetool/"$i" || echo "${IRed}FAILED${Color_Off} to unmount $i" + else + # shellcheck disable=SC2128 + zfs set mountpoint=legacy "$path""$pvc" && echo "$i unmounted" && rmdir /mnt/truetool/"$i" || echo "${IRed}FAILED${Color_Off} to unmount $i" + fi + done + rmdir /mnt/truetool +else + echo -e "${IRed}Invalid selection, \"$selection\" was not an option${Color_Off}" +fi +} +export -f mountPVC diff --git a/includes/no_args.sh b/includes/no_args.sh new file mode 100644 index 00000000..7db6e4a5 --- /dev/null +++ b/includes/no_args.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +no_args(){ + echo "0 Show Help" + echo "1 List Internal Service DNS Names" + echo "2 Mount and Unmount PVC storage for easy access" + echo "3 List Backups" + echo "4 Create a Backup" + echo "5 Restore a Backup" + echo "6 Delete a Backup" + echo "7 Enable Helm Commands" + echo "8 Enable Apt and Apt-Get Commands" + echo "9 Update All Apps" + read -rt 600 -p "Please select an option by number: " selection + + case $selection in + 1) + help="true" + ;; + 2) + dns="true" + ;; + 3) + mountPVC="true" + ;; + 4) + listBackups="true" + ;; + 5) + read -rt 600 -p "Please type the max number of backups to keep: " backups + re='^[0-9]+$' + number_of_backups=$backups + ! [[ $backups =~ $re ]] && echo -e "Error: -b needs to be assigned an interger\n\"""$number_of_backups""\" is not an interger" >&2 && exit + [[ "$number_of_backups" -le 0 ]] && echo "Error: Number of backups is required to be at least 1" && exit + ;; + 6) + restore="true" + ;; + 7) + deleteBackup="true" + ;; + 8) + helmEnable="true" + ;; + 9) + aptEnable="true" + ;; + 10) + echo "" + echo "1 Update Apps Excluding likely breaking major changes" + echo "2 Update Apps Including likely breaking major changes" + read -rt 600 -p "Please select an option by number: " updateType + if [[ "$updateType" == "1" ]]; then + update_apps="true" + elif [[ "$updateType" == "2" ]]; then + update_all_apps="true" + else + echo "INVALID ENTRY" && exit 1 + fi + ;; + *) + echo "Unknown option" && exit 1 + ;; + esac + echo "" +} +export -f no_args diff --git a/includes/title.sh b/includes/title.sh new file mode 100755 index 00000000..b6c9e740 --- /dev/null +++ b/includes/title.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Fancy ascii title. +title(){ +if [[ -z $titleShown ]]; then + echo -e "${IRed} _______ _____ _ _ "; + echo " |__ __| / ____| | | | "; + echo " | |_ __ _ _ ___| | | |__ __ _ _ __| |_ ___ "; + echo -e "${IYellow} | | '__| | | |/ _ \ | | '_ \ / _\` | '__| __/ __|"; + echo " | | | | |_| | __/ |____| | | | (_| | | | |_\__ \\"; + echo -e "${IGreen} __|_|_| \__,_|\___|\_____|_| |_|\__,_|_| \__|___/"; + echo " |__ __| |__ __| | | "; + echo -e "${IBlue} | |_ __ _ _ ___| | ___ ___ | | "; + echo " | | '__| | | |/ _ \ |/ _ \ / _ \| | "; + echo -e "${IPurple} | | | | |_| | __/ | (_) | (_) | | "; + echo " |_|_| \__,_|\___|_|\___/ \___/|_| "; + echo " "; + echo -e "${Color_Off} "; +fi +titleShown='true' +} +export -f title diff --git a/includes/update.sh b/includes/update.sh new file mode 100755 index 00000000..0aaa7506 --- /dev/null +++ b/includes/update.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +update_apps(){ +echo -e "${BWhite}App Updater${Color_Off}" +[[ -z $timeout ]] && echo -e "Default Timeout: 500" && timeout=500 || echo -e "\nCustom Timeout: $timeout" +[[ "$timeout" -le 120 ]] && echo "Warning: Your timeout is set low and may lead to premature rollbacks or skips" + +echo "" +echo "Creating list of Apps to update..." + +# Render a list of ignored applications, so users can verify their ignores got parsed correctly. +if [[ -z ${ignore[*]} ]]; then + echo "No apps added to ignore list, continuing..." +else + echo "ignored applications:" + for ignored in "${ignore[@]}" + do + echo "${ignored}" + done +fi +echo "" + +mapfile -t array < <(cli -m csv -c 'app chart_release query name,update_available,human_version,human_latest_version,container_images_update_available,status' | grep -E ",true(,|$)" | sort) +[[ -z ${array[*]} ]] && echo -e "\nThere are no updates available or middleware timed out" && return 0 || echo -e "\n${#array[@]} update(s) available:" +PIDlist=() + +# Draft a list of app names, seperate from actuall execution +# This prevents outputs getting mixed together +for i in "${array[@]}" +do + app_name=$(echo "$i" | awk -F ',' '{print $1}') #print out first catagory, name. + echo "$app_name" +done + +echo "" +echo "Updating Apps..." + +# Create a background task for each update as async solution +for i in "${array[@]}" +do + executeUpdate "${i}" & + PIDlist+=($!) +done +echo "" +echo "Waiting for update results..." + +# Wait for all the async updates to complete +for p in "${PIDlist[@]}" +do + wait "${p}" ||: +done + +} +export -f update_apps + + + +# This is a combination of stopping previously-stopped apps and apps stuck Deploying after update +after_update_actions(){ +SECONDS=0 +count=0 +sleep 15 + +# Keep this running and exit the endless-loop based on a timer, instead of a countered-while-loop +# shellcheck disable=SC2050 +while [[ "0" != "1" ]] +do + (( count++ )) + status=$(cli -m csv -c 'app chart_release query name,update_available,human_version,human_latest_version,status' | grep "^$app_name," | awk -F ',' '{print $2}') + if [[ "$status" == "ACTIVE" && "$startstatus" == "STOPPED" ]]; then + [[ "$verbose" == "true" ]] && echo "Returing to STOPPED state.." + midclt call chart.release.scale "$app_name" '{"replica_count": 0}' &> /dev/null && echo "Stopped"|| echo "FAILED" + break + elif [[ "$SECONDS" -ge "$timeout" && "$status" == "DEPLOYING" && "$failed" != "true" ]]; then + echo -e "Error: Run Time($SECONDS) for $app_name has exceeded Timeout($timeout)\nIf this is a slow starting application, set a higher timeout with -t\nIf this applicaion is always DEPLOYING, you can disable all probes under the Healthcheck Probes Liveness section in the edit configuration\nReverting update.." + midclt call chart.release.rollback "$app_name" "{\"item_version\": \"$rollback_version\"}" &> /dev/null + [[ "$startstatus" == "STOPPED" ]] && failed="true" && after_update_actions && unset failed #run back after_update_actions function if the app was stopped prior to update + break + elif [[ "$SECONDS" -ge "$timeout" && "$status" == "DEPLOYING" && "$failed" == "true" ]]; then + echo -e "Error: Run Time($SECONDS) for $app_name has exceeded Timeout($timeout)\nThe application failed to be ACTIVE even after a rollback,\nManual intervention is required\nAbandoning" + break + elif [[ "$status" == "STOPPED" ]]; then + [[ "$count" -le 1 && "$verbose" == "true" ]] && echo "Verifying Stopped.." && sleep 15 && continue #if reports stopped on FIRST time through loop, double check + [[ "$count" -le 1 && -z "$verbose" ]] && sleep 15 && continue #if reports stopped on FIRST time through loop, double check + echo "Stopped" && break #if reports stopped any time after the first loop, assume its extermal services. + elif [[ "$status" == "ACTIVE" ]]; then + [[ "$count" -le 1 && "$verbose" == "true" ]] && echo "Verifying Active.." && sleep 15 && continue #if reports active on FIRST time through loop, double check + [[ "$count" -le 1 && -z "$verbose" ]] && sleep 15 && continue #if reports active on FIRST time through loop, double check + echo "Active" && break #if reports active any time after the first loop, assume actually active. + else + [[ "$verbose" == "true" ]] && echo "Waiting $((timeout-SECONDS)) more seconds for $app_name to be ACTIVE" + sleep 15 + continue + fi +done +} +export -f after_update_actions + +# Determine what all the required information for the App to update, check it and execute the update using the SCALE API +executeUpdate(){ + app_name=$(echo "$1" | awk -F ',' '{print $1}') #print out first catagory, name. + old_app_ver=$(echo "$1" | awk -F ',' '{print $4}' | awk -F '_' '{print $1}' | awk -F '.' '{print $1}') #previous/current Application MAJOR Version + new_app_ver=$(echo "$1" | awk -F ',' '{print $5}' | awk -F '_' '{print $1}' | awk -F '.' '{print $1}') #new Application MAJOR Version + old_chart_ver=$(echo "$1" | awk -F ',' '{print $4}' | awk -F '_' '{print $2}' | awk -F '.' '{print $1}') # Old Chart MAJOR version + new_chart_ver=$(echo "$1" | awk -F ',' '{print $5}' | awk -F '_' '{print $2}' | awk -F '.' '{print $1}') # New Chart MAJOR version + status=$(echo "$1" | awk -F ',' '{print $2}') #status of the app: STOPPED / DEPLOYING / ACTIVE + startstatus=$status + diff_app=$(diff <(echo "$old_app_ver") <(echo "$new_app_ver")) #caluclating difference in major app versions + diff_chart=$(diff <(echo "$old_chart_ver") <(echo "$new_chart_ver")) #caluclating difference in Chart versions + old_full_ver=$(echo "$1" | awk -F ',' '{print $4}') #Upgraded From + new_full_ver=$(echo "$1" | awk -F ',' '{print $5}') #Upraded To + rollback_version=$(echo "$1" | awk -F ',' '{print $4}' | awk -F '_' '{print $2}') + printf '%s\0' "${ignore[@]}" | grep -iFxqz "${app_name}" && echo -e "\n$app_name\nIgnored, skipping" && return #If application is on ignore list, skip + if [[ "$diff_app" == "$diff_chart" || "$update_all_apps" == "true" ]]; then #continue to update + [[ "$verbose" == "true" ]] && echo "Updating.." + # shellcheck disable=SC2015 + cli -c 'app chart_release upgrade release_name=''"'"$app_name"'"' &> /dev/null && echo -e "Updated $app_name\n$old_full_ver\n$new_full_ver" && after_update_actions || { echo -e "$app_name: update ${IRed}FAILED${Color_Off}"; return; } + else + echo -e "\n$app_name\nMajor Release, update manually" + return + fi +} +export -f executeUpdate diff --git a/includes/update_self.sh b/includes/update_self.sh new file mode 100755 index 00000000..7ade24e2 --- /dev/null +++ b/includes/update_self.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +## AutoUpdate TrueTool using Git +updater(){ +echo -e "${BWhite}Checking for updates...${Color_Off}" +git remote set-url origin "${targetRepo}" +BRANCH=$(git rev-parse --abbrev-ref HEAD) +git fetch -q +git update-index -q --refresh +if [[ $(git status --branch --porcelain) == *"behind"* ]]; then + echo -e "${IPurple}TrueTool requires update${Color_Off}" + git reset --hard -q + git checkout -q "${BRANCH}" + git pull -q + echo "script updated" + if [[ "$CHANGED" == "true" ]]; then + echo "LOOP DETECTED, exiting" + exit 1 + else + echo "restarting script after update..." + export CHANGED="true" + . "${SCRIPT_DIR}/truetool.sh" "$@" + exit + fi +else + echo -e "${IGreen}script up-to-date${Color_Off}" + export CHANGED="false" +fi +echo "" +} +export -f updater diff --git a/setup.py b/setup.py deleted file mode 100644 index fee39cbb..00000000 --- a/setup.py +++ /dev/null @@ -1,55 +0,0 @@ -from setuptools import setup, find_packages -from os.path import abspath, dirname, join - -# Fetches the content from README.md -# This will be used for the "long_description" field. -README_MD = open(join(dirname(abspath(__file__)), "README.md")).read() - -setup( - name="truetool", - version="3.0.3", - - # The packages that constitute your project. - # For my project, I have only one - "pydash". - # Either you could write the name of the package, or - # alternatively use setuptools.findpackages() - # - # If you only have one file, instead of a package, - # you can instead use the py_modules field instead. - # EITHER py_modules OR packages should be present. - packages=find_packages(), - - entry_points = { - 'console_scripts': ['truetool=truetool.command_line:main'], - }, - - # The description that will be shown on PyPI. - # Keep it short and concise - # This field is OPTIONAL - description="An easy utility to managed frequently used TrueNAS SCALE CLI features", - - # The content that will be shown on your project page. - # In this case, we're displaying whatever is there in our README.md file - # This field is OPTIONAL - long_description=README_MD, - - # Now, we'll tell PyPI what language our README file is in. - # In my case it is in Markdown, so I'll write "text/markdown" - # Some people use reStructuredText instead, so you should write "text/x-rst" - # If your README is just a text file, you have to write "text/plain" - # This field is OPTIONAL - long_description_content_type="text/markdown", - - # The url field should contain a link to a git repository, the project's website - # or the project's documentation. I'll leave a link to this project's Github repository. - # This field is OPTIONAL - url="https://github.com/truecharts/truetool", - - # The author name and email fields are self explanatory. - # These fields are OPTIONAL - author_name="truecharts", - author_email="into@truecharts.org", - - # For additional fields, check: - # https://github.com/pypa/sampleproject/blob/master/setup.py -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/truetool.sh b/truetool.sh new file mode 100755 index 00000000..a7cffded --- /dev/null +++ b/truetool.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# Constants +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; +dir=$(basename "$SCRIPT_DIR") + +# Change this if you want to fork the project +enableUpdate="true" +targetRepo="https://github.com/truecharts/truetool.git" + + +# Includes +# shellcheck source=includes/backup.sh +source includes/backup.sh +# shellcheck source=includes/chores.sh +source includes/chores.sh +# shellcheck source=includes/colors.sh +source includes/colors.sh +# shellcheck source=includes/dns.sh +source includes/dns.sh +# shellcheck source=includes/help.sh +source includes/help.sh +# shellcheck source=includes/mount.sh +source includes/mount.sh +# shellcheck source=includes/no_args.sh +source includes/no_args.sh +# shellcheck source=includes/title.sh +source includes/title.sh +# shellcheck source=includes/update.sh +source includes/update.sh +# shellcheck source=includes/update_self.sh +source includes/update_self.sh + +# CD to the folder containing the script to ensure consistent runs +cd "${SCRIPT_DIR}" || echo -e "${IRed}ERROR: Something went wrong accessing the script directory${Color_Off}" + +title + +[[ "$enableUpdate" == "true" ]] && updater "$@" + +#If no argument is passed, kill the script. +if [[ -z "$*" || "-" == "$*" || "--" == "$*" ]]; then + no_args +else + + # Parse script options + while getopts ":si:b:t:uUpSv-:" opt + do + case $opt in + -) + case "${OPTARG}" in + help) + help="true" + ;; + dns) + dns="true" + ;; + mount) + mountPVC="true" + ;; + restore) + restore="true" + ;; + delete-backup) + deleteBackup="true" + ;; + list-backups) + listBackups="true" + ;; + helm-enable) + helmEnable="true" + ;; + apt-enable) + aptEnable="true" + ;; + no-color) + noColor + ;; + *) + echo -e "Invalid Option \"--$OPTARG\"\n" && help + exit + ;; + esac + ;; + \?) + echo -e "Invalid Option \"-$OPTARG\"\n" && help + exit + ;; + :) + echo -e "Option: \"-$OPTARG\" requires an argument\n" && help + exit + ;; + b) + re='^[0-9]+$' + number_of_backups=$OPTARG + ! [[ $OPTARG =~ $re ]] && echo -e "Error: -b needs to be assigned an interger\n\"""$number_of_backups""\" is not an interger" >&2 && exit + [[ "$number_of_backups" -le 0 ]] && echo "Error: Number of backups is required to be at least 1" && exit + ;; + i) + ignore+=("$OPTARG") + ;; + t) + re='^[0-9]+$' + timeout=$OPTARG + ! [[ $timeout =~ $re ]] && echo -e "Error: -t needs to be assigned an interger\n\"""$timeout""\" is not an interger" >&2 && exit + ;; + s) + sync="true" + ;; + U) + update_all_apps="true" + ;; + u) + update_apps="true" + ;; + p) + prune="true" + ;; + v) + verbose="true" + ;; + *) + echo -e "Invalid Option \"--$OPTARG\"\n" && help + exit + ;; + esac + done +fi + +## Exit if incompatable functions are called +[[ "$update_all_apps" == "true" && "$update_apps" == "true" ]] && echo -e "-U and -u cannot BOTH be called" && exit + +## Exit if unsafe combinations are used +# Restore and update right after eachother, might cause super weird issues tha are hard to bugtrace +[[ ( "$update_all_apps" == "true" || "$update_apps" == "true" ) && ( "$restore" == "true" ) ]] && echo -e "Update and Restore cannot both be done in the same run..." && exit + +# Backup Deletion is generally considered to be a "once in a while" thing and not great to sync with automated updates for that reason +[[ ( "$update_all_apps" == "true" || "$update_apps" == "true" ) && ( "$deleteBackup" == "true" ) ]] && echo -e "Update Backup-Deletion cannot both be done in the same run..." && exit + +# Backup Deletion is generally considered to be a "once in a while" thing and not great to sync with automated updates for that reason +[[ ( "$update_all_apps" == "true" || "$update_apps" == "true" ) && ( "$deleteBackup" == "true" ) ]] && echo -e "Update and Backup-Deletion cannot both be done in the same run..." && exit + +# Backup listing is a printout, which would either clutter the output or be already outdated when combined with backup +[[ ( "$update_all_apps" == "true" || "$update_apps" == "true" ) && ( "$listBackups" == "true" ) ]] && echo -e "Update and Listing Backups cannot both be done in the same run..." && exit + +# Backup backup would be done after a backup is restored, which would lead to a backup that is... the same as the one restored... +[[ ( "$restore" == "true" && "$number_of_backups" -ge 1 )]] && echo -e "Restoring a backup and making a backup cannot both be done in the same run..." && exit + +# While technically possible, this is asking for user error... where a user by habit mistakes one prompt, for the other. +[[ ( "$restore" == "true" && "$deleteBackup" == "true" )]] && echo -e "restoring a backup and deleting a backup cannot both be done in the same run..." && exit + + +# Continue to call functions in specific order +[[ "$help" == "true" ]] && help +[[ "$helmEnable" == "true" ]] && helmEnable +[[ "$aptEnable" == "true" ]] && aptEnable +[[ "$aptEnable" == "true" || "$helmEnable" == "true" ]] && exit +[[ "$listBackups" == "true" ]] && listBackups && exit +[[ "$deleteBackup" == "true" ]] && deleteBackup && exit +[[ "$dns" == "true" ]] && dns && exit +[[ "$restore" == "true" ]] && restore && exit +[[ "$mountPVC" == "true" ]] && mountPVC && exit +[[ "$number_of_backups" -ge 1 ]] && backup +[[ "$sync" == "true" ]] && sync +[[ "$update_all_apps" == "true" || "$update_apps" == "true" ]] && update_apps +[[ "$prune" == "true" ]] && prune diff --git a/truetool/__init__.py b/truetool/__init__.py deleted file mode 100644 index cb06dc45..00000000 --- a/truetool/__init__.py +++ /dev/null @@ -1,238 +0,0 @@ -import subprocess -import sys -import argparse -import time -from datetime import datetime - -class Chart(object): - def __setattr__(self, name, value): - if name == 'status': - value = value.casefold() - if 'update_available' in name: - value = True if value.casefold() == "true" else False - super(Chart, self).__setattr__(name, value) - - def new_attr(self, attr): - setattr(self, attr, attr) - -INSTALLED_CHARTS = [] - -def parse_headers(charts: str): - for line in charts.split("\n"): - if line.startswith("+-"): - continue - if "name" in line.casefold(): - return [col.strip() for col in line.casefold().strip().split("|") if col and col != ""] - -def parse_charts(charts: str): - headers = parse_headers(charts) - table_data = charts.split("\n")[3:-2:] # Skip the header part of the table - for row in table_data: - row = [section.strip() for section in row.split("|") if section and section != ""] - chart = Chart() - for item in zip(headers, row): - setattr(chart, item[0], item[1]) - INSTALLED_CHARTS.append(chart) - -def check_semver(current: str, latest: str): - split_current_semver = current.split(".", 3) - split_latest_semver = latest.split(".", 3) - if split_current_semver[0] != split_latest_semver[0]: - type="major" - if VERSIONING == "major": - return True - if split_current_semver[1] != split_latest_semver[1]: - type="minor" - if VERSIONING != "patch": - return True - if split_current_semver[2] != split_latest_semver[2]: - type="patch" - return True - return False - - -def execute_upgrades(): - if UPDATE: - if ALL: - if CATALOG == "ALL": - filtered = filter(lambda a: a.update_available, INSTALLED_CHARTS) - else: - filtered = filter(lambda a: a.update_available and a.catalog == CATALOG, INSTALLED_CHARTS) - else: - if CATALOG == "ALL": - filtered = filter(lambda a: a.update_available and a.status == "active", INSTALLED_CHARTS) - else: - filtered = filter(lambda a: a.update_available and a.status == "active" and a.catalog == CATALOG, INSTALLED_CHARTS) - for chart in filtered: - pre_update_ver = chart.human_version - post_update_ver = chart.human_latest_version - split_current_version = chart.human_version.split("_", 1) - current_version = split_current_version[1] - split_latest = chart.human_latest_version.split("_", 1) - latest_version = split_latest[1] - if check_semver(current_version, latest_version): - print(f"Updating {chart.name}... \n") - pre_update_ver = chart.human_version - result = subprocess.run(['cli', '-c', f'app chart_release upgrade release_name="{chart.name}"'], capture_output=True) - post_update_ver = chart.human_latest_version - if "Upgrade complete" not in result.stdout.decode('utf-8'): - print(f"{chart.name} failed to upgrade. \n{result.stdout.decode('utf-8')}") - else: - print(f"{chart.name} upgraded ({pre_update_ver} --> {post_update_ver})") - else: - print("Update disabled, skipping...") - -def fetch_charts(): - rawcharts = subprocess.run(["cli", "-c", "app chart_release query"], stdout=subprocess.PIPE) - charts = rawcharts.stdout.decode('utf-8') - return(charts) - -def process_args(): - global CATALOG - global VERSIONING - global SYNC - global PRUNE - global ALL - global BACKUP - global UPDATE - global RESTORE - global LIST - global DELETE - parser = argparse.ArgumentParser(description='TrueCharts CLI Tool. Warning: please do NOT combine short arguments like -ubs always use -u -b -s etc.') - parser.add_argument('-c', '--catalog', nargs='?', default='ALL', help='name of the catalog you want to process in caps. Or "ALL" to render all catalogs.') - parser.add_argument('-v', '--versioning', nargs='?', default='minor', help='Name of the versioning scheme you want to update. Options: major, minor or patch. Defaults to minor') - parser.add_argument('-b', '--backup', nargs='?', const='14', help='backup the complete Apps system prior to updates, add a number to specify the max old backups to keep') - parser.add_argument('-r', '--restore', nargs='?', help='restore a previous backup, disables all other features') - parser.add_argument('-d', '--delete', nargs='?', help='delete a specific backup') - parser.add_argument('-s', '--sync', action="store_true", help='sync catalogs before trying to update') - parser.add_argument('-u', '--update', action="store_true", help='update the Apps in the selected catalog') - parser.add_argument('-p', '--prune', action="store_true", help='prune old docker images after update') - parser.add_argument('-a', '--all', action="store_true", help='update all apps for said catalog, including "stopped" or "stuck" apps') - parser.add_argument('-l', '--list', action="store_true", help='lists existing backups') - args = parser.parse_args() - CATALOG = args.catalog - VERSIONING = args.versioning - RESTORE = args.restore - BACKUP = args.backup - DELETE = args.delete - if args.update: - UPDATE = True - else: - UPDATE = False - if args.sync: - SYNC = True - else: - SYNC = False - if args.prune: - PRUNE = True - else: - PRUNE = False - if args.all: - ALL = True - else: - ALL = False - if args.list: - LIST = True - else: - LIST = False - - -def sync_catalog(): - if SYNC: - print("Syncing Catalogs...\n") - process = subprocess.Popen(["cli", "-c", "app catalog sync_all"], stdout=subprocess.PIPE) - while process.poll() is None: - lines = process.stdout.readline() - print (lines.decode('utf-8')) - temp = process.stdout.read() - if temp: - print (temp.decode('utf-8')) - else: - print("Catalog Sync disabled, skipping...") - -def docker_prune(): - if PRUNE: - print("Pruning old docker images...\n") - process = subprocess.run(["docker", "image", "prune", "-af"], stdout=subprocess.PIPE) - print("Images pruned.\n") - else: - print("Container Image Pruning disabled, skipping...") - -def apps_backup(): - if BACKUP: - print(f"Cleaning old backups to a max. of {BACKUP}...\n") - backups_fetch = get_backups_names() - backups_cleaned = [k for k in backups_fetch if 'TrueTool' in k] - backups_remove = backups_cleaned[:len(backups_cleaned)-int(BACKUP)] - for backup in backups_remove: - backups_delete(backup) - - print("Running App Backup...\n") - now = datetime.now() - command = "app kubernetes backup_chart_releases backup_name=TrueTool_"+now.strftime("%Y_%m_%d_%H_%M_%S") - process = subprocess.Popen(["cli", "-c", command], stdout=subprocess.PIPE) - while process.poll() is None: - lines = process.stdout.readline() - print (lines.decode('utf-8')) - temp = process.stdout.read() - if temp: - print (temp.decode('utf-8')) - else: - print("Backup disabled, skipping...") - -def backups_list(): - if LIST: - print("Generating Backup list...\n") - backups = get_backups_names() - for backup in backups: - print(f"{backup}") - -def backups_delete(backup: str): - print(f"removing {backup}...") - process = subprocess.run(["midclt", "call", "kubernetes.delete_backup", backup], stdout=subprocess.PIPE) - -def get_backups_names(): - names = [] - process = subprocess.run(["cli", "-c", "app kubernetes list_backups"], stdout=subprocess.PIPE) - output = process.stdout.decode('utf-8') - for line in output.split("\n"): - if line.startswith("+-"): - continue - else: - rowlist = [col.strip() for col in line.strip().split("|") if col and col != ""] - if rowlist: - names.append(rowlist[0]) - names.sort() - return names - -def apps_restore(): - print("Running Backup Restore...\n") - process = subprocess.run(["midclt", "call", "kubernetes.restore_backup", RESTORE], stdout=subprocess.PIPE) - time.sleep(5) - print("Restoration started, please check the restoration process in the TrueNAS SCALE Web GUI...\n") - print("Please remember: This can take a LONG time.\n") - -def run(): - process_args() - print("Starting TrueCharts TrueTool...\n") - if RESTORE: - apps_restore() - elif LIST: - backups_list() - elif DELETE: - backups_delete(DELETE) - else: - apps_backup() - sync_catalog() - charts = fetch_charts() - parse_charts(charts) - print("Executing Updates...\n") - execute_upgrades() - docker_prune() - print("TrueTool Finished\n") - exit(0) - - -if __name__ == '__main__': - run() - diff --git a/truetool/command_line.py b/truetool/command_line.py deleted file mode 100644 index 33e703bc..00000000 --- a/truetool/command_line.py +++ /dev/null @@ -1,4 +0,0 @@ -import truetool - -def main(): - truetool.run() \ No newline at end of file