port-authority 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OGQyMDZkOGNmMTM1ZTEyYTI4ZDA5YWYxZDc2M2ZhYjY5NDIwZGJmOA==
5
+ data.tar.gz: !binary |-
6
+ ZjMwNDUxNWY0MzllNTc2ZjZhNWEyZWQyYWNlYWY2NTQ4ZjhhM2NhOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MDcxNWMxYTVhODE1MjMwM2U1ODk0OGUxOWQ0MDAyYWIzZTMwYTE4NGU3YWU4
10
+ OGViYzJmODc4NmM4NjMwZmM3MTVkYTg1YWQ4Y2Y4NDdiMTFmODc3NDE1NzRm
11
+ M2IzYWM2NDFhY2I1MTQzMjUwMmExNTFhMmRlMDhlOGY2Y2E3NWI=
12
+ data.tar.gz: !binary |-
13
+ YjM3ZDEzOGNmZmM4MDU2ZGY2ZDc0NDc1ZjE1OWZlYTI3NzkxN2E2MGE3MGZj
14
+ NWI3MmQ4ZjBmNWRjMzBhYzQxZDc5Mjk0M2M3ZThhYWVkYjA3MjE0NGExYjE3
15
+ ZjI2ZjhhZGRhMjc3MTdkYzI2YWNmNzdjMTJkOGI5ODQ2ZWJkZDk=
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'port-authority/watchdog/master'
3
+ PortAuthority::Watchdog::Master.new.run
@@ -0,0 +1,62 @@
1
+ require 'yaml'
2
+ require 'etcd-tools'
3
+
4
+ module PortAuthority
5
+ module Util
6
+ module Config
7
+ private
8
+
9
+ def default_config
10
+ { debug: false,
11
+ syslog: false,
12
+ etcd: {
13
+ endpoint: 'http://localhost:4001',
14
+ interval: 1,
15
+ timeout: 2
16
+ },
17
+ docker: {
18
+ socket: 'tcp://localhost:4243'
19
+ },
20
+ icmp: {
21
+ count: 2,
22
+ interval: 1
23
+ },
24
+ arping: {
25
+ count: 1,
26
+ wait: 1
27
+ },
28
+ vip: {
29
+ interval: 1,
30
+ ip: '172.17.1.5',
31
+ mask: '255.255.255.0',
32
+ interface: 'eth0'
33
+ },
34
+ lb: {
35
+ image: 'docker-registry.prz/stackdocks/haproxy:latest',
36
+ name: 'lb',
37
+ network: 'overlay'
38
+ },
39
+ commands: {
40
+ arping: `which arping`.chomp,
41
+ arp: `which arp`.chomp,
42
+ iproute: `which ip`.chomp
43
+ }
44
+ }
45
+ end
46
+
47
+ def config
48
+ cfg = default_config
49
+ if File.exist? '/etc/port-authority.yaml'
50
+ cfg = cfg.deep_merge YAML.load_file('/etc/port-authority.yaml')
51
+ puts 'loaded config from /etc/port-authority.yaml'
52
+ elsif File.exist? './port-authority.yaml'
53
+ cfg = cfg.deep_merge YAML.load_file('./port-authority.yaml')
54
+ puts 'loaded config from ./port-authority.yaml'
55
+ else
56
+ puts 'no config file loaded, using defaults'
57
+ end
58
+ cfg
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+ require 'etcd'
2
+
3
+ module PortAuthority
4
+ module Util
5
+ module Etcd
6
+ # connect to ETCD
7
+ def etcd_connect!
8
+ (host, port) = @config[:etcd][:endpoint].gsub(/^https?:\/\//, '').gsub(/\/$/, '').split(':')
9
+ etcd = ::Etcd.client(host: host, port: port)
10
+ begin
11
+ versions = JSON.parse(etcd.version)
12
+ info "conncted to ETCD at #{@config[:etcd][:endpoint]}"
13
+ info "server version: #{versions['etcdserver']}"
14
+ info "cluster version: #{versions['etcdcluster']}"
15
+ info "healthy: #{etcd.healthy?}"
16
+ return etcd
17
+ rescue Exception => e
18
+ err "couldn't connect to etcd at #{host}:#{port}"
19
+ err "#{e.message}"
20
+ @exit = true
21
+ return nil
22
+ end
23
+ end
24
+
25
+ def swarm_leader(etcd)
26
+ etcd.get('/_pa/docker/swarm/leader')
27
+ end
28
+
29
+ def am_i_leader?(etcd)
30
+ Socket.ip_address_list.map(){|a| a.ip_address }.member?(swarm_leader(etcd).split(':').first)
31
+ rescue Exception => e
32
+ false
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ require 'socket'
2
+
3
+ module PortAuthority
4
+ module Util
5
+ module Helpers
6
+ def hostname
7
+ @hostname ||= Socket.gethostname
8
+ end
9
+
10
+ def my_ip
11
+ @my_ip ||= Socket.ip_address_list.detect { |i| i.ipv4_private? }.ip_address
12
+ end
13
+
14
+ def arping
15
+ @config[:commands][:arping]
16
+ end
17
+
18
+ def iproute
19
+ @config[:commands][:iproute]
20
+ end
21
+
22
+ def arp
23
+ @config[:commands][:arp]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ require 'docker-api'
2
+
3
+ module PortAuthority
4
+ module Util
5
+ module LoadBalancer
6
+ # connect to ETCD
7
+ def lb_docker_setup!
8
+ Docker.url = @config[:lb][:docker_endpoint]
9
+ Docker.version
10
+ true
11
+ rescue
12
+ false
13
+ end
14
+
15
+ def lb_create
16
+ img = Docker::Image.create('fromImage' => @config[:lb][:image])
17
+
18
+ # setup port bindings hash
19
+ port_bindings = Hash.new
20
+ img.json['ContainerConfig']['ExposedPorts'].keys.each do |port|
21
+ port_bindings[port] = [ { 'HostPort' => "#{port.split('/').first}" } ]
22
+ end
23
+
24
+ # create container with
25
+ @lb_container = Docker::Container.create(
26
+ 'Image' => i.json['Id'],
27
+ 'name' => @config[:lb][:name],
28
+ 'Hostname' => @config[:lb][:name],
29
+ 'Env' => [ "ETCDCTL_ENDPOINT=#{@config[:etcd][:endpoint]}" ],
30
+ 'RestartPolicy' => { 'Name' => 'never' },
31
+ 'HostConfig' => {
32
+ 'PortBindings' => port_bindings,
33
+ 'NetworkMode' => @config[:lb][:network]
34
+ }
35
+ )
36
+ end
37
+
38
+ def lb_up?
39
+ @lb_container.info['Status'] =~ /^[Uu]p/
40
+ end
41
+
42
+ def lb_start!
43
+ @lb_container.start
44
+ end
45
+
46
+ def lb_stop!
47
+ @lb_container.stop
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,42 @@
1
+ require 'logger'
2
+
3
+ module PortAuthority
4
+ module Util
5
+ module Logger
6
+ def info(message)
7
+ if @config[:debug]
8
+ @semaphore[:log].synchronize do
9
+ $stdout.puts(Time.now.to_s + ' INFO (TID:' + Thread.current.object_id.to_s + ') ' + message.to_s)
10
+ $stdout.flush
11
+ end
12
+ else
13
+ @semaphore[:log].synchronize do
14
+ $stdout.puts(Time.now.to_s + ' INFO ' + message.to_s)
15
+ $stdout.flush
16
+ end
17
+ end
18
+ end
19
+
20
+ def err(message)
21
+ if @config[:debug]
22
+ @semaphore[:log].synchronize do
23
+ $stdout.puts(Time.now.to_s + ' ERROR (TID:' + Thread.current.object_id.to_s + ') ' + message.to_s)
24
+ $stdout.flush
25
+ end
26
+ else
27
+ @semaphore[:log].synchronize do
28
+ $stdout.puts(Time.now.to_s + ' ERROR ' + message.to_s)
29
+ $stdout.flush
30
+ end
31
+ end
32
+ end
33
+
34
+ def debug(message)
35
+ @semaphore[:log].synchronize do
36
+ $stdout.puts(Time.now.to_s + ' DEBUG (TID:' + Thread.current.object_id.to_s + ') ' + message.to_s)
37
+ $stdout.flush
38
+ end if @config[:debug]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ module PortAuthority
2
+ module Util
3
+ module Vip
4
+ # add or remove VIP on interface
5
+ # <IMPLEMENTED>
6
+ def vip_handle!(leader)
7
+ ip = IPAddr.new(@config[:vip][:ip])
8
+ mask = @config[:vip][:mask]
9
+ cmd = [ iproute,
10
+ 'address',
11
+ '',
12
+ "#{ip}/#{mask}",
13
+ 'dev',
14
+ @config[:vip][:interface],
15
+ 'label',
16
+ @config[:vip][:interface] + '-vip',
17
+ '>/dev/null 2>&1'
18
+ ]
19
+ leader ? cmd[2] = 'add' : cmd[2] = 'delete'
20
+ debug "#{cmd.join(' ')}"
21
+ if system(cmd.join(' '))
22
+ return true
23
+ else
24
+ return false
25
+ end
26
+ end
27
+
28
+ # send gratuitous ARP to the network
29
+ def vip_update_arp!
30
+ cmd = [ arping, '-U', '-q',
31
+ '-c', @config[:arping][:count],
32
+ '-I', @config[:vip][:interface],
33
+ @config[:vip][:ip] ]
34
+ debug "#{cmd.join(' ')}"
35
+ if system(cmd.join(' '))
36
+ return true
37
+ else
38
+ return false
39
+ end
40
+ end
41
+
42
+ # check whether VIP is assigned to me
43
+ def got_vip?
44
+ Socket.ip_address_list.map(){|a| a.ip_address }.member?(@config[:vip][:ip])
45
+ end
46
+
47
+ # check reachability of VIP by ICMP echo
48
+ def vip_alive?(icmp)
49
+ (1..@config[:icmp][:count]).each { return true if icmp.ping }
50
+ return false
51
+ end
52
+
53
+ # check whether the IP is registered anywhere
54
+ def vip_dup?
55
+ cmd_arp = [ arp, '-d', @config[:vip][:ip], '>/dev/null 2>&1' ]
56
+ cmd_arping = [ arping, '-D', '-q',
57
+ '-c', @config[:arping][:count],
58
+ '-w', @config[:arping][:wait],
59
+ '-I', @config[:vip][:interface],
60
+ @config[:vip][:ip] ]
61
+ debug "#{cmd_arp.join(' ')}"
62
+ system(cmd_arp.join(' '))
63
+ debug "#{cmd_arping.join(' ')}"
64
+ if system(cmd_arping.join(' '))
65
+ return false
66
+ else
67
+ return true
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,38 @@
1
+ require 'timeout'
2
+ require 'json'
3
+ require 'etcd-tools'
4
+ require 'port-authority/util/config'
5
+ require 'port-authority/util/logger'
6
+ require 'port-authority/util/helpers'
7
+
8
+ module PortAuthority
9
+ module Watchdog
10
+ class Init
11
+
12
+ include PortAuthority::Util::Config
13
+ include PortAuthority::Util::Logger
14
+ include PortAuthority::Util::Helpers
15
+
16
+ def initialize
17
+ @config = { debug: false }
18
+ @config = config
19
+ @exit = false
20
+ @exit_sigs = ['INT', 'TERM']
21
+ @exit_sigs.each { |sig| Signal.trap(sig) { @exit = true } }
22
+ Signal.trap('USR1') { @config[:debug] = false }
23
+ Signal.trap('USR2') { @config[:debug] = true }
24
+ Signal.trap('HUP') { @config = config }
25
+ end
26
+
27
+ def setup(proc_name, nice = -20)
28
+ if RUBY_VERSION >= '2.1'
29
+ Process.setproctitle(proc_name)
30
+ else
31
+ $0 = proc_name
32
+ end
33
+ Process.setpriority(Process::PRIO_PROCESS, 0, nice)
34
+ # FIXME: Process.daemon ...
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,145 @@
1
+ require 'ipaddr'
2
+ require 'port-authority/util/vip'
3
+ require 'port-authority/util/etcd'
4
+ require 'port-authority/util/loadbalancer'
5
+ require 'port-authority/watchdog/init'
6
+ require 'port-authority/watchdog/threads/icmp'
7
+ require 'port-authority/watchdog/threads/swarm'
8
+
9
+ module PortAuthority
10
+ module Watchdog
11
+ class Manager < PortAuthority::Watchdog::Init
12
+
13
+ include PortAuthority::Util::Etcd
14
+ include PortAuthority::Util::Vip
15
+ include PortAuthority::Util::LoadBalancer
16
+ include PortAuthority::Watchdog::Threads
17
+
18
+ def run
19
+ # exit if not root
20
+ if Process.euid != 0
21
+ $stderr.puts 'Must run under root user!'
22
+ exit! 1
23
+ end
24
+
25
+ # set process name and nice level (default: -20)
26
+ setup 'pa-master-watchdog'
27
+
28
+ # prepare semaphores
29
+ @semaphore = {
30
+ log: Mutex.new,
31
+ swarm: Mutex.new,
32
+ icmp: Mutex.new
33
+ }
34
+
35
+ # prepare threads
36
+ @thread = {
37
+ icmp: thread_icmp,
38
+ etcd: thread_swarm,
39
+ }
40
+
41
+ # prepare status vars
42
+ @status_swarm = false
43
+ @status_icmp = false
44
+
45
+ # start threads
46
+ @thread.each_value(&:run)
47
+
48
+ # wait for threads to make sure they gather something
49
+ debug 'waiting for threads to gather something...'
50
+ sleep @config[:vip][:interval]
51
+ first_cycle = true
52
+
53
+ # main loop
54
+ while !@exit do
55
+ # initialize local state vars on first iteration
56
+ status_swarm = status_icmp = false if first_cycle
57
+
58
+ # iteration interval
59
+ sleep @config[:vip][:interval]
60
+
61
+ # sync state to local variables
62
+ @semaphore[:icmp].synchronize { status_icmp = @status_icmp }
63
+ @semaphore[:swarm].synchronize { status_swarm = @status_swarm }
64
+
65
+ # the logic (should be self-explanatory ;))
66
+ if am_i_leader?
67
+ if got_vip?
68
+ debug 'i am the leader with VIP, that is OK'
69
+ else
70
+ info 'i am the leader without VIP, checking whether it is free'
71
+ if status_icmp
72
+ info 'VIP is still up! (ICMP)'
73
+ # FIXME: notify by sensu client socket
74
+ else
75
+ info 'VIP is unreachable by ICMP, checking for duplicates on L2'
76
+ if vip_dup?
77
+ info 'VIP is still assigned! (ARP)'
78
+ # FIXME: notify by sensu client socket
79
+ else
80
+ info 'VIP is free :) assigning'
81
+ vip_handle! status_swarm
82
+ info 'updating other hosts about change'
83
+ vip_update_arp!
84
+ end
85
+ end
86
+ end
87
+ if lb_up?
88
+ debug 'i am the leader and load-balancer is up, that is OK'
89
+ else
90
+ info 'i am the leader and load-balancer is down, starting'
91
+ lb_start!
92
+ end
93
+ else
94
+ if got_vip?
95
+ info 'i got VIP and should not, removing'
96
+ vip_handle! status_swarm
97
+ info 'updating other hosts about change'
98
+ vip_update_arp!
99
+ else
100
+ debug 'i am not the leader and i do not have the VIP, that is OK'
101
+ end
102
+ if lb_up?
103
+ info 'i am not the leader and load-balancer is up, stopping'
104
+ lb_stop!
105
+ else
106
+ debug 'i am not the leader and load-balancer is down, that is OK'
107
+ end
108
+ end
109
+
110
+ # short report on first cycle
111
+ if first_cycle
112
+ info "i #{status_swarm ? 'AM' : 'am NOT'} the leader"
113
+ info "i #{got_vip? ? 'DO' : 'do NOT'} have the VIP"
114
+ info "i #{status_icmp ? 'CAN' : 'CANNOT'} see the VIP"
115
+ info "i #{status_haproxy ? 'CAN' : 'CANNOT'} see the VIP"
116
+ first_cycle = false
117
+ end
118
+ end
119
+
120
+ # this is triggerred on exit
121
+ info 'SIGTERM received'
122
+ info 'waiting for threads to finish...'
123
+ @thread.each_value(&:join)
124
+
125
+ # remove VIP on shutdown
126
+ if got_vip?
127
+ info 'removing VIP'
128
+ vip_handle! false
129
+ vip_update_arp!
130
+ end
131
+
132
+ # stop LB on shutdown
133
+ if lb_up?
134
+ info 'stopping load-balancer'
135
+ lb_stop!
136
+ end
137
+
138
+ info 'exiting...'
139
+ exit 0
140
+ end
141
+
142
+
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,22 @@
1
+ require 'net/ping'
2
+
3
+ module PortAuthority
4
+ module Watchdog
5
+ module Threads
6
+ def thread_icmp
7
+ Thread.new do
8
+ debug 'starting ICMP thread...'
9
+ icmp = Net::Ping::ICMP.new(@config[:vip][:ip])
10
+ while !@exit do
11
+ debug 'checking state by ICMP echo'
12
+ status = vip_alive? icmp
13
+ @semaphore[:icmp].synchronize { @status_icmp = status }
14
+ debug "VIP is #{status ? 'alive' : 'down' } according to ICMP"
15
+ sleep @config[:icmp][:interval]
16
+ end
17
+ info 'ending ICMP thread...'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module PortAuthority
2
+ module Watchdog
3
+ module Threads
4
+ def thread_swarm
5
+ Thread.new do
6
+ debug '<swarm> starting thread...'
7
+ etcd = etcd_connect!
8
+ while !@exit do
9
+ debug '<swarm> checking etcd state'
10
+ status = am_i_leader? etcd
11
+ @semaphore[:swarm].synchronize { @status_swarm = status }
12
+ debug "<swarm> i am #{status ? 'the leader' : 'not the leader' }"
13
+ sleep @config[:etcd][:interval]
14
+ end
15
+ info '<swarm> ending thread...'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module PortAuthority
2
+ module Watchdog
3
+ end
4
+
5
+ module Util
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: port-authority
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Radek 'blufor' Slavicinsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: etcd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.3.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.3'
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: etcd-tools
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: '0.2'
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: 0.2.9
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ version: '0.2'
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: 0.2.9
53
+ - !ruby/object:Gem::Dependency
54
+ name: net-ping
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: '1.7'
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 1.7.8
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.7'
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: 1.7.8
73
+ - !ruby/object:Gem::Dependency
74
+ name: docker-api
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ~>
78
+ - !ruby/object:Gem::Version
79
+ version: '1.0'
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.25.0
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: 1.25.0
93
+ description: Tools for PortAuthority
94
+ email: radek.slavicinsky@gmail.com
95
+ executables:
96
+ - pa-master-watchdog
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - bin/pa-master-watchdog
101
+ - lib/port-authority.rb
102
+ - lib/port-authority/util/config.rb
103
+ - lib/port-authority/util/etcd.rb
104
+ - lib/port-authority/util/helpers.rb
105
+ - lib/port-authority/util/loadbalancer.rb
106
+ - lib/port-authority/util/logger.rb
107
+ - lib/port-authority/util/vip.rb
108
+ - lib/port-authority/watchdog/init.rb
109
+ - lib/port-authority/watchdog/master.rb
110
+ - lib/port-authority/watchdog/threads/icmp.rb
111
+ - lib/port-authority/watchdog/threads/swarm.rb
112
+ homepage: https://github.com/blufor/etcd-tools
113
+ licenses:
114
+ - GPLv2
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.9.3
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.4.3
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: Port Authority
136
+ test_files: []