1#!/usr/bin/env bash
2################################################################################
3# Copyright (C) 2015 Daniel Preussker, QuxLabs UG <preussker@quxlabs.com>
4# Copyright (C) 2016 Layne "Gorian" Breitkreutz <Layne.Breitkreutz@thelenon.com>
5# Copyright (C) 2017 Tony Murray <murraytony@gmail.com>
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <https://www.gnu.org/licenses/>.
18################################################################################
19
20#######################################
21# CONSTANTS
22#######################################
23# define DAILY_SCRIPT as the full path to this script and LIBRENMS_DIR as the directory this script is in
24DAILY_SCRIPT=$(readlink -f "$0")
25LIBRENMS_DIR=$(dirname "$DAILY_SCRIPT")
26COMPOSER="php ${LIBRENMS_DIR}/scripts/composer_wrapper.php --no-interaction"
27
28# set log_file, using librenms 'log_dir' config setting, if set
29# otherwise we default to <LibreNMS Install Directory>/logs
30LOG_DIR=$(php -r "@include '${LIBRENMS_DIR}/config.php'; echo isset(\$config['log_dir']) ? \$config['log_dir'] : '${LIBRENMS_DIR}/logs';")
31
32# get the librenms user
33# shellcheck source=.env.example
34source "${LIBRENMS_DIR}/.env"
35LIBRENMS_USER="${LIBRENMS_USER:-librenms}"
36LIBRENMS_USER_ID=$(id -u "$LIBRENMS_USER")
37
38#######################################
39# Fancy-Print and run commands
40# Globals:
41#   LOG_DIR
42# Arguments:
43#   Text
44#   Command
45# Returns:
46#   Exit-Code of Command
47#######################################
48status_run() {
49    # Explicitly define our arguments
50    local args arg_text arg_command arg_option log_file exit_code tmp log_file
51
52    args=("$@")
53    arg_text=$1
54    arg_command=$2
55    arg_option=$3
56    log_file=${LOG_DIR}/daily.log
57
58    # set log_file, using librenms $config['log_dir'], if set
59    # otherwise we default to ./logs/daily.log
60
61    printf "%-50s" "${arg_text}"
62    echo "${arg_text}" >> "${log_file}"
63    tmp=$(bash -c "${arg_command}" 2>&1)
64    exit_code=$?
65    echo "${tmp}" >> "${log_file}"
66    echo "Returned: ${exit_code}" >> "${log_file}"
67
68    # print OK if the command ran successfully
69    # or FAIL otherwise (non-zero exit code)
70    if [[ "${exit_code}" == "0" ]]; then
71        printf " \\033[0;32mOK\\033[0m\\n"
72    else
73        printf " \\033[0;31mFAIL\\033[0m\\n"
74        if [[ "${arg_option}" == "update" ]]; then
75            php "${LIBRENMS_DIR}/daily.php" -f notify -o "${tmp}"
76        fi
77        if [[ -n "${tmp}" ]]; then
78            # print output in case of failure
79            echo "${tmp}"
80        fi
81    fi
82    return ${exit_code}
83}
84
85#######################################
86# Call daily.php
87# Globals:
88#   LIBRENMS_DIR
89# Arguments:
90#   args:
91#        Array of arguments to pass to
92#        daily.php
93# Returns:
94#   Exit-Code of Command
95#######################################
96call_daily_php() {
97    local args
98
99    args=("$@")
100
101    for arg in "${args[@]}"; do
102        php "${LIBRENMS_DIR}/daily.php" -f "${arg}"
103    done
104}
105
106#######################################
107# Send result of a notifiable process to php code for processing
108# Globals:
109#   LIBRENMS_DIR
110# Arguments:
111#   args:
112#        Type: update
113#        Result: 1 for success, 0 for failure
114# Returns:
115#   Exit-Code of Command
116#######################################
117set_notifiable_result() {
118    local args arg_type arg_result
119
120    args=("$@")
121    arg_type=$1
122    arg_result=$2
123
124    php "${LIBRENMS_DIR}/daily.php" -f handle_notifiable -t "${arg_type}" -r "${arg_result}"
125}
126
127#######################################
128# Check the PHP and Python version and branch and switch to the appropriate branch
129# Returns:
130#   Exit-Code: 0 >= min ver, 1 < min ver
131#######################################
132check_dependencies() {
133    local branch ver_56 ver_71 ver_72 ver_73 python3 python_deps phpver pythonver old_branches msg
134
135    branch=$(git rev-parse --abbrev-ref HEAD)
136    scripts/check_requirements.py > /dev/null 2>&1 || pip3 install -r requirements.txt > /dev/null 2>&1
137
138    ver_56=$(php -r "echo (int)version_compare(PHP_VERSION, '5.6.4', '<');")
139    ver_71=$(php -r "echo (int)version_compare(PHP_VERSION, '7.1.3', '<');")
140    ver_72=$(php -r "echo (int)version_compare(PHP_VERSION, '7.2.5', '<');")
141    ver_73=$(php -r "echo (int)version_compare(PHP_VERSION, '7.3', '<');")
142    python3=$(python3 -c "import sys;print(int(sys.version_info < (3, 4)))" 2> /dev/null)
143    python_deps=$("${LIBRENMS_DIR}/scripts/check_requirements.py" > /dev/null 2>&1; echo $?)
144    phpver="master"
145    pythonver="master"
146
147    old_branches="^(php53|php56|php71-python2|php72)$"
148    if [[ $branch =~ $old_branches ]] && [[ "$ver_73" == "0" && "$python3" == "0" && "$python_deps" == "0" ]]; then
149        status_run "Supported PHP and Python version, switched back to master branch." 'git checkout master'
150    elif [[ "$ver_56" != "0" ]]; then
151        phpver="php53"
152        if [[ "$branch" != "php53" ]]; then
153            status_run "Unsupported PHP version, switched to php53 branch." 'git checkout php53'
154        fi
155    elif [[ "$ver_71" != "0" ]]; then
156        phpver="php56"
157        if [[ "$branch" != "php56" ]]; then
158            status_run "Unsupported PHP version, switched to php56 branch." 'git checkout php56'
159        fi
160    elif [[ "$ver_72" != "0" || "$python3" != "0" || "$python_deps" != "0" ]]; then
161        msg=""
162        if [[ "$ver_72" != "0" ]]; then
163            msg="Unsupported PHP version, $msg"
164            phpver="php71"
165        fi
166        if [[ "$python3" != "0" ]]; then
167            msg="python3 is not available, $msg"
168            pythonver="python3-missing"
169        elif [[ "$python_deps" != "0" ]]; then
170            msg="Python 3 dependencies missing, $msg"
171            pythonver="python3-deps"
172        fi
173
174        if [[ "$branch" != "php71-python2" ]]; then
175            status_run "${msg}switched to php71-python2 branch." 'git checkout php71-python2'
176        fi
177    elif [[ "$ver_73" != "0" ]]; then
178        phpver="php72"
179        if [[ "$branch" != "php72" ]]; then
180            status_run "Unsupported PHP version, switched to php72 branch." 'git checkout php72'
181        fi
182    fi
183
184    set_notifiable_result phpver ${phpver}
185    set_notifiable_result pythonver ${pythonver}
186
187    if [[ "$phpver" == "master" && "$pythonver" == "master" ]]; then
188        return 0
189    fi
190    return 1
191}
192
193#######################################
194# Compare two numeric versions
195# Arguments:
196#   args:
197#        version 1
198#        version 2
199#        parts: Number of parts to compare, from the left, compares all if unspecified
200# Returns:
201#   Exit-Code: 0: if equal 1: if 1 > 2  2: if 1 < 2
202#######################################
203version_compare () {
204    local i ver1 ver2 parts1 parts2
205
206    if [[ "$1" == "$2" ]]; then
207        return 0
208    fi
209
210    IFS=. read -ra ver1 <<< "$1"
211    IFS=. read -ra ver2 <<< "$2"
212
213    parts2=${#ver2[@]}
214    [[ -n $3 ]] && parts2=$3
215
216    # fill empty fields in ver1 with zeros
217    for ((i=${#ver1[@]}; i<parts2; i++)); do
218        ver1[i]=0
219    done
220
221    parts1=${#ver1[@]}
222    [[ -n $3 ]] && parts1=$3
223
224    for ((i=0; i<parts1; i++)); do
225        if [[ -z ${ver2[i]} ]]; then
226            # fill empty fields in ver2 with zeros
227            ver2[i]=0
228        fi
229        if ((10#${ver1[i]} > 10#${ver2[i]})); then
230            return 1
231        fi
232        if ((10#${ver1[i]} < 10#${ver2[i]})); then
233            return 2
234        fi
235    done
236    return 0
237}
238
239
240#######################################
241# Entry into program
242# Globals:
243#   LIBRENMS_DIR
244# Arguments:
245#
246# Returns:
247#   Exit-Code of Command
248#######################################
249main () {
250    local arg old_version new_version branch options
251
252    arg="$1"
253    old_version="$2"
254    new_version="$3"
255    old_version="${old_version:=unset}"  # if $1 is unset, make it mismatch for pre-update daily.sh
256
257    cd "${LIBRENMS_DIR}" || exit 1
258
259    # if not running as $LIBRENMS_USER (unless $LIBRENMS_USER = root), relaunch
260    if [[ "$LIBRENMS_USER" != "root" ]]; then
261        # only try to su if we are root (or sudo)
262        if [[ "$EUID" -eq 0 ]]; then
263            echo "Re-running ${DAILY_SCRIPT} as ${LIBRENMS_USER} user"
264            sudo -u "$LIBRENMS_USER" "$DAILY_SCRIPT" "$@"
265            exit
266        fi
267
268        if [[ "$EUID" -ne "$LIBRENMS_USER_ID" ]]; then
269            printf "\\033[0;93mWARNING\\033[0m: You should run this script as %s\\n" "${LIBRENMS_USER}"
270        fi
271    fi
272
273    # make sure autoload.php exists before trying to run any php that may require it
274    if [ ! -f "${LIBRENMS_DIR}/vendor/autoload.php" ]; then
275        ${COMPOSER} install --no-dev
276    fi
277
278    if [[ -z "$arg" ]]; then
279        up=$(php daily.php -f update >&2; echo $?)
280        if [[ "$up" == "0" ]]; then
281            ${DAILY_SCRIPT} no-code-update
282            set_notifiable_result update 1  # make sure there are no update notifications if update is disabled
283            exit
284        fi
285
286        check_dependencies
287        php_ver_ret=$?
288
289        # make sure the vendor directory is clean
290        git checkout vendor/ --quiet > /dev/null 2>&1
291
292        update_res=0
293        if [[ "$up" == "1" ]] || [[ "$php_ver_ret" == "1" ]]; then
294            # Update current branch to latest
295            branch=$(git rev-parse --abbrev-ref HEAD)
296            if [[ "$branch" == "HEAD" ]]; then
297                # if the branch is HEAD, then we are not on a branch, checkout master
298                git checkout master
299            fi
300
301            old_ver=$(git rev-parse --short HEAD)
302            status_run 'Updating to latest codebase' 'git pull --quiet' 'update'
303            update_res=$?
304            new_ver=$(git rev-parse --short HEAD)
305        else
306            # Update to last Tag
307            old_ver=$(git describe --exact-match --tags "$(git log -n1 --pretty='%h')" 2> /dev/null)
308
309            # fetch new tags
310            status_run 'Fetching new release information' "git fetch --tags" 'update'
311
312            # collect versions full, base, new tag and hash
313            IFS='-' read -ra full_version <<< "$(git describe --tags 2>/dev/null)"
314            base_ver="${full_version[0]}"
315            latest_hash=$(git rev-list --tags --max-count=1)
316            latest_tag=$(git describe --exact-match --tags "${latest_hash}")
317
318            #compare current base and latest version numbers (only the first two sections)
319            version_compare "$base_ver" "$latest_tag" 2
320            newer_check=$?
321
322            if [[ -z $old_ver ]] && [[ $newer_check -eq 0 ]]; then
323                echo 'Between releases, waiting for newer release'
324            else
325                status_run 'Updating to latest release' "git checkout ${latest_hash}" 'update'
326                update_res=$?
327                new_ver=$(git describe --exact-match --tags "$(git log -n1 --pretty='%h')")
328            fi
329        fi
330
331        if (( update_res > 0 )); then
332            set_notifiable_result update 0
333        fi
334
335        # Call ourself again in case above pull changed or added something to daily.sh
336        ${DAILY_SCRIPT} post-pull "${old_ver}" "${new_ver}"
337    else
338        case $arg in
339            no-code-update)
340                # Updates of the code are disabled, just check for schema updates
341                # and clean up the db.
342                status_run 'Updating SQL-Schema' 'php includes/sql-schema/update.php'
343                status_run 'Cleaning up DB' "$DAILY_SCRIPT cleanup"
344            ;;
345            post-pull)
346                # re-check dependencies after pull with the new code
347                check_dependencies
348
349                # Check for missing vendor dir
350                if [ ! -f vendor/autoload.php ]; then
351                    git checkout 609676a9f8d72da081c61f82967e1d16defc0c4e -- vendor/
352                    git reset HEAD vendor/  # don't add vendor directory to the index
353                fi
354
355                status_run 'Updating Composer packages' "${COMPOSER} install --no-dev" 'update'
356
357                # Check if we need to revert (Must be in post pull so we can update it)
358                if [[ "$old_version" != "$new_version" ]]; then
359                    check_dependencies # check php and python version and switch branches
360
361                    # new_version may be incorrect if we just switch branches... ignoring that detail
362                    status_run "Updated from $old_version to $new_version" ''
363                    set_notifiable_result update 1  # only clear the error if update was a success
364                fi
365
366                # List all tasks to do after pull in the order of execution
367                status_run 'Updating SQL-Schema' 'php includes/sql-schema/update.php'
368                status_run 'Updating submodules' "$DAILY_SCRIPT submodules"
369                status_run 'Cleaning up DB' "$DAILY_SCRIPT cleanup"
370                status_run 'Fetching notifications' "$DAILY_SCRIPT notifications"
371                status_run 'Caching PeeringDB data' "$DAILY_SCRIPT peeringdb"
372                status_run 'Caching Mac OUI data' "$DAILY_SCRIPT mac_oui"
373            ;;
374            cleanup)
375                # Cleanups
376                options=("refresh_alert_rules"
377                               "refresh_os_cache"
378                               "refresh_device_groups"
379                               "recalculate_device_dependencies"
380                               "syslog"
381                               "eventlog"
382                               "authlog"
383                               "callback"
384                               "device_perf"
385                               "purgeusers"
386                               "bill_data"
387                               "alert_log"
388                               "rrd_purge"
389                               "ports_fdb"
390                               "route"
391                               "ports_purge")
392                call_daily_php "${options[@]}"
393            ;;
394            submodules)
395                # Init+Update our submodules
396                git submodule --quiet init
397                git submodule --quiet update
398            ;;
399            notifications)
400                # Get notifications
401                options=("notifications")
402                call_daily_php "${options[@]}"
403            ;;
404            peeringdb)
405                options=("peeringdb")
406                call_daily_php "${options[@]}"
407            ;;
408            mac_oui)
409                options=("mac_oui")
410                call_daily_php "${options[@]}"
411        esac
412    fi
413}
414
415main "$@"
416