apprentice 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5668ebd248945d42c501dba8c9a41ec836e33fe5
4
+ data.tar.gz: 1fc8746957d18e8e60197b3be49ada6e141ac6fc
5
+ SHA512:
6
+ metadata.gz: bc00be6fd55cda823fe1be0482449da93abebb77fae7e76f2e12ddc36189a755a1d19c2cd057b74de857a33558dc87d7a2bbc0ea5780b94a3e7f8c09482f6035
7
+ data.tar.gz: 1ba4da56b061846ad0dbefab3ad37478f17e9c77f5719ca311fbd54a7630071cde6f1f11cafebe6e1d2858f81652c097201e42809579decb154dd9db28965fa0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'mysql2'
4
+ gem 'eventmachine'
5
+
6
+ gem 'rspec'
7
+ gem 'ipaddress'
8
+
9
+ # Specify your gem's dependencies in apprentice.gemspec
10
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Moritz Heiber
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Apprentice
2
+
3
+ Apprentice is tiny server application that determines the current state of a running [MariaDB Galera master-master cluster setup](https://mariadb.com/kb/en/what-is-mariadb-galera-cluster/) and responds to HTTP requests on a pre-defined port, depending on the state of the server it is checking on.
4
+
5
+ ## How does it work?
6
+
7
+ You can find out about the syntax by running `apprentice --help`:
8
+
9
+ $ apprentice --help
10
+ Usage: apprentice [options]
11
+
12
+ Specific options:
13
+ -s, --server SERVER Connect to SERVER
14
+ -u, --user USER USER to connect the server with
15
+ -p, --password PASSWORD PASSWORD to use
16
+ -i, --ip IP Local IP to bind to
17
+ --port PORT Local PORT to use
18
+ --sql_port PORT Port of the MariaDB server to connect to
19
+ --[no-]accept-donor Accept cluster state "Donor/Desynced" as valid
20
+
21
+ Common options:
22
+ -h, --help Show this message
23
+ -v, --version Show version
24
+
25
+
26
+ ## What it does
27
+
28
+ It determines whether or not the server it is connected to is alive and ready to serve connections to clients. Furthermore, it also determines whether said server is a healthy part of the MariaDB cluster it belongs.
29
+
30
+ ## What it doesn't do
31
+
32
+ * **Loadbalancing**: In turn, it's ment to supply loadbalancers with data on whether or not to include a certain node into their balancing pool
33
+ * **Relay client connections**: Apprentice itself only serves two responses on a pre-determined port:
34
+ * *`200 OK`*: The server Apprentice is checking is healthy and ready to accept connections
35
+ * *`503 Service Unavailable`*: The server is unavailable and not ready for connections
36
+
37
+ ## What's it checking exactly?
38
+
39
+ Apprentice checks the following variables:
40
+
41
+ * **wsrep_cluster_size**: A cluster size below 2 is considered an error since there must never be one single server inside a cluster setup.
42
+ * **wsrep_ready**: Shows whether or not the replication is actually running or not. This must return `ON` for the server to be considered
43
+ * **wsrep_local_state**: This should read `4`, however, you may also use the --donor-allowed flag on the command-line to turn the value `2` into an acceptable value. Whether or not this is a desired state in your environment is at your discretion.
44
+ * *Note*: The value `2` indicates the server in question is currently being used as a donor to another member of the cluster and might be exhibiting slow-downs and/or erratic behaviour due to elevated network traffic and disc IO. For further explanation please [consult the MariaDB documentation](https://mariadb.com/kb/en/what-is-mariadb-galera-cluster/).
45
+
46
+ ## That's great and all, but what gives?
47
+ By itself, Apprentice doesn't do aynthing all that useful. However, it accommodates [HAProxy's httpchk method](http://cbonte.github.io/haproxy-dconv/configuration-1.4.html#option%20httpchk) quite nicely, making it possible to let HAProxy not only balance connection among a large pool of MariaDB cluster servers but also check on the cluster members health while doing so. With it you needn't care about a server dropping out of the cluster and clients still connecting to it since HAProxy and Apprentice are going to take care of that failing member by taking him out of the connection pool.
48
+
49
+ ## Goodies
50
+
51
+ I've included an (untested) init.d script which you may use in order to start Apprentice at boot time.
52
+
53
+ ## TODO
54
+
55
+ * Write better (r)docs. I'm sorry for the abysmal state they're in right now
56
+ * Be a lot more forgiving when it comes to SQL connection errors/reconnects/server going awol.
57
+ * Finish the rspec definitions. Sorry for missing out on those as well.
58
+ * Write a better init script
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'apprentice/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'apprentice'
8
+ spec.version = Apprentice::VERSION
9
+ spec.authors = 'Moritz Heiber'
10
+ spec.email = %w{moritz.heiber@gmail.com}
11
+ spec.description = 'A MariaDB cluster integrity checker'
12
+ spec.summary = 'Check given servers for consistency and replication status'
13
+ spec.homepage = 'http://github.com/moritzheiber/apprentice'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = %w{lib}
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.3'
22
+ spec.add_development_dependency 'rake'
23
+ end
data/bin/apprentice ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'apprentice'
4
+
5
+ sentinel = Apprentice::Sentinel.new
6
+ sentinel.run
@@ -0,0 +1,83 @@
1
+ #!/bin/bash
2
+ ### BEGIN INIT INFO
3
+ # Provides: apprentice
4
+ # Required-Start: mysql
5
+ # Required-Stop:
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: 0 1 6
8
+ # Short-Description: a MariaDB cluster integrity checker
9
+ ### END INIT INFO
10
+
11
+ NAME="`basename ${0/.sh/}`"
12
+ DAEMON="`which apprentice`"
13
+ PIDFILE="/var/run/${NAME}.pid"
14
+
15
+ [ -r /etc/default/${NAME} ] && source /etc/default/${NAME}
16
+
17
+ for file in /lib/init/vars.sh /lib/lsb/init-functions ; do
18
+ source ${file}
19
+ done
20
+
21
+ #
22
+ # Function that starts the daemon/service
23
+ #
24
+ do_start()
25
+ {
26
+ log_begin_msg "Starting ${NAME}..."
27
+
28
+ if [ ! "${START}" = "true" ]; then
29
+ log_failure_msg "this service is disabled. Enable it in /etc/default/$NAME"
30
+ return 2
31
+ elif [ ! "${SERVER}" ] || [ ! "${PASSWORD}" ] || [ ! ${USER} ] ; then
32
+ log_failure_msg "Missing variables inside defaults file."
33
+ return 2
34
+ fi
35
+
36
+ pidfile_dirname=`dirname ${PIDFILE}`
37
+
38
+ [ -d "$pidfile_dirname" ] || mkdir -p "$pidfile_dirname"
39
+ chown $USER:$GROUP "$pidfile_dirname"
40
+ chmod 0750 "$pidfile_dirname"
41
+
42
+ DAEMON_ARGS="--password ${PASSWORD} --user ${USER} --server ${SERVER} ${EXTRA_ARGS}"
43
+
44
+ start-stop-daemon --start --background --make-pidfile --quiet \
45
+ --pidfile ${PIDFILE} --exec ${DAEMON} --test > /dev/null || return 1
46
+ start-stop-daemon --start --background --make-pidfile --quiet \
47
+ --pidfile ${PIDFILE} --exec ${DAEMON} -- ${DAEMON_ARGS} || return 2
48
+ log_end_msg $?
49
+ }
50
+
51
+ do_stop()
52
+ {
53
+ log_begin_msg "Stopping ${NAME}..."
54
+
55
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --signal 15 --pidfile ${PIDFILE}
56
+ RETVAL="$?"
57
+ [ "$RETVAL" = 2 ] && return 2
58
+ [ "$?" = 2 ] && return 2
59
+ rm -f ${PIDFILE}
60
+ log_end_msg $?
61
+ return "$RETVAL"
62
+ }
63
+
64
+ case "$1" in
65
+ start)
66
+ do_start
67
+ ;;
68
+ stop)
69
+ do_stop
70
+ ;;
71
+ reload)
72
+ do_stop
73
+ do_start
74
+ ;;
75
+ restart|force-reload)
76
+ do_stop
77
+ do_start
78
+ ;;
79
+ *)
80
+ echo "Usage: ${NAME} {start|stop|restart|reload|force-reload}" >&2
81
+ exit 3
82
+ ;;
83
+ esac
data/lib/apprentice.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'eventmachine'
2
+ require 'apprentice/configuration'
3
+ require 'apprentice/version'
4
+ require 'apprentice/server'
5
+
6
+ module Apprentice
7
+ class Sentinel
8
+ include Configuration
9
+ include Server
10
+
11
+ def initialize
12
+ @options = get_config
13
+ end
14
+
15
+ def run
16
+ EM.run do
17
+ Signal.trap('INT') { EventMachine.stop }
18
+ Signal.trap('TERM') { EventMachine.stop }
19
+ EventMachine.start_server(
20
+ @options.ip,
21
+ @options.port,
22
+ Server::EventServer,
23
+ @options
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Checker
2
+ require 'apprentice/checks/galera'
3
+ include Galera
4
+
5
+ STATES = {1 => 'Joining',2 => 'Donor/Desynced',3 => 'Joined',4 => 'Synced'}
6
+ CODES = {200 => 'OK',503 => 'Service Unavailable'}
7
+
8
+ def format_text(texts)
9
+ value = ''
10
+ if !texts.empty?
11
+ texts.each do |t|
12
+ value << "#{t}\r\n"
13
+ end
14
+ end
15
+ return value
16
+ end
17
+
18
+ def generate_response(code = 503, text)
19
+ "HTTP/1.1 #{code} #{CODES[code]}\r\nContent-type: text/plain\r\nContent-length: #{text.length}\r\n\r\n#{text}"
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ module Galera
2
+ def get_galera_status
3
+ begin
4
+ result = @client.query "SHOW STATUS LIKE 'wsrep_%';"
5
+ if result.count > 0
6
+ result.each do |r|
7
+ @status.merge!(Hash[*r])
8
+ end
9
+ end
10
+ rescue Exception => message
11
+ puts message
12
+ end
13
+ end
14
+
15
+ def run_checks
16
+ get_galera_status
17
+ unless @status.empty?
18
+ response = {code: 200, text: []}
19
+ if !check_cluster_size
20
+ response[:text] << "Cluster size is #{@status['wsrep_cluster_size']}. Split-brain situation is likely."
21
+ end
22
+ if !check_ready_state
23
+ response[:text] << 'Cluster replication is not running.'
24
+ end
25
+ if !check_local_state
26
+ response[:text] << "Local state is '#{STATES[@status['wsrep_local_state']]}'."
27
+ end
28
+ response[:code] = 503 unless response[:text].empty?
29
+ return response
30
+ else
31
+ return {code: 503, text: ['Unable to determine cluster status']}
32
+ end
33
+ end
34
+
35
+ def check_cluster_size
36
+ return true if Integer(@status['wsrep_cluster_size']) > 1
37
+ false
38
+ end
39
+
40
+ def check_ready_state
41
+ return true if @status['wsrep_ready'] == 'ON'
42
+ false
43
+ end
44
+
45
+ def check_local_state
46
+ s = Integer(@status['wsrep_local_state'])
47
+ return true if s == 4 || (s == 2 && @donor_allowed)
48
+ false
49
+ end
50
+
51
+ end
@@ -0,0 +1,60 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module Configuration
5
+ def get_config
6
+ options = OpenStruct.new
7
+ options.ip = '0.0.0.0'
8
+ options.port = 3307
9
+ options.sql_port = 3306
10
+ options.accept_donor = false
11
+
12
+ opt_parser = OptionParser.new do |opts|
13
+ opts.banner = "Usage: apprentice [options]\n"
14
+ opts.separator ''
15
+ opts.separator 'Specific options:'
16
+
17
+ opts.on('-s SERVER', '--server SERVER',
18
+ 'Connect to SERVER') { |s| options.server = s }
19
+ opts.on('-u USER', '--user USER',
20
+ 'USER to connect the server with') { |u| options.user = u }
21
+ opts.on('-p PASSWORD', '--password PASSWORD',
22
+ 'PASSWORD to use') { |p| options.password = p }
23
+
24
+ opts.on('-i', '--ip IP',
25
+ 'Local IP to bind to') { |i| options.ip = i }
26
+ opts.on('--port PORT',
27
+ 'Local PORT to use') { |p| options.port = p }
28
+ opts.on('--sql_port PORT',
29
+ 'Port of the MariaDB server to connect to') { |p| options.sql_port = p }
30
+ opts.on('--[no-]accept-donor',
31
+ 'Accept cluster state "Donor/Desynced" as valid') { |ad| options.accept_donor = ad }
32
+
33
+ opts.separator ''
34
+ opts.separator 'Common options:'
35
+
36
+ opts.on_tail('-h', '--help', 'Show this message') do
37
+ puts opts
38
+ exit
39
+ end
40
+ opts.on_tail('-v', '--version', 'Show version') do
41
+ puts "Apprentice #{Apprentice::VERSION}"
42
+ exit
43
+ end
44
+ end
45
+
46
+ begin
47
+ ARGV << 's-h' if ARGV.size < 3
48
+ opt_parser.parse!(ARGV)
49
+ unless options.server && options.user && options.password
50
+ $stderr.puts 'Error: you have to specify a user, a password and the server to connect to'
51
+ $stderr.puts 'Try -h/--help for more options'
52
+ exit
53
+ end
54
+ return options
55
+ rescue OptionParser::ParseError
56
+ $stderr.print "Error: #{$!}\n"
57
+ exit
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ module Server
2
+ class EventServer < EM::Connection
3
+ require 'apprentice/checker'
4
+ require 'mysql2/em'
5
+ include Checker
6
+
7
+ attr_accessor :client
8
+
9
+ def initialize(options)
10
+ @ip = options.ip
11
+ @port = options.port
12
+ @sql_port = options.sql_port
13
+ @server = options.server
14
+ @user = options.user
15
+ @password = options.password
16
+ @donor_allowed = options.donor_allowed
17
+ @status = {}
18
+
19
+ begin
20
+ @client = Mysql2::Client.new(
21
+ host: @server,
22
+ port: @sql_port,
23
+ username: @user,
24
+ password: @password,
25
+ as: :array,
26
+ reconnect: true
27
+ )
28
+ rescue Exception => message
29
+ puts message
30
+ EM.stop_server
31
+ end
32
+ end
33
+
34
+ def receive_data(data)
35
+ response = run_checks
36
+ response_text = format_text(response[:text])
37
+ send_data generate_response(response[:code], response_text)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Apprentice
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'apprentice'
3
+ require 'ipaddress'
4
+
5
+ describe Apprentice do
6
+ describe Apprentice::Sentinel do
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init.d` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apprentice
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Moritz Heiber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A MariaDB cluster integrity checker
42
+ email:
43
+ - moritz.heiber@gmail.com
44
+ executables:
45
+ - apprentice
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - .gitignore
50
+ - .rspec
51
+ - Gemfile
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - apprentice.gemspec
56
+ - bin/apprentice
57
+ - init.d/apprentice.sh
58
+ - lib/apprentice.rb
59
+ - lib/apprentice/checker.rb
60
+ - lib/apprentice/checks/galera.rb
61
+ - lib/apprentice/configuration.rb
62
+ - lib/apprentice/server.rb
63
+ - lib/apprentice/version.rb
64
+ - spec/lib/apprentice_spec.rb
65
+ - spec/spec_helper.rb
66
+ homepage: http://github.com/moritzheiber/apprentice
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.0.7
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Check given servers for consistency and replication status
90
+ test_files:
91
+ - spec/lib/apprentice_spec.rb
92
+ - spec/spec_helper.rb
93
+ has_rdoc: