From 4ecd51c2fa0acb2c192c92d0266dfaba26ec516e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20-=20Le=20Filament?= <remi@le-filament.com> Date: Wed, 16 Jun 2021 20:01:52 +0200 Subject: [PATCH] Add check gitlab from https://gitlab.com/6uellerBpanda updating shebang --- check_gitlab.rb | 404 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 check_gitlab.rb diff --git a/check_gitlab.rb b/check_gitlab.rb new file mode 100644 index 0000000..f8d3dc5 --- /dev/null +++ b/check_gitlab.rb @@ -0,0 +1,404 @@ +#!/opt/gitlab/embedded/bin/ruby +# frozen_string_literal: true + +# +# Gitlab Plugin +# == +# Author: Marco Peterseil +# Created: 03-2017 +# License: GPLv3 - http://www.gnu.org/licenses +# URL: https://gitlab.com/6uellerBpanda/check_gitlab +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +require 'optparse' +require 'net/https' +require 'json' +require 'date' + +version = 'v0.6.0' + +# optparser +banner = <<~HEREDOC + check_gitlab #{version} [https://gitlab.com/6uellerBpanda/check_gitlab]\n + This plugin checks various parameters of Gitlab\n + Mode: + health Check the Gitlab web endpoint for health + services Check if any service of 'gitlab-ctl status' is down + group-size Check size of group in MB + ci-pipeline-duration Check duration of a CI pipeline + ci-pipeline-status Check status of a CI pipeline + ci-runner-status Check status of CI runners + ci-runner-jobs-duration Check duration (in seconds) of running jobs of CI runners + license-expires Check remaining days when license expires - only warning status possible + license-overage Check if more active then licensed users are present + sidekiq-jobs Check size of sidekiq jobs + + Usage: #{File.basename(__FILE__)} [options] +HEREDOC + +options = {} +OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength + opts.banner = banner.to_s + opts.separator '' + opts.separator 'Options:' + opts.on('-s', '--address ADDRESS', '-H', 'Gitlab address') do |s| + options[:address] = s + end + opts.on('-t', '--token TOKEN', 'Access token') do |t| + options[:token] = t + end + opts.on('-i', '--id ID', 'Project/Group/CI-Runner ID') do |i| + options[:id] = i + end + opts.on('-k', '--insecure', 'No ssl verification') do |k| + options[:insecure] = k + end + opts.on('-m', '--mode MODE', 'Mode to check') do |m| + options[:mode] = m + end + opts.on('-n', '--name NAME', 'Name of group (regex), or sidekiq jobs') do |n| + options[:name] = n + end + opts.on('-e', '--exclude EXCLUDE', 'Exclude (regex)') do |e| + options[:exclude] = e + end + opts.on('--status STATUS', 'Status to use') do |status| + options[:status] = status + end + opts.on('-w', '--warning WARNING', 'Warning threshold') do |w| + options[:warning] = w + end + opts.on('-c', '--critical CRITICAL', 'Critical threshold') do |c| + options[:critical] = c + end + opts.on('-d', '--debug', 'Print extra debugging/status output (available for health check)') do |d| + options[:debug] = d + end + opts.on('-v', '--version', 'Print version information') do + puts "check_gitlab #{version}" + end + opts.on('-h', '--help', 'Show this help message') do + puts opts + end + ARGV.push('-h') if ARGV.empty? +end.parse! + +# check gitlab +class CheckGitlab + def initialize(options) # rubocop:disable Metrics/MethodLength + @options = options + init_arr + validate_check_modes + health_check + ci_pipeline_status + ci_pipeline_duration + ci_runner_jobs_duration + ci_runner_status + services_check + group_size + license_expire + license_overage + sidekiq_jobs + end + + #--------# + # HELPER # + #--------# + + def init_arr + @perfdata = [] + @message = [] + @critical = [] + @warning = [] + @okays = [] + end + + # define some helper methods for naemon + def ok_msg(message) + puts "OK - #{message}" + @debug.to_s + exit 0 + end + + def crit_msg(message) + puts "Critical - #{message}" + @debug.to_s + exit 2 + end + + def warn_msg(message) + puts "Warning - #{message}" + @debug.to_s + exit 1 + end + + def unk_msg(message) + puts "Unknown - #{message}" + exit 3 + end + + def validate_check_modes + check_modes = %w[ + health ci-pipeline-duration ci-pipeline-status + ci-runner-jobs-duration ci-runner-status services group-size + sidekiq-jobs license-expire license-overage + ] + warn_msg('Mode not found. Check configuration.') unless check_modes.include?(@options[:mode]) + end + + # convert the bytes + def convert_to_mb(data:) + @used_size = data.to_i / 1024 / 1024 + end + + # debug output + def debug(data:) + @debug = data.map { |k, v| "\n#{k} is #{v[0]['status']}" }.join + end + + def build_perfdata(perfdata:) + @perfdata << "#{perfdata};#{@options[:warning]};#{@options[:critical]}" + end + + # build service output + def build_output(msg:) + @message = msg + end + + ### helpers for threshold checking + def check_thresholds_int(data:, type: 'non-array') + if data > @options[:critical].to_i + @critical << @message + elsif data > @options[:warning].to_i + @warning << @message + else + @okays << @message + end + # make the final step + build_final_output unless type != 'non-array' + end + + def check_thresholds_string(data:) + case data + when /#{@options[:critical]}/ + @critical << @message + when /#{@options[:warning]}/ + @warning << @message + else + @okays << @message + end + build_final_output(perf_enabled: false) + end + + # mix everything together for exit + def build_final_output(pretext: '', perf_enabled: true) + perf_output = " | #{@perfdata.join(' ')}" unless perf_enabled == false + if @critical.any? + crit_msg(pretext + @critical.join(', ') + perf_output.to_s) + elsif @warning.any? + warn_msg(pretext + @warning.join(', ') + perf_output.to_s) + else + ok_msg(pretext + @okays.join(', ') + perf_output.to_s) + end + end + + #----------# + # API AUTH # + #----------# + + # create url + def url(path:) + uri = URI("#{@options[:address]}/#{path}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options[:insecure] + request = Net::HTTP::Get.new(uri.request_uri) + request.add_field 'PRIVATE-TOKEN', @options[:token] if @options[:mode] != 'health' + @response = http.request(request) + rescue StandardError => e + unk_msg(e) + end + + # init http req + def http_connect(path:) + url(path: path) + check_http_response + end + + # check http response + def check_http_response + unk_msg(@response.message).to_s if @response.code != '200' + end + + #--------# + # CHECKS # + #--------# + + ###--- HEALTH CHECK ---### + def health_check + return unless @options[:mode] == 'health' + http_connect(path: '-/readiness?all=1') + data_json = JSON.parse(@response.body) + data_json.delete('status') + unhealthy_probes = data_json.reject { |_k, v| v[0]['status'] == 'ok' } + debug(data: data_json) if @options[:debug] + if unhealthy_probes.empty? + ok_msg('Gitlab probes are in healthy state') + else + warn_msg(unhealthy_probes.map { |k, _v| k }.join(', ') + ' probe has problems') # rubocop:disable Style/StringConcatenation + end + end + + ###--- CI-PIPELINE STATUS CHECK ---### + def ci_pipeline_status + return unless @options[:mode] == 'ci-pipeline-status' + http_connect(path: "api/v4/projects/#{@options[:id]}/pipelines") + ci_pipeline_data = JSON.parse(@response.body).first + build_output(msg: "Status of pipeline ##{ci_pipeline_data['id']}: #{ci_pipeline_data['status'].capitalize}") + check_thresholds_string(data: ci_pipeline_data['status']) + end + + ###--- CI-PIPELINE DURATION CHECK ---### + def ci_pipeline_duration + return unless @options[:mode] == 'ci-pipeline-duration' + http_connect(path: "api/v4/projects/#{@options[:id]}/pipelines?scope=finished") + # get latest pipeline + http_connect(path: "api/v4/projects/#{@options[:id]}/pipelines/#{JSON.parse(@response.body).first['id']}") + ci_pipeline_data = JSON.parse(@response.body) + build_output(msg: "Pipeline ##{ci_pipeline_data['id']} took #{ci_pipeline_data['duration']}s") + build_perfdata(perfdata: "duration=#{ci_pipeline_data['duration']}s") + check_thresholds_int(data: ci_pipeline_data['duration']) + end + + ###--- CI-RUNNER ---### + def ci_runner_get_all(status:) + http_connect(path: "api/v4/runners/all?status=#{status.downcase}&per_page=100") + @all_runners = JSON.parse(@response.body) + @all_runners.delete_if { |item| /#{@options[:exclude]}/.match(item['description']) } unless @options[:exclude].to_s.empty? + # bail out if nothing comes back + if @all_runners.empty? && @options[:status] == 'online' + crit_msg("No #{@options[:status]} runners") + elsif @all_runners.empty? + ok_msg("No #{@options[:status]} runners") + end + end + + ### jobs duration + def ci_runner_jobs_duration + return unless @options[:mode] == 'ci-runner-jobs-duration' + http_connect(path: "api/v4/runners/#{@options[:id]}/jobs?status=running") + jobs = JSON.parse(@response.body).first + if jobs.nil? + ok_msg('No running jobs') + else + build_output(msg: "#{jobs['name']} is running for #{jobs['duration'].round}s") + build_perfdata(perfdata: "duration=#{jobs['duration'].round}s") + check_thresholds_int(data: jobs['duration']) + end + end + + ### runner status + def ci_runner_status + return unless @options[:mode] == 'ci-runner-status' + ci_runner_get_all(status: @options[:status]) + @all_runners.each do |item| + http_connect(path: "api/v4/runners/#{item['id']}") + @runners = JSON.parse(@response.body) + build_output(msg: "#{@runners['description']}(#{@runners['id']})") + check_thresholds_int(type: 'array', data: @all_runners.count) + end + build_perfdata(perfdata: "#{@options[:status]}=#{@all_runners.count}") + build_final_output(pretext: "#{@options[:status].capitalize} runners: ") + end + + ###--- SERVICES CHECK ---### + # check if sudoers entry with 'gitlab-ctl' command is present for current user + def gitlab_ctl_sudoers_check + `sudo -ln gitlab-ctl status 2>/dev/null` + unk_msg('No sudoers entry found for gitlab-ctl command') unless $?.success? # rubocop:disable Style/SpecialGlobalVars + rescue StandardError => e + unk_msg(e) + end + + # get all services with down status + def gitlab_ctl_status + `sudo gitlab-ctl status`.scan(/(?:down: )(\w+.\w+)/) + rescue StandardError => e + unk_msg(e) + end + + def services_check + return unless @options[:mode] == 'services' + gitlab_ctl_sudoers_check + if gitlab_ctl_status.any? + down_srvc = gitlab_ctl_status.join(', ') + crit_msg("#{down_srvc} is down") + else + ok_msg('All services are running') + end + end + + ###--- GROUP SIZE ---### + def groups_get_all + http_connect(path: 'api/v4/groups?statistics=true&per_page=100') + @all_groups = JSON.parse(@response.body) + @all_groups.delete_if { |item| /#{@options[:exclude]}/.match(item['name']) } unless @options[:exclude].to_s.empty? + @all_groups.keep_if { |item| /#{@options[:name]}/.match(item['name']) } unless @options[:name].to_s.empty? + end + + def group_size + return unless @options[:mode] == 'group-size' + groups_get_all + @all_groups.each do |item| + convert_to_mb(data: item['statistics']['storage_size']) + build_output(msg: "#{item['name']}: used space #{@used_size}MB") + check_thresholds_int(type: 'array', data: @used_size) + build_perfdata(perfdata: "#{item['name']}=#{@used_size}MB") + end + build_final_output + end + + ###--- LICENSE ---### + def license_expire + return unless @options[:mode] == 'license-expire' + http_connect(path: 'api/v4/license') + due_date = JSON.parse(@response.body)['expires_at'] + expire = Date.parse(due_date) - Date.today + if expire.to_i > @options[:warning].to_i + ok_msg("License will expire at #{due_date} - #{expire.to_i} days left") + else + warn_msg("License will expire at #{due_date} - #{expire.to_i} days left") + end + end + + def license_overage + return unless @options[:mode] == 'license-overage' + http_connect(path: 'api/v4/license') + data = JSON.parse(@response.body) + build_output(msg: "Active users: #{data['active_users']}, Overage: #{data['overage']}") + build_perfdata(perfdata: "active_users=#{data['active_users']} overage=#{data['overage']}") + check_thresholds_int(data: data['overage']) + end + + ###--- SIDEKIQ ---### + def sidekiq_jobs + return unless @options[:mode] == 'sidekiq-jobs' + http_connect(path: 'api/v4/sidekiq/job_stats') + data = JSON.parse(@response.body)['jobs'] + build_output(msg: "Sidekiq jobs - #{@options[:name].capitalize}: #{data[@options[:name]]}") + build_perfdata(perfdata: "#{@options[:name]}=#{data[@options[:name]]}") + check_thresholds_int(data: data[@options[:name]]) + end +end + +CheckGitlab.new(options) -- GitLab