panteras_api 0.0.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/README.md +30 -0
- data/Rakefile +2 -0
- data/extra/mesos_consul_consistency_check +181 -0
- data/extra/mesos_consul_consistency_check_nagios_wrapper +124 -0
- data/lib/panteras_api.rb +11 -0
- data/lib/panteras_api/consul_cluster.rb +45 -0
- data/lib/panteras_api/docker_host.rb +77 -0
- data/lib/panteras_api/http_utils.rb +40 -0
- data/lib/panteras_api/marathon_endpoint.rb +40 -0
- data/lib/panteras_api/mesos_cluster.rb +80 -0
- data/lib/panteras_api/utils.rb +17 -0
- data/lib/panteras_api/version.rb +3 -0
- data/panteras_api.gemspec +27 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 864bf7c91dedd75b7472623ef2b101983e0b6d47
|
4
|
+
data.tar.gz: edabdfc6739a10e554fdc9af1b7f5bda129ea1ce
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d84d1d010ce1032e30262e40c5216415a756b1a142a005c18c552aea2285c6ada1f4b39de68e8bf18b4013eae3b237c10cca919580503c0495938f236af1f6c
|
7
|
+
data.tar.gz: 1af38e7b35769ed315f12283d748e19c14b1fb865d4280803c7738f032eebfe4f6d096e8cf4815000b0c7306e524caa522d1a925140e33d5325270142644ce7d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# PanterasApi
|
2
|
+
|
3
|
+
This is a program that checks services consistencies in a mesos/marathon/consul/docker PaaS infrastructure.
|
4
|
+
|
5
|
+
It is intended for use together with the [Panteras](https://github.com/eBayClassifiedsGroup/PanteraS) project.
|
6
|
+
|
7
|
+
**It will not work on other systems without tweaking the main program.**
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
$ gem install panteras_api
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Usage: bin/panteras_api [options]
|
16
|
+
-m MESOS_MASTER_HOSTNAME, Default: localhost
|
17
|
+
--mesos-master-hostname
|
18
|
+
-p MESOS_MASTER_PORT, Default: 5050
|
19
|
+
--mesos-master-port
|
20
|
+
-d, --debug Default: false
|
21
|
+
-f FULLY_QUALIFIED_HOSTNAME, Default: autodiscovery via gethostbyname
|
22
|
+
--fqdn
|
23
|
+
|
24
|
+
## Notes
|
25
|
+
|
26
|
+
* Exits with error code 1 when problems seen
|
27
|
+
* Exits with code 0 when everything is OK
|
28
|
+
* outputs a short text summary
|
29
|
+
* intended for use as a nagios-style check
|
30
|
+
* this script should be run on all mesos slaves (docker hosts)
|
data/Rakefile
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'panteras_api'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
config_default = {
|
8
|
+
:mesos_master_hostname => 'localhost',
|
9
|
+
:mesos_master_port => 5050,
|
10
|
+
:debug => false,
|
11
|
+
:fqdn => nil,
|
12
|
+
:error_exit_code => 1
|
13
|
+
}
|
14
|
+
|
15
|
+
options = {}
|
16
|
+
bad_news = []
|
17
|
+
good_news = []
|
18
|
+
|
19
|
+
OptionParser.new("Usage: #{$0} [options]") do |opts|
|
20
|
+
opts.release = PanterasApi::VERSION
|
21
|
+
|
22
|
+
opts.on("-m", "--mesos-master-hostname MESOS_MASTER_HOSTNAME", "Default: #{config_default[:mesos_master_hostname]}") do |m|
|
23
|
+
options[:mesos_master_hostname] = m
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-p", "--mesos-master-port MESOS_MASTER_PORT", "Default: #{config_default[:mesos_master_port]}") do |p|
|
27
|
+
options[:mesos_master_port] = p
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("-d", "--debug", "Default: #{config_default[:debug]}") do |d|
|
31
|
+
options[:debug] = d
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-c", "--critical", "Exit with Nagios CRITICAL (code 2). Default: WARNING (#{config_default[:error_exit_code]})") do |c|
|
35
|
+
options[:error_exit_code] = 2
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-f", "--fqdn FULLY_QUALIFIED_HOSTNAME", "Default: autodiscovery via gethostbyname") do |f|
|
39
|
+
options[:fqdn] = f
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
end.parse!
|
44
|
+
|
45
|
+
config_default[:fqdn] = Utils.fully_qualified_hostname if options[:fqdn].nil?
|
46
|
+
config = config_default.merge!(options)
|
47
|
+
|
48
|
+
### MESOS
|
49
|
+
|
50
|
+
my_fqdn = config[:fqdn]
|
51
|
+
|
52
|
+
puts "#" * 75 if config[:debug]
|
53
|
+
puts "* My hostname: #{my_fqdn}" if config[:debug]
|
54
|
+
|
55
|
+
begin
|
56
|
+
puts "* Connecting to Mesos host: #{config[:mesos_master_hostname]}:#{config[:mesos_master_port]}" if config[:debug]
|
57
|
+
mesos = MesosCluster.new(config[:mesos_master_hostname], config[:mesos_master_port])
|
58
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
59
|
+
abort("Problem connecting to mesos host #{config[:mesos_master_hostname]}:#{config[:mesos_master_port]}: #{e.message}")
|
60
|
+
end
|
61
|
+
|
62
|
+
abort("No running mesos tasks seen on mesos master #{config[:mesos_master_hostname]}:#{config[:mesos_master_port]}") if mesos.tasks.size == 0
|
63
|
+
|
64
|
+
puts "#" * 75 if config[:debug]
|
65
|
+
puts "* Mesos tasks ids running this cluster (all slaves):" if config[:debug]
|
66
|
+
puts mesos.tasks if config[:debug]
|
67
|
+
|
68
|
+
|
69
|
+
puts "#" * 75 if config[:debug]
|
70
|
+
puts "* Mesos tasks ids for tasks running on #{my_fqdn}:" if config[:debug]
|
71
|
+
mesos_tasks = mesos.my_tasks_ids(my_fqdn).flatten
|
72
|
+
puts mesos_tasks if config[:debug]
|
73
|
+
|
74
|
+
good_news << "#{mesos_tasks.size} mesos tasks running." if ! mesos_tasks.empty?
|
75
|
+
|
76
|
+
|
77
|
+
### MARATHON
|
78
|
+
marathon = MarathonEndpoint.new(mesos.master_hostname)
|
79
|
+
|
80
|
+
puts "#" * 75 if config[:debug]
|
81
|
+
puts "* Marathon tasks on #{my_fqdn}:" if config[:debug]
|
82
|
+
marathon_tasks = marathon.my_task_ids(my_fqdn)
|
83
|
+
puts marathon_tasks if config[:debug]
|
84
|
+
|
85
|
+
good_news << "#{marathon_tasks.size} marathon tasks running."
|
86
|
+
|
87
|
+
## compare mesos tasks (source of truth) with tasks seen in marathon
|
88
|
+
only_in_mesos = mesos_tasks - marathon_tasks
|
89
|
+
|
90
|
+
## compare marathon tasks with tasks seen in marathon
|
91
|
+
only_in_marathon = marathon_tasks - mesos_tasks
|
92
|
+
|
93
|
+
if ! only_in_mesos.empty?
|
94
|
+
bad_news << "mesos tasks not seen in marathon: #{only_in_mesos.join(',')}"
|
95
|
+
end
|
96
|
+
|
97
|
+
if ! only_in_marathon.empty?
|
98
|
+
bad_news << "marathon tasks not seen in mesos: #{only_in_marathon.join(',')}"
|
99
|
+
end
|
100
|
+
|
101
|
+
### CONSUL
|
102
|
+
consul = ConsulCluster.new(mesos.master_hostname)
|
103
|
+
### TODO: test with paas-formatted service names
|
104
|
+
puts "#" * 75 if config[:debug]
|
105
|
+
puts "* Consul services for #{my_fqdn}:" if config[:debug]
|
106
|
+
consul_services = consul.my_service_ids
|
107
|
+
puts consul.my_services.join("\n") if config[:debug]
|
108
|
+
|
109
|
+
good_news << "#{consul.my_services.size} consul services running."
|
110
|
+
|
111
|
+
### set these environment variables here or in shell for testing a remote docker (ie, boot2docker)
|
112
|
+
#ENV['DOCKER_HOST']='tcp://192.168.59.103:2376'
|
113
|
+
#ENV['DOCKER_CERT_PATH']='/Users/jcolby/.boot2docker/certs/boot2docker-vm'
|
114
|
+
#ENV['DOCKER_TLS_VERIFY']='1'
|
115
|
+
|
116
|
+
### Compare mesos vs docker tasks
|
117
|
+
|
118
|
+
docker_inspect = DockerHost.inspect
|
119
|
+
docker_tasks = docker_inspect.collect { |d| d[:mesos_task_id] }.compact.reject { |i| i.empty? }
|
120
|
+
docker_mesos_ids = docker_inspect.collect { |d| d[:name] }.compact.reject { |i| i.empty? }
|
121
|
+
|
122
|
+
puts "#" * 75 if config[:debug]
|
123
|
+
puts "* Docker tasks on #{my_fqdn}:" if config[:debug]
|
124
|
+
puts docker_inspect if config[:debug]
|
125
|
+
|
126
|
+
mesos_not_docker = mesos_tasks - docker_tasks
|
127
|
+
if ! mesos_not_docker.empty?
|
128
|
+
bad_news << "mesos tasks not running in docker: #{mesos_not_docker.join(',')}"
|
129
|
+
end
|
130
|
+
|
131
|
+
docker_not_mesos = docker_tasks - mesos_tasks
|
132
|
+
if ! docker_not_mesos.empty?
|
133
|
+
bad_news << "docker tasks not seen in mesos: #{docker_not_mesos.join(',')}"
|
134
|
+
end
|
135
|
+
|
136
|
+
good_news << "#{docker_tasks.size} docker tasks running"
|
137
|
+
|
138
|
+
### Compare consul-registered tasks vs docker tasks
|
139
|
+
docker_mesos_ids_no_panteras = docker_inspect.reject { |p| p[:name] =~ /panteras/ }.collect { |d| d[:name] }.compact.reject { |i| i.empty? }
|
140
|
+
consul_not_docker = consul_services - docker_mesos_ids_no_panteras
|
141
|
+
docker_not_consul = docker_mesos_ids_no_panteras - consul_services
|
142
|
+
|
143
|
+
if ! consul_not_docker.empty?
|
144
|
+
bad_news << "consul tasks not seen in docker: #{consul_not_docker.join(',')}"
|
145
|
+
end
|
146
|
+
|
147
|
+
if ! docker_not_consul.empty?
|
148
|
+
bad_news << "docker tasks not seen in consul: #{docker_not_consul.join(',')}"
|
149
|
+
end
|
150
|
+
|
151
|
+
if ! bad_news.empty?
|
152
|
+
puts "problems on #{my_fqdn}: #{bad_news.join(' ')}"
|
153
|
+
exit config[:error_exit_code]
|
154
|
+
end
|
155
|
+
|
156
|
+
puts good_news.join(' ')
|
157
|
+
|
158
|
+
__END__
|
159
|
+
|
160
|
+
##### MAPPING ###
|
161
|
+
CHECKS:
|
162
|
+
mesos vs marathon
|
163
|
+
mesos vs docker
|
164
|
+
docker vs consul
|
165
|
+
|
166
|
+
consul (api):
|
167
|
+
Slave Host, mesos-name (ServiceID) "paasslave46-2:mesos-c71da994-a35b-4dde-9970-d175c04da735:8080"
|
168
|
+
|
169
|
+
docker inspect:
|
170
|
+
container-id (Id) "7caf2037e7870ca4b0c5574a9f5a2415e43999fc83a9269c0360c34007957b2b"
|
171
|
+
mesos-name (Name) "mesos-c71da994-a35b-4dde-9970-d175c04da735"
|
172
|
+
mesos-task-id (ENV MESOS_TASK_ID) "vehicle-catalog-service.49383b0b-26eb-11e5-8a45-56847afe9799"
|
173
|
+
|
174
|
+
marathon API:
|
175
|
+
mesos-task-id (TaskId): "vehicle-catalog-service.49383b0b-26eb-11e5-8a45-56847afe9799"
|
176
|
+
|
177
|
+
|
178
|
+
mesos API:
|
179
|
+
"id": "vehicle-catalog-service.49383b0b-26eb-11e5-8a45-56847afe9799"
|
180
|
+
|
181
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'socket'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
$VERSION = '1.0.0'
|
8
|
+
$SEND_NSCA='/usr/sbin/send_nsca'
|
9
|
+
$SCRIPT='/usr/local/bin/panteras_api'
|
10
|
+
my_datacenter=Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address.split('.')[1]
|
11
|
+
$MESOS_MASTER="paasmaster#{my_datacenter.strip}-1.mobile.rz"
|
12
|
+
$NAGIOS_SERVICE='paas_consistency_check'
|
13
|
+
|
14
|
+
# default nagios host
|
15
|
+
defaults = { :nsca_host => 'monitor.mobile.rz', :debug => false, :error_code => 1 }
|
16
|
+
options = {}
|
17
|
+
|
18
|
+
OptionParser.new("Options: #{$0} -n NAGIOS_HOST [options]") do |opts|
|
19
|
+
|
20
|
+
opts.release = $VERSION
|
21
|
+
|
22
|
+
opts.on("-n","--nsca-host NAGIOS_HOST","") do |n|
|
23
|
+
options[:nsca_host] = n
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-d","--debug","") do |d|
|
27
|
+
options[:debug] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("-c", "--critical", "Exit with CRITICAL when problems seen. Default is WARNING") do |c|
|
31
|
+
options[:error_code] = 2
|
32
|
+
end
|
33
|
+
|
34
|
+
end.parse!
|
35
|
+
|
36
|
+
config = defaults.merge!(options)
|
37
|
+
|
38
|
+
|
39
|
+
$DEBUG = config[:debug]
|
40
|
+
my_hostname = Socket.gethostname.strip
|
41
|
+
my_fqdn = Socket.gethostbyname(my_hostname).first
|
42
|
+
nsca_host = config[:nsca_host]
|
43
|
+
output = []
|
44
|
+
error = []
|
45
|
+
puts "DEBUG MODE ON" if $DEBUG
|
46
|
+
puts "NSCA host set to: #{nsca_host}" if $DEBUG
|
47
|
+
|
48
|
+
def send_ncsa(nagios_host, host, result, code)
|
49
|
+
output = []
|
50
|
+
error = []
|
51
|
+
command="#{$SEND_NSCA} -c /etc/send_nsca.cfg -H #{nagios_host}"
|
52
|
+
puts "Running command: #{command}" if $DEBUG
|
53
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
54
|
+
|
55
|
+
result_line = "#{host}\t#{$NAGIOS_SERVICE}\t#{code}\t#{result}"
|
56
|
+
puts "Sending result to #{nagios_host}: #{result_line}" if $DEBUG
|
57
|
+
stdin.puts result_line
|
58
|
+
stdin.close
|
59
|
+
|
60
|
+
exit_status = wait_thr.value
|
61
|
+
|
62
|
+
unless exit_status.success?
|
63
|
+
$stderr.puts stderr.readlines
|
64
|
+
raise StandardError, "Command '#{command}' returned status #{exit_status.exitstatus}"
|
65
|
+
end
|
66
|
+
|
67
|
+
while line = stderr.gets
|
68
|
+
if line.strip.length > 0
|
69
|
+
puts "Error (send_ncsa): #{line}" if $DEBUG
|
70
|
+
error << line.strip
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
while line = stdout.gets
|
75
|
+
if line.strip.length > 0
|
76
|
+
puts "Output (send ncsa): #{line}" if $DEBUG
|
77
|
+
output << line.strip
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
puts output.join if ( ! output.empty? && $DEBUG )
|
84
|
+
puts error.join if ( ! error.empty? && $DEBUG )
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
command = "#{$SCRIPT} -m #{$MESOS_MASTER}"
|
89
|
+
puts "Running command: #{command}" if $DEBUG
|
90
|
+
|
91
|
+
|
92
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
93
|
+
|
94
|
+
exit_status = wait_thr.value
|
95
|
+
unless exit_status.success?
|
96
|
+
send_ncsa(nsca_host, my_hostname, stderr.readlines, config[:error_code])
|
97
|
+
raise StandardError, "Command '#{command}' returned status #{exit_status.exitstatus}"
|
98
|
+
end
|
99
|
+
|
100
|
+
while line = stderr.gets
|
101
|
+
if line.strip.length > 0
|
102
|
+
puts "Error: #{line}" if $DEBUG
|
103
|
+
error << line.strip
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
while line = stdout.gets
|
108
|
+
if line.strip.length > 0
|
109
|
+
puts "Output: #{line}" if $DEBUG
|
110
|
+
output << line.strip
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if ! error.empty?
|
115
|
+
error_msg = error.join
|
116
|
+
puts "#{my_hostname}: #{error_msg} exit_code #{config[:error_code]}"
|
117
|
+
send_ncsa(nsca_host, my_hostname, error.join, config[:error_code])
|
118
|
+
else
|
119
|
+
output_msg = output.join
|
120
|
+
puts "#{my_hostname}: #{output_msg} exit_code #{config[:error_code]}"
|
121
|
+
send_ncsa(nsca_host, my_hostname, output.join, 0)
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
data/lib/panteras_api.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "panteras_api/version"
|
2
|
+
require "panteras_api/utils"
|
3
|
+
require "panteras_api/http_utils"
|
4
|
+
require "panteras_api/mesos_cluster"
|
5
|
+
require "panteras_api/marathon_endpoint"
|
6
|
+
require "panteras_api/consul_cluster"
|
7
|
+
require "panteras_api/docker_host"
|
8
|
+
|
9
|
+
module PanterasApi
|
10
|
+
# Your code goes here...
|
11
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class ConsulCluster
|
2
|
+
include HTTPUtils
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize(host,port=8500)
|
6
|
+
@host = host
|
7
|
+
@port = port
|
8
|
+
@datacenters = datacenters
|
9
|
+
@dc = ""
|
10
|
+
@hostname = Utils.hostname
|
11
|
+
end
|
12
|
+
|
13
|
+
def dc=(datacenter)
|
14
|
+
raise ArgumentError, "#{datacenter} is not a valid datacenter" unless (@datacenters.include? datacenter)
|
15
|
+
@dc = datacenter
|
16
|
+
end
|
17
|
+
|
18
|
+
def datacenters
|
19
|
+
to_j(get_response_with_redirect(@host, '/v1/catalog/datacenters', @port))
|
20
|
+
end
|
21
|
+
|
22
|
+
def services
|
23
|
+
service_names.collect { |s| service s.to_s }
|
24
|
+
end
|
25
|
+
|
26
|
+
def service_names
|
27
|
+
services = to_j(get_response_with_redirect(@host, '/v1/catalog/services?dc=' + @dc, @port)).keys
|
28
|
+
# exclude the consul service itself
|
29
|
+
services.reject { |s| s.to_s == "consul" }
|
30
|
+
end
|
31
|
+
|
32
|
+
def my_services(hostname=@hostname)
|
33
|
+
raise ArgumentError, "missing hostname argument", caller if hostname.nil?
|
34
|
+
services.collect { |s| s.select { |i| i[:Node] =~ /^#{hostname}$/ } }.flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
def my_service_ids
|
38
|
+
my_services.collect { |s| s[:ServiceID].split(/:/)[1] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def service(service)
|
42
|
+
to_j(get_response_with_redirect(@host, '/v1/catalog/service/' + service + '?dc=' + @dc, @port))
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
class DockerHost
|
4
|
+
extend HTTPUtils
|
5
|
+
|
6
|
+
def self.ps
|
7
|
+
self.command("docker ps")[:output]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.running_containers
|
11
|
+
output = self.command("docker ps")[:output]
|
12
|
+
raise MesosConsulCommandError, "Command 'docker ps' did not return an array of strings" if (output.class != Array || output.length == 0)
|
13
|
+
|
14
|
+
result = output.map.with_index do |line, i|
|
15
|
+
next if i == 0
|
16
|
+
container_id, image, *center, name = line.split
|
17
|
+
{ container_id: container_id, name: name, image: image }
|
18
|
+
end
|
19
|
+
|
20
|
+
return result.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.inspect(*keys)
|
24
|
+
containers = self.running_containers
|
25
|
+
containers.map do |c|
|
26
|
+
inspect = self.command("docker inspect #{c[:container_id]}")[:output]
|
27
|
+
h = to_j(inspect.join).first
|
28
|
+
task_id = h[:Config][:Env].select { |env| env =~ /MESOS_TASK_ID=/ }.first
|
29
|
+
mesos_task_id = task_id.nil? ? '' : task_id.split(/=/)[1]
|
30
|
+
{ id: h[:Id], image: h[:Image], name: h[:Name][1..-1], mesos_task_id: mesos_task_id }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.inspect_partial
|
35
|
+
self.inspect
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
class MesosConsulCommandError < StandardError ; end
|
41
|
+
|
42
|
+
def self.command(command)
|
43
|
+
output = []
|
44
|
+
error = []
|
45
|
+
begin
|
46
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
47
|
+
|
48
|
+
exit_status = wait_thr.value
|
49
|
+
|
50
|
+
unless exit_status.success?
|
51
|
+
$stderr.puts stderr.readlines
|
52
|
+
raise MesosConsulCommandError, "Command '#{command}' returned status #{exit_status.exitstatus}"
|
53
|
+
end
|
54
|
+
|
55
|
+
while line = stderr.gets
|
56
|
+
$stderr.puts line
|
57
|
+
if line.strip.length > 0
|
58
|
+
error << line.strip
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
while line = stdout.gets
|
63
|
+
if line.strip.length > 0
|
64
|
+
output << line.strip
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
rescue Errno::ENOENT => e
|
71
|
+
raise MesosConsulCommandError, "Error while running command #{command}: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
return { :output => output, :error => error}
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module HTTPUtils
|
6
|
+
|
7
|
+
def get_response_with_redirect(*args)
|
8
|
+
response = Net::HTTP.get_response(*args)
|
9
|
+
if response.is_a?(Net::HTTPRedirection)
|
10
|
+
begin
|
11
|
+
redirect_uri = URI.parse(response.header['location'])
|
12
|
+
return redirect_uri
|
13
|
+
rescue SocketError => e
|
14
|
+
raise e, "Error connecting to #{redirect_uri}: #{e.message}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
response
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_j(response)
|
21
|
+
if response.is_a? Net::HTTPResponse
|
22
|
+
JSON.parse(response.body, :symbolize_names => true)
|
23
|
+
elsif response.is_a? String
|
24
|
+
JSON.parse(response, :symbolize_names => true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_url(url)
|
29
|
+
if (url =~ /\A#{URI::regexp}\z/)
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
def construct_uri(url)
|
36
|
+
raise Error::BadURLError unless (valid_url(url))
|
37
|
+
return URI.parse(url)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class MarathonEndpoint
|
2
|
+
include HTTPUtils
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
def initialize(host, port=8080)
|
6
|
+
@host = host
|
7
|
+
@port = port
|
8
|
+
end
|
9
|
+
|
10
|
+
def app(app_name)
|
11
|
+
raise ArgumentError, "Argument be a String" unless (app_name.class == String )
|
12
|
+
to_j(get_response_with_redirect(@host, '/v2/apps/' + app_name, @port))[:app]
|
13
|
+
end
|
14
|
+
|
15
|
+
def all_apps
|
16
|
+
to_j(get_response_with_redirect(@host, '/v2/apps/', @port))[:apps]
|
17
|
+
end
|
18
|
+
|
19
|
+
def app_names
|
20
|
+
all_apps.collect { |a| a[:id][1..-1] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def tasks_ids(app_name)
|
24
|
+
app(app_name)[:tasks].collect { |t| t[:id] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def my_tasks(hostname)
|
28
|
+
raise ArgumentError, "missing hostname argument", caller if hostname.nil?
|
29
|
+
app_names.collect { |n| app(n)[:tasks].select { |t| t[:host] =~ /^#{hostname}$/ } }
|
30
|
+
end
|
31
|
+
|
32
|
+
def my_task_ids(hostname)
|
33
|
+
my_tasks(hostname).collect { |t| t.collect { |a| a[:id] } }.flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def task_ids
|
37
|
+
app_names.collect { |a| tasks_ids(a) }
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class MesosCluster
|
7
|
+
include HTTPUtils
|
8
|
+
include Utils
|
9
|
+
|
10
|
+
attr_reader :state
|
11
|
+
|
12
|
+
def initialize(host, port=5050)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
@master_info = "/master/redirect"
|
16
|
+
@state_info = "/state.json"
|
17
|
+
@state = parse_state
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_state
|
21
|
+
redirect_response = get_response_with_redirect(@host, @master_info, @port)
|
22
|
+
|
23
|
+
if redirect_response.is_a?(URI::HTTP)
|
24
|
+
if ! valid_url(redirect_response.to_s)
|
25
|
+
raise StandardError, "Response from http://#{@host}:#{@port}#{@master_info} is not a valid url: #{redirect_response.to_s}"
|
26
|
+
end
|
27
|
+
return to_j(get_response_with_redirect(URI.join(redirect_response.to_s, @state_info)))
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def master_hostname
|
33
|
+
state[:hostname]
|
34
|
+
end
|
35
|
+
|
36
|
+
def master_id
|
37
|
+
state[:id]
|
38
|
+
end
|
39
|
+
|
40
|
+
def frameworks
|
41
|
+
state[:frameworks]
|
42
|
+
end
|
43
|
+
|
44
|
+
def tasks_completed
|
45
|
+
state[:frameworks].collect { |f| f[:completed_tasks].collect { |t| t } }
|
46
|
+
end
|
47
|
+
|
48
|
+
def tasks
|
49
|
+
results = frameworks.collect { |f| f[:tasks].collect { |t| t[:slave_hostname] = slave_hostname_by_id(t[:slave_id]) ; t } }
|
50
|
+
results.reject { |r| r.nil? or r.length == 0 }.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def task_ids
|
54
|
+
tasks.collect { |t| t[:id] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def my_tasks_ids(hostname)
|
58
|
+
raise ArgumentError, "missing hostname argument", caller if hostname.nil?
|
59
|
+
tasks.select { |t| t[:slave_hostname] =~ /^#{hostname}$/ }.collect { |t| t[:id] }
|
60
|
+
end
|
61
|
+
|
62
|
+
def resources
|
63
|
+
state[:frameworks].collect { |f| f[:used_resources] }
|
64
|
+
end
|
65
|
+
|
66
|
+
def slave_hostname_by_id(id)
|
67
|
+
slaves.select { |s| s[:id] == id }.first[:hostname]
|
68
|
+
end
|
69
|
+
|
70
|
+
def slaves
|
71
|
+
state[:slaves].collect do |s|
|
72
|
+
s.select { |k,v| [:hostname, :id].include?(k)}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
JSON.pretty_generate @state
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
def self.hostname
|
5
|
+
Socket.gethostname.strip
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.fully_qualified_hostname
|
9
|
+
begin
|
10
|
+
Socket.gethostbyname(Socket.gethostname.strip).first
|
11
|
+
rescue SocketError => e
|
12
|
+
raise StandardError, "Could not get fully qualified domain name using gethostbyname."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'panteras_api/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "panteras_api"
|
8
|
+
spec.version = PanterasApi::VERSION
|
9
|
+
spec.authors = ["Jonathan Colby"]
|
10
|
+
spec.email = ["jcolby@team.mobile.de"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby API library for Panteras PaaS platform.}
|
13
|
+
spec.description = %q{A convenient api for getting information from consul, mesos, marathon, and docker in the Panteras PaaS infrastructure.}
|
14
|
+
spec.homepage = "https://github.com/joncolby/panteras_api"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
# spec.bindir = "bin"
|
18
|
+
# spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# if spec.respond_to?(:metadata)
|
22
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com' to prevent pushes to rubygems.org, or delete to allow pushes to any server."
|
23
|
+
# end
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: panteras_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Colby
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-02 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.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description: A convenient api for getting information from consul, mesos, marathon,
|
42
|
+
and docker in the Panteras PaaS infrastructure.
|
43
|
+
email:
|
44
|
+
- jcolby@team.mobile.de
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- ".rspec"
|
51
|
+
- ".travis.yml"
|
52
|
+
- Gemfile
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- extra/mesos_consul_consistency_check
|
56
|
+
- extra/mesos_consul_consistency_check_nagios_wrapper
|
57
|
+
- lib/panteras_api.rb
|
58
|
+
- lib/panteras_api/consul_cluster.rb
|
59
|
+
- lib/panteras_api/docker_host.rb
|
60
|
+
- lib/panteras_api/http_utils.rb
|
61
|
+
- lib/panteras_api/marathon_endpoint.rb
|
62
|
+
- lib/panteras_api/mesos_cluster.rb
|
63
|
+
- lib/panteras_api/utils.rb
|
64
|
+
- lib/panteras_api/version.rb
|
65
|
+
- panteras_api.gemspec
|
66
|
+
homepage: https://github.com/joncolby/panteras_api
|
67
|
+
licenses: []
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.4.5
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: Ruby API library for Panteras PaaS platform.
|
89
|
+
test_files: []
|