port-authority 0.4.8 → 0.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c6efb1e9b1f802cf04ea83d0ab8703cd15cad51e
4
- data.tar.gz: bdf0153379d87f1cbc1f02e164122ab8d513707c
3
+ metadata.gz: 221e5235470801ff068ea1fd1cf29494ac4e5c7d
4
+ data.tar.gz: d916df00dc140f8d88692dc3df7dd1d427522e8a
5
5
  SHA512:
6
- metadata.gz: 618be24122be3c671931165e2f8c4338b3bfc9bb37553981f04f9a74986d4a67603b7ce9bdd9baca05a85947d163762e0bf317832d6fada116752ed64492c3dc
7
- data.tar.gz: 64201d646e37d98b70392c8ce2db90f32c605d19b6a967a066b2f8dbc30ff1b416b1d0f966e90df5950c99a7039c4d3e96b2c697262e27cbfe5a4cf5314116b9
6
+ metadata.gz: 99168b0111a49424e6166a91a0586819b65df8007edfe766740df902bb922400c442ef58a52ae4611f3737d5f609b52bdabf92a24b319849d5fd3e1af669eae1
7
+ data.tar.gz: 12f4a20fc48a4606d9e841f5fbba75189294090f668d7f32822d236d39ebe979b871f3e2a4efc19ed395593d9ef174535989493ae25ecab29674e4ac11f8ffd2
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'port-authority/agents/lbaas'
3
+ PortAuthority::Agents::LBaaS.new
data/bin/pa-service-list CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
- require 'port-authority/services'
3
- PortAuthority::Services::App.new.run
2
+ require 'port-authority/tools/service_list'
3
+ PortAuthority::Tools::ServiceList.new
@@ -1,12 +1,4 @@
1
1
  module PortAuthority
2
- module Manager
3
- end
4
-
5
- module Services
6
- end
7
-
8
- module Util
9
- end
10
2
 
11
3
  module Errors
12
4
 
@@ -0,0 +1,164 @@
1
+ require 'timeout'
2
+ require 'digest/sha2'
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'port-authority'
6
+ require 'port-authority/config'
7
+ require 'port-authority/logger'
8
+ require 'port-authority/etcd'
9
+
10
+ module PortAuthority
11
+ # Scaffolding class for agents
12
+ class Agent
13
+ # Common agent process init. Contains configuration load,
14
+ # common signal responses and runtime variables init.
15
+ # Implements execution of actual agents via \+run+ method.
16
+ # Also handles any uncaught exceptions.
17
+ def initialize
18
+ Thread.current[:name] = 'main' # name main thread
19
+ @@_exit = false # prepare exit flag
20
+ @@_semaphores = { log: Mutex.new } # init semaphores
21
+ @@_threads = {} # init threads
22
+ Signal.trap('INT') { exit!(1) } # end immediatelly
23
+ Signal.trap('TERM') { end! } # end gracefully
24
+ Config.load! || exit!(1) # load config or die
25
+ begin # all-wrapping exception ;)
26
+ run # hook to child class
27
+ rescue StandardError => e
28
+ Logger.alert "UNCAUGHT EXCEPTION IN THREAD main! Dying! X.X"
29
+ Logger.alert [' ', "#{e.class}:", e.message].join(' ')
30
+ e.backtrace.each {|line| Logger.debug " #{line}"}
31
+ exit! 1
32
+ end
33
+ end
34
+
35
+ # Setup the agent process.
36
+ # Initializes logging, system process parameters,
37
+ # daemonizing.
38
+ #
39
+ # There are 4 optional parameters:
40
+ # +:name+:: \+String+ Agent name. Defaults to \+self.class.downcase+ of the child agent
41
+ # +:root+:: \+Bool+ Require to be ran as root. Defaults to \+false+.
42
+ # +:daemonize+:: \+Bool+ Daemonize the process. Defaults to \+false+.
43
+ # +:nice+:: \+Int+ nice of the process. Defaults to \+0+
44
+ def setup(args = {})
45
+ name = args[:name] || self.class.to_s.downcase.split('::').last
46
+ args[:root] ||= false
47
+ args[:daemonize] ||= false
48
+ args[:nice] ||= 0
49
+ Logger.init! @@_semaphores[:log]
50
+ Logger.info 'Starting main thread'
51
+ Logger.debug 'Setting process name'
52
+ if RUBY_VERSION >= '2.1'
53
+ Process.setproctitle("pa-#{name}-agent")
54
+ else
55
+ $0 = "pa-#{name}-agent"
56
+ end
57
+ if args[:root] && Process.uid != 0
58
+ Logger.alert 'Must run under root user!'
59
+ exit! 1
60
+ end
61
+ Logger.debug 'Setting CPU nice level'
62
+ Process.setpriority(Process::PRIO_PROCESS, 0, args[:nice])
63
+ if args[:daemonize]
64
+ Logger.info 'Daemonizing process'
65
+ if RUBY_VERSION < '1.9'
66
+ exit if fork
67
+ Process.setsid
68
+ exit if fork
69
+ Dir.chdir('/')
70
+ else
71
+ Process.daemon
72
+ end
73
+ end
74
+ end
75
+
76
+ # Has the exit flag been raised?
77
+ def exit?
78
+ @@_exit
79
+ end
80
+
81
+ # Raise the exit flag
82
+ def end!
83
+ @@_exit = true
84
+ end
85
+
86
+ # Create a named \+Mutex+ semaphore
87
+ def sem_create(name)
88
+ @@_semaphores.merge!(Hash[name.to_sym], Mutex.new)
89
+ end
90
+
91
+ # Create a named \+Thread+ with its \+Mutex+ semaphore.
92
+ # The definition includes \+&block+ of code that should run
93
+ # within the thread.
94
+ #
95
+ # The method requires 3 parameters:
96
+ # +name+:: \+Symbol+ Thread/Mutex name.
97
+ # +interval+:: \+Integer+ Thread loop interval.
98
+ # +&block+:: \+Proc+ Block of code to run.
99
+ def thr_create(name, interval, &block)
100
+ @@_semaphores.merge!(Hash[name.to_sym, Mutex.new])
101
+ @@_threads.merge!(Hash[name.to_sym, Thread.new do
102
+ Thread.current[:name] = name.to_s
103
+ Logger.info "Starting thread #{Thread.current[:name]}"
104
+ begin
105
+ until exit?
106
+ yield block
107
+ sleep interval
108
+ end
109
+ Logger.info "Ending thread #{Thread.current[:name]}"
110
+ rescue StandardError => e
111
+ Logger.alert "UNCAUGHT EXCEPTION IN THREAD #{Thread.current[:name]}"
112
+ Logger.alert [' ', "#{e.class}:", e.message].join(' ')
113
+ e.backtrace.each {|line| Logger.debug " #{line}"}
114
+ end!
115
+ end
116
+ end
117
+ ])
118
+ end
119
+
120
+ # Run thread-safe code.
121
+ # The \+name+ parameter can be omitted when used
122
+ # from within a block of thread code. In this case
123
+ # the Mutex with the same \+:name+ will be used.
124
+ #
125
+ # The method accepts following parameters:
126
+ # +name+:: \+Symbol+ Mutex name.
127
+ # +&block+:: \+Proc+ Block of code to run.
128
+ def thr_safe(name=Thread.current[:name].to_sym, &block)
129
+ @@_semaphores[name.to_sym].synchronize do
130
+ yield block
131
+ end
132
+ end
133
+
134
+ # Start named thread.
135
+ # If the name is omitted, applies to all spawned threads ;)
136
+ def thr_start(name=nil)
137
+ return @@_threads[name].run if name
138
+ @@_threads.each_value(&:run)
139
+ end
140
+
141
+
142
+ # Wait for named thread to finish.
143
+ # If the name is omitted, applies to all spawned threads ;)
144
+ def thr_wait(name=nil)
145
+ return @@_threads[name].join if name
146
+ @@_threads.each_value(&:join)
147
+ end
148
+
149
+ # Run command in Bash.
150
+ def shellcmd(*args)
151
+ cmd = args.join(' ').to_s
152
+ cksum = Digest::SHA256.hexdigest(args.join.to_s)[0..15]
153
+ Logger.debug "Executing shellcommand #{cksum} - #{cmd}"
154
+ ret = system cmd
155
+ Logger.debug "Shellcommand #{cksum} returned #{ret.to_s}"
156
+ end
157
+
158
+ # Return hostname.
159
+ def hostname
160
+ @hostname ||= Socket.gethostname
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,134 @@
1
+ # rubocop:disable MethodLength, CyclomaticComplexity, Metrics/BlockNesting, Metrics/LineLength, Metrics/AbcSize, Metrics/PerceivedComplexity
2
+ require 'socket'
3
+ require 'port-authority/agent'
4
+ require 'port-authority/mechanism/load_balancer'
5
+ require 'port-authority/mechanism/floating_ip'
6
+
7
+ module PortAuthority
8
+ module Agents
9
+ class LBaaS < PortAuthority::Agent
10
+ include PortAuthority::Mechanism
11
+
12
+ def run
13
+ setup(daemonize: Config.daemonize, nice: -10, root: true)
14
+ Signal.trap('HUP') { Config.load! && LoadBalancer.init! && FloatingIP.init! }
15
+ Signal.trap('USR1') { Logger.debug! }
16
+ Signal.trap('USR2') { @lb_update_hook = true }
17
+ @status_swarm = false
18
+ @etcd = PortAuthority::Etcd.cluster_connect Config.etcd
19
+
20
+ thr_create(:swarm, Config.lbaas[:swarm_interval] || Config.lbaas[:interval]) do
21
+ begin
22
+ Logger.debug 'Checking Swarm state'
23
+ status = @etcd.am_i_swarm_leader?
24
+ thr_safe { @status_swarm = status }
25
+ Logger.debug "I am Swarm #{status ? 'leader' : 'follower' }"
26
+ rescue StandardError => e
27
+ Logger.error [ e.class, e.message ].join(': ')
28
+ e.backtrace.each {|line| Logger.debug " #{line}"}
29
+ thr_safe { @status_swarm = false }
30
+ sleep(Config.lbaas[:swarm_interval] || Config.lbaas[:interval])
31
+ retry unless exit?
32
+ end
33
+ end
34
+
35
+ thr_start
36
+
37
+ FloatingIP.init!
38
+ LoadBalancer.init!
39
+ LoadBalancer.container || ( LoadBalancer.pull! && LoadBalancer.create! )
40
+
41
+ Logger.debug 'Waiting for threads to gather something...'
42
+ sleep Config.lbaas[:interval]
43
+ first_cycle = true
44
+ status_time = Time.now.to_i - 60
45
+
46
+ until exit?
47
+ status_swarm = false if first_cycle
48
+ if @lb_update_hook
49
+ Logger.notice 'LoadBalancer update triggerred'
50
+ LoadBalancer.update!
51
+ @lb_update_hook = false
52
+ Logger.notice 'LoadBalancer update finished'
53
+ end
54
+ sleep Config.lbaas[:interval]
55
+ thr_safe(:swarm) { status_swarm = @status_swarm }
56
+ # main logic
57
+ if status_swarm
58
+ # handle FloatingIP on leader
59
+ Logger.debug 'I am the LEADER'
60
+ if FloatingIP.up?
61
+ Logger.debug 'Got FloatingIP, that is OK'
62
+ else
63
+ Logger.notice 'No FloatingIP here, checking whether it is free'
64
+ FloatingIP.arp_del!
65
+ if FloatingIP.reachable?
66
+ Logger.notice 'FloatingIP is still up! (ICMP)'
67
+ else
68
+ Logger.info 'FloatingIP is unreachable by ICMP, checking for duplicates on L2'
69
+ FloatingIP.arp_del!
70
+ if FloatingIP.duplicate?
71
+ Logger.error 'FloatingIP is still assigned! (ARP)'
72
+ else
73
+ Logger.notice 'FloatingIP is free :) assigning'
74
+ FloatingIP.handle! status_swarm
75
+ Logger.notice 'Notifying the network about change'
76
+ FloatingIP.update_arp!
77
+ end
78
+ end
79
+ end
80
+ # handle LoadBalancer on leader
81
+ if LoadBalancer.up?
82
+ Logger.debug 'LoadBalancer is up, that is OK'
83
+ else
84
+ Logger.notice 'LoadBalancer is down, starting'
85
+ LoadBalancer.start!
86
+ end
87
+ else
88
+ # handle FloatingIP on follower
89
+ Logger.debug 'I am a follower'
90
+ if FloatingIP.up?
91
+ Logger.notice 'I got FloatingIP and should not, removing'
92
+ FloatingIP.handle! status_swarm
93
+ FloatingIP.arp_del!
94
+ Logger.notice 'Notifying the network about change'
95
+ FloatingIP.update_arp!
96
+ else
97
+ Logger.debug 'No FloatingIP here, that is OK'
98
+ end
99
+ # handle LoadBalancer on follower
100
+ if LoadBalancer.up?
101
+ Logger.notice 'LoadBalancer is up, stopping'
102
+ LoadBalancer.stop!
103
+ else
104
+ Logger.debug 'LoadBalancer is down, that is OK'
105
+ end
106
+ end # logic end
107
+ end
108
+
109
+ thr_wait
110
+
111
+ # remove FloatingIP on shutdown
112
+ if FloatingIP.up?
113
+ Logger.notice 'Removing FloatingIP'
114
+ FloatingIP.handle! false
115
+ FloatingIP.update_arp!
116
+ end
117
+
118
+ # stop LB on shutdown
119
+ if LoadBalancer.up?
120
+ Logger.notice 'Stopping LoadBalancer'
121
+ LoadBalancer.stop!
122
+ end
123
+
124
+ Logger.notice 'Exiting...'
125
+ exit 0
126
+ end
127
+
128
+ def my_ip
129
+ @my_ip ||= Socket.ip_address_list.detect(&:ipv4_private?).ip_address
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ require 'etcd-tools/mixins/hash'
2
+
3
+ module PortAuthority
4
+ module Config
5
+
6
+ extend self
7
+
8
+ attr_reader :_cfg
9
+
10
+ def method_missing(name, *_args, &_block)
11
+ return @_cfg[name.to_sym] if @_cfg[name.to_sym] != nil
12
+ fail(NoMethodError, "unknown configuration section #{name}", caller)
13
+ end
14
+
15
+ def load!
16
+ @_cfg = {
17
+ debug: false,
18
+ syslog: false,
19
+ daemonize: false,
20
+ etcd: {
21
+ endpoints: ['http://localhost:2379'],
22
+ timeout: 5
23
+ },
24
+ commands: {
25
+ arping: `which arping`.chomp,
26
+ arp: `which arp`.chomp,
27
+ iproute: `which ip`.chomp
28
+ }
29
+ }
30
+ files = ['/etc/port-authority.yaml', './etc/port-authority.yaml'].delete_if {|f| !File.exists?(f)}
31
+ dir_files = Dir['/etc/port-authority.d/**.yaml'] + Dir['./etc/port-authority.d/**.yaml']
32
+ files += dir_files
33
+ return false if files.empty?
34
+ files.each do |f|
35
+ @_cfg = @_cfg.deep_merge(YAML.load_file(f))
36
+ end
37
+ true
38
+ end
39
+
40
+ def dump
41
+ self._cfg
42
+ end
43
+
44
+ def to_yaml
45
+ self._cfg.to_yaml.to_s
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ require 'etcd-tools'
2
+ require 'etcd-tools/mixins'
3
+
4
+ module PortAuthority
5
+ class Etcd < Etcd::Client
6
+
7
+ def self.cluster_connect(config)
8
+ endpoints = config[:endpoints].map {|ep| Hash[[:host, :port].zip(ep.match(/^(?:https?:\/\/)?([0-9a-zA-Z\.-_]+):([0-9]+)\/?/).captures)]}
9
+ timeout = config[:timeout].to_i
10
+ PortAuthority::Etcd.new(cluster: endpoints, read_timeout: timeout)
11
+ end
12
+
13
+ def self.shell_cluster_connect(env, timeout = 2)
14
+ env.split(',').each do |u|
15
+ (host, port) = u.gsub(/^https?:\/\//, '').gsub(/\/$/, '').split(':')
16
+ etcd = PortAuthority::Etcd.new(cluster: [{ host: host, port: port }], read_timeout: timeout)
17
+ return etcd if etcd.healthy?
18
+ next
19
+ end
20
+ raise Etcd::ClusterConnectError
21
+ end
22
+
23
+
24
+ def swarm_leader
25
+ get('/_pa/docker/swarm/leader').value.split(':').first
26
+ end
27
+
28
+ def am_i_swarm_leader?
29
+ Socket.ip_address_list.map(&:ip_address).member?(swarm_leader)
30
+ end
31
+
32
+ def swarm_overlay_id(name)
33
+ get_hash('/_pa/docker/network/v1.0/network').each_value do |network|
34
+ return network['id'] if network['name'] == name
35
+ end
36
+ end
37
+
38
+ def swarm_list_services(network, service_name='.*')
39
+ services = {}
40
+ self.get_hash("/_pa/docker/network/v1.0/endpoint/#{swarm_overlay_id(network)}").each_value do |container|
41
+ next unless Regexp.new(service_name).match container['name']
42
+ services = { container['name'] => { 'id' => container['id'], 'ip' => container['ep_iface']['addr'].sub(/\/[0-9]+$/, ''), 'ports' => container['exposed_ports'].map { |port| port['Port'] } } }
43
+ end
44
+ services
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,56 @@
1
+ # rubocop:disable Metrics/LineLength, Metrics/AbcSize, Metrics/MethodLength
2
+ require 'syslog'
3
+
4
+ module PortAuthority
5
+ module Logger
6
+
7
+ extend self
8
+
9
+ def init!(s)
10
+ @_s = s
11
+ @debug = Config.debug
12
+ Syslog.open($0, Syslog::LOG_PID, Syslog::LOG_DAEMON) if Config.syslog
13
+ end
14
+
15
+ def debug!
16
+ @debug = !@debug
17
+ end
18
+
19
+ def debug(message)
20
+ log :debug, message if @debug
21
+ end
22
+
23
+ def method_missing(name, *args, &_block)
24
+ if name
25
+ return log(name.to_sym, args[0])
26
+ else
27
+ fail(NoMethodError, "Unknown Logger method '#{name}'", caller)
28
+ end
29
+ end
30
+
31
+ def log(lvl, msg)
32
+ if Config.syslog
33
+ case lvl
34
+ when :debug
35
+ l = Syslog::LOG_DEBUG
36
+ when :info
37
+ l = Syslog::LOG_INFO
38
+ when :notice
39
+ l = Syslog::LOG_NOTICE
40
+ when :error
41
+ l = Syslog::LOG_ERR
42
+ when :alert
43
+ l = Syslog::LOG_ALERT
44
+ end
45
+ @_s.synchronize do
46
+ Syslog.log(l, "(%s) %s", Thread.current[:name], msg.to_s)
47
+ end
48
+ else
49
+ @_s.synchronize do
50
+ $stdout.puts [Time.now.to_s, sprintf('%-6.6s', lvl.to_s.upcase), "(#{Thread.current[:name]})", msg.to_s].join(' ')
51
+ $stdout.flush
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end