bixby-agent 0.3.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.
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
+