bixby-agent 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.testguardrc +1 -0
  4. data/.travis.yml +25 -0
  5. data/Gemfile +61 -0
  6. data/Gemfile.lock +237 -0
  7. data/LICENSE +21 -0
  8. data/Rakefile +13 -0
  9. data/VERSION +1 -0
  10. data/bin/bixby-agent +15 -0
  11. data/bixby-agent.gemspec +186 -0
  12. data/etc/bixby-god.initd +70 -0
  13. data/etc/bixby.god +20 -0
  14. data/etc/god.d/bixby-agent.god +71 -0
  15. data/lib/bixby-agent.rb +16 -0
  16. data/lib/bixby-agent/agent.rb +98 -0
  17. data/lib/bixby-agent/agent/config.rb +109 -0
  18. data/lib/bixby-agent/agent/crypto.rb +73 -0
  19. data/lib/bixby-agent/agent/handshake.rb +81 -0
  20. data/lib/bixby-agent/agent/shell_exec.rb +111 -0
  21. data/lib/bixby-agent/agent_handler.rb +38 -0
  22. data/lib/bixby-agent/app.rb +208 -0
  23. data/lib/bixby-agent/app/cli.rb +112 -0
  24. data/lib/bixby-agent/config_exception.rb +5 -0
  25. data/lib/bixby-agent/help/system_time.rb +41 -0
  26. data/lib/bixby-agent/version.rb +8 -0
  27. data/lib/bixby-agent/websocket/client.rb +186 -0
  28. data/tasks/cane.rake +14 -0
  29. data/tasks/coverage.rake +2 -0
  30. data/tasks/coveralls.rake +11 -0
  31. data/tasks/jeweler.rake +21 -0
  32. data/tasks/test.rake +5 -0
  33. data/tasks/yard.rake +6 -0
  34. data/test/base.rb +92 -0
  35. data/test/helper.rb +29 -0
  36. data/test/stub_eventmachine.rb +38 -0
  37. data/test/support/root_dir/bixby.yml +8 -0
  38. data/test/support/root_dir/id_rsa +27 -0
  39. data/test/support/root_dir/server +27 -0
  40. data/test/support/root_dir/server.pub +9 -0
  41. data/test/support/test_bundle/bin/cat +2 -0
  42. data/test/support/test_bundle/bin/echo +2 -0
  43. data/test/support/test_bundle/digest +17 -0
  44. data/test/support/test_bundle/manifest.json +0 -0
  45. data/test/test_agent.rb +110 -0
  46. data/test/test_agent_exec.rb +53 -0
  47. data/test/test_agent_handler.rb +72 -0
  48. data/test/test_app.rb +138 -0
  49. data/test/test_bixby_common.rb +18 -0
  50. data/test/test_crypto.rb +78 -0
  51. data/test/websocket/test_client.rb +110 -0
  52. metadata +557 -0
@@ -0,0 +1,5 @@
1
+
2
+ module Bixby
3
+ class ConfigException < Exception
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Bixby
3
+ module Help
4
+ class SystemTime
5
+
6
+ extend Bixby::Script::Distro
7
+ extend Bixby::Script::Platform
8
+
9
+ def self.print
10
+ $stderr.puts self.message
11
+ end
12
+
13
+ # Registration may fail if the system clock is too far in the past (more than 15 minutes)
14
+ # Show the user how to fix the issue and try again
15
+ def self.message
16
+ s = " it appears your system clock is out of sync"
17
+
18
+ res = HTTPI.get("http://google.com")
19
+ if not res.error? then
20
+ current_time = Time.parse(res.headers["Date"]).utc
21
+ s += " > current time: #{current_time}"
22
+ s += " > system time: #{Time.new.utc}"
23
+ end
24
+
25
+ $stderr.puts
26
+ if linux? && ubuntu? then
27
+ s += " to fix:"
28
+ s += " sudo apt-get install ntpdate && sudo ntpdate ntp.ubuntu.com"
29
+ elsif linux? && centos? then
30
+ s += " to fix:"
31
+ s += " sudo yum install ntpdate && sudo ntpdate ntp.ubuntu.com"
32
+ else
33
+ s += " you can fix this on most unix systems by running 'sudo ntpdate ntp.ubuntu.com'"
34
+ end
35
+
36
+ s
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+
2
+ module Bixby
3
+ class Agent
4
+
5
+ VERSION = File.new(File.expand_path(File.join(File.dirname(__FILE__), "../../VERSION"))).read.strip
6
+
7
+ end
8
+ end
@@ -0,0 +1,186 @@
1
+
2
+ require 'bixby-agent/help/system_time'
3
+
4
+ require 'faye/websocket'
5
+ require 'eventmachine'
6
+ require 'timeout'
7
+
8
+ module Bixby
9
+ module WebSocket
10
+
11
+ # WebSocket Client
12
+ class Client
13
+
14
+ include Bixby::Log
15
+
16
+ attr_reader :ws, :api
17
+
18
+ def initialize(url, handler)
19
+ @url = url
20
+ @handler = handler
21
+ @tries = 0
22
+ @exiting = false
23
+ end
24
+
25
+ # Start the Client thread
26
+ #
27
+ # NOTE: This call never returns!
28
+ def start
29
+
30
+ @exiting = false
31
+
32
+ Kernel.trap("EXIT") do
33
+ # :nocov:
34
+ @exiting = true
35
+ # :nocov:
36
+ end
37
+
38
+ logger.debug "connecting to #{@url}"
39
+ EM.run {
40
+ connect()
41
+ }
42
+ end
43
+
44
+ def stop
45
+ @exiting = true
46
+ EM.stop_event_loop if EM.reactor_running?
47
+ end
48
+
49
+
50
+ private
51
+
52
+ # Connect to the WebSocket endpoint given by @url. Will attempt to keep
53
+ # the connection open forever, reconnecting as needed.
54
+ def connect
55
+
56
+ # Ping is set to 55 sec to workaround issues with certain gateway devices which have a hard
57
+ # 60 sec timeout, like the AWS ELB:
58
+ # http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/ts-elb-healthcheck.html
59
+ @ws = Faye::WebSocket::Client.new(@url, nil, :ping => 55)
60
+ @api = Bixby::WebSocket::APIChannel.new(@ws, @handler)
61
+
62
+ ws.on :open do |e|
63
+ begin
64
+ authenticate(e)
65
+ rescue Exception => ex
66
+ logger.error ex
67
+ end
68
+ end
69
+
70
+ ws.on :message do |e|
71
+ begin
72
+ api.message(e)
73
+ rescue Exception => ex
74
+ raise ex if ex.kind_of? SystemExit # possible when message is a connect() response
75
+ logger.error ex
76
+ end
77
+ end
78
+
79
+ ws.on(:close, &lambda { |e|
80
+ begin
81
+ was_connected = api.connected?
82
+ api.close(e)
83
+ return if @exiting or not EM.reactor_running?
84
+ if was_connected then
85
+ logger.info "lost connection to manager"
86
+ else
87
+ logger.debug "failed to connect"
88
+ end
89
+ reconnect()
90
+ rescue Exception => ex
91
+ logger.error ex
92
+ end
93
+ })
94
+ end
95
+
96
+ # Send a connection request to authenticate with the manager
97
+ def authenticate(e)
98
+ logger.debug "connected to manager, authenticating"
99
+
100
+ json_req = JsonRequest.new("", "")
101
+ signed_req = SignedJsonRequest.new(json_req, Bixby.agent.access_key, Bixby.agent.secret_key)
102
+ auth_req = Request.new(signed_req, SecureRandom.uuid, "connect")
103
+
104
+ id = api.execute_async(auth_req) do |ret|
105
+ if ret.success? then
106
+ logger.info "Successfully connected to manager at #{@url}"
107
+ api.open(e)
108
+ @tries = 0
109
+
110
+ else
111
+ if ret.message =~ /900 seconds old/ then
112
+ logger.error "error authenticating with manager:\n" + Help::SystemTime.message
113
+ else
114
+ logger.error "error authenticating with manager: #{ret.code} #{ret.message}"
115
+ end
116
+ logger.error "exiting since we failed to auth"
117
+ @exiting = true
118
+ exit 1 # bail out since we failed to connect, nothing to do
119
+ end
120
+ end
121
+
122
+ # Start a thread to watch for auth timeout
123
+ Thread.new do
124
+ begin
125
+ sec = 60
126
+ Timeout.timeout(sec) do
127
+ api.fetch_response(id) # blocks until request is completed
128
+ end
129
+
130
+ rescue Timeout::Error => ex
131
+ logger.warn("Authentication timed out after #{sec} seconds; trying again")
132
+ reconnect()
133
+
134
+ rescue Exception => ex
135
+ logger.error(ex)
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ def cleanup
142
+ @api.close(nil) if @api
143
+ @ws.close if @ws
144
+ end
145
+
146
+ def reconnect
147
+ if backoff() then
148
+ cleanup()
149
+ connect()
150
+ end
151
+ end
152
+
153
+ # Delay reconnection by a slowly increasing interval
154
+ def backoff
155
+
156
+ if @exiting or not EM.reactor_running? then
157
+ # shutting down, don't try to reconnect
158
+ logger.debug "not retrying since we are shutting down"
159
+ return false
160
+ end
161
+
162
+ @tries += 1
163
+ if @tries == 1 then
164
+ logger.debug "retrying immediately"
165
+
166
+ # :nocov:
167
+ elsif @tries == 2 then
168
+ logger.debug "retrying every 1 sec"
169
+ sleep 1
170
+ elsif @tries <= 30 then
171
+ sleep 1
172
+ elsif @tries == 31 then
173
+ logger.debug "retrying every 5 sec"
174
+ sleep 5
175
+ else
176
+ sleep 5
177
+ end
178
+ # :nocov:
179
+
180
+ true
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+ end
data/tasks/cane.rake ADDED
@@ -0,0 +1,14 @@
1
+
2
+ begin
3
+ require 'cane/rake_task'
4
+
5
+ desc "Run cane to check quality metrics"
6
+ Cane::RakeTask.new(:quality) do |cane|
7
+ cane.abc_max = 10
8
+ cane.add_threshold 'coverage/covered_percent', :>=, 99
9
+ cane.no_style = true
10
+ end
11
+
12
+ task :default => :quality
13
+ rescue LoadError
14
+ end
@@ -0,0 +1,2 @@
1
+
2
+ require "easycov/rake"
@@ -0,0 +1,11 @@
1
+
2
+ desc "Report coverage to coveralls"
3
+ task :coveralls do
4
+ require "easycov"
5
+ require "coveralls"
6
+
7
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
8
+ if File.exists? File.join(EasyCov.path, ".resultset.json") then
9
+ SimpleCov::ResultMerger.merged_result.format!
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+
2
+ require 'jeweler'
3
+
4
+ Jeweler::Tasks.new do |gemspec|
5
+ gemspec.name = "bixby-agent"
6
+ gemspec.summary = "Bixby Agent"
7
+ gemspec.description = "Bixby Agent"
8
+ gemspec.email = "chetan@pixelcop.net"
9
+ gemspec.homepage = "http://github.com/chetan/bixby-agent"
10
+ gemspec.authors = ["Chetan Sarva"]
11
+ gemspec.license = "MIT"
12
+
13
+ gemspec.executables = %w{ bixby-agent }
14
+
15
+ # exclude these bin scripts for now
16
+ %w{ ci.sh ci_setup.sh install.sh }.each do |f|
17
+ gemspec.files.exclude "bin/#{f}"
18
+ end
19
+
20
+ end
21
+ Jeweler::RubygemsDotOrgTasks.new
data/tasks/test.rake ADDED
@@ -0,0 +1,5 @@
1
+
2
+ require "micron/rake"
3
+ Micron::Rake.new do |task|
4
+ end
5
+ task :default => :test
data/tasks/yard.rake ADDED
@@ -0,0 +1,6 @@
1
+
2
+ begin
3
+ require 'yard'
4
+ YARD::Rake::YardocTask.new
5
+ rescue LoadError
6
+ end
data/test/base.rb ADDED
@@ -0,0 +1,92 @@
1
+
2
+ require 'helper'
3
+ require 'micron/test_case/redir_logging'
4
+
5
+ module Bixby
6
+ module Test
7
+
8
+ class TestCase < Micron::TestCase
9
+
10
+ include Micron::TestCase::RedirLogging
11
+ self.redir_logger = Logging.logger[Bixby]
12
+
13
+ def setup
14
+ super
15
+ WebMock.reset!
16
+
17
+ @git_path = File.expand_path(File.join(File.dirname(__FILE__), ".."))
18
+ @support_path = File.join(@git_path, "test", "support")
19
+
20
+ @manager_uri = "http://localhost:3000"
21
+ @tenant = "pixelcop"
22
+ @password = "foobar"
23
+ @root_dir = Dir.mktmpdir("bixby-agent-test-")
24
+ @port = 9999
25
+
26
+ Bixby.manager_uri = @manager_uri
27
+ @api_url = @manager_uri + "/api"
28
+ `rm -rf #{@root_dir}`
29
+
30
+ vendor_path = File.join(@root_dir, "repo", "vendor")
31
+ `mkdir -p #{vendor_path}`
32
+ `cp -a #{File.join(@git_path, "repo/vendor/*")} #{vendor_path}`
33
+
34
+ ENV["BIXBY_NOCRYPTO"] = "1"
35
+ ENV["BIXBY_HOME"] = @root_dir
36
+ ARGV.clear
37
+ end
38
+
39
+ def teardown
40
+ `rm -rf #{@root_dir}`
41
+ @agent = nil
42
+ ENV["BIXBY_HOME"] = nil
43
+ end
44
+
45
+ def setup_existing_agent
46
+ ENV["BIXBY_HOME"] = @root_dir
47
+ src = File.join(@support_path, "root_dir")
48
+ dest = File.join(@root_dir, "etc")
49
+ FileUtils.mkdir_p(dest)
50
+ FileUtils.copy_entry(src, dest)
51
+ @agent = Agent.create
52
+ end
53
+
54
+ def setup_root
55
+ # copy repo to path
56
+ `mkdir -p #{@root_dir}/repo/support`
57
+ `cp -a #{@bundle_path} #{@root_dir}/repo/support/`
58
+ end
59
+
60
+ def setup_test_bundle(repo, bundle, command, digest=nil)
61
+ @bundle_path = File.join(@support_path, "test_bundle")
62
+ @c = CommandSpec.new({ :repo => repo, :bundle => bundle,
63
+ :command => command, :digest => digest })
64
+ end
65
+
66
+ def create_new_agent
67
+ ENV["BIXBY_HOME"] = nil
68
+ @agent = Agent.create(@root_dir)
69
+ end
70
+
71
+
72
+ # common routines for crypto tests
73
+
74
+ def server_private_key
75
+ s = File.join(@root_dir, "etc", "server")
76
+ OpenSSL::PKey::RSA.new(File.read(s))
77
+ end
78
+
79
+ def encrypt_for_agent(msg)
80
+ Bixby::CryptoUtil.encrypt(msg, "server_uuid", @agent.private_key, server_private_key)
81
+ end
82
+
83
+ def decrypt_from_agent(data)
84
+ data = StringIO.new(data, 'rb')
85
+ uuid = data.readline.strip
86
+ Bixby::CryptoUtil.decrypt(data, server_private_key, @agent.private_key)
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development, :test)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+
11
+ require 'micron/minitest'
12
+
13
+ # load curb first so webmock can stub it out as necessary
14
+ require 'curb'
15
+ require 'webmock'
16
+ include WebMock::API
17
+ require 'mocha/setup'
18
+
19
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
20
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
21
+ ENV["RUBYLIB"] = $:.first
22
+ require 'bixby-agent'
23
+
24
+ require "base"
25
+ Dir.glob(File.dirname(__FILE__) + "/../lib/**/*.rb").each{ |f| require f }
26
+
27
+ EasyCov.path = "coverage"
28
+ EasyCov.filters << EasyCov::IGNORE_GEMS << EasyCov::IGNORE_STDLIB
29
+ EasyCov.start
@@ -0,0 +1,38 @@
1
+
2
+ module EventMachine
3
+ class << self
4
+
5
+ alias_method :_orig_run, :run
6
+ alias_method :_orig_next_tick, :next_tick
7
+ alias_method :_orig_stop, :stop
8
+ alias_method :_orig_stop_event_loop, :stop_event_loop
9
+
10
+ def run_immediately(&block)
11
+ @reactor_running = true
12
+ block.call() if block
13
+ end
14
+
15
+ def tick_immediately(&block)
16
+ block.call() if block
17
+ end
18
+
19
+ def noop
20
+ end
21
+
22
+ def stub!
23
+ define_singleton_method :run, method(:run_immediately)
24
+ define_singleton_method :next_tick, method(:tick_immediately)
25
+ define_singleton_method :stop, method(:noop)
26
+ define_singleton_method :stop_event_loop, method(:noop)
27
+ end
28
+
29
+ def disable_stub!
30
+ define_singleton_method :run, method(:_orig_run)
31
+ define_singleton_method :next_tick, method(:_orig_next_tick)
32
+ define_singleton_method :stop, method(:_orig_stop)
33
+ define_singleton_method :stop_event_loop, method(:_orig_stop_event_loop)
34
+ end
35
+
36
+ end
37
+ end
38
+