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,81 @@
1
+
2
+ require 'facter'
3
+ require 'uuidtools'
4
+
5
+ require "bixby-agent/agent/crypto"
6
+
7
+ module Bixby
8
+ class Agent
9
+
10
+ module Handshake
11
+
12
+ include Crypto
13
+
14
+ # Register the agent with the server
15
+ #
16
+ # @param [String] url Bixby manager URL
17
+ # @param [String] tenant Tenant name
18
+ # @param [String] password Tenant registration password
19
+ # @param [String] tags Comma-separated list of tags (e.g., "foo,bar")
20
+ #
21
+ # @return [JsonResponse] response from server
22
+ def register_agent(url, tenant, password, tags=nil)
23
+ Bixby.manager_uri = @manager_uri = url
24
+ ret = Bixby::Inventory.register_agent({
25
+ :uuid => @uuid,
26
+ :public_key => self.public_key.to_s,
27
+ :hostname => get_hostname(),
28
+ :tenant => tenant,
29
+ :password => password,
30
+ :tags => tags,
31
+ :version => Bixby::Agent::VERSION
32
+ })
33
+
34
+ if ret.fail? then
35
+ return ret
36
+ end
37
+
38
+ @access_key = ret.data["access_key"]
39
+ @secret_key = ret.data["secret_key"]
40
+ Bixby.client = Bixby::Client.new(access_key, secret_key)
41
+
42
+ # success, store server's pub key
43
+ File.open(self.server_key_file, 'w') do |f|
44
+ f.puts(ret.data["server_key"])
45
+ end
46
+
47
+ return ret
48
+ end
49
+
50
+ def mac_changed?
51
+ (not @mac_address.nil? and (@mac_address != get_mac_address()))
52
+ end
53
+
54
+ def get_hostname
55
+ `hostname`.strip
56
+ end
57
+
58
+ # Get the mac address of the system's primary interface
59
+ def get_mac_address
60
+ return @mac if not @mac.nil?
61
+ Facter.collection.fact(:ipaddress).value # force value to be loaded now (usually lazy-loaded)
62
+ Facter.collection.fact(:interfaces).value
63
+ Facter.collection.fact(:macaddress).value
64
+ vals = Facter.collection.to_hash
65
+ ip = vals["ipaddress"]
66
+ raise "Unable to find IP address" if ip.nil?
67
+ # use the primary IP of the system to find the associated interface name (e.g., en0 or eth0)
68
+ int = vals.find{ |k,v| v == ip && k != "ipaddress" }.first.to_s.split(/_/)[1]
69
+ raise "Unable to find primary interface" if int.nil? or int.empty?
70
+ # finally, get the mac address
71
+ @mac = vals["macaddress_#{int}"]
72
+ end
73
+
74
+ def create_uuid
75
+ UUIDTools::UUID.random_create.hexdigest
76
+ end
77
+
78
+ end # Handshake
79
+
80
+ end # Agent
81
+ end # Bixby
@@ -0,0 +1,111 @@
1
+
2
+ require 'mixlib/shellout'
3
+
4
+ module Bixby
5
+ class Agent
6
+
7
+ module ShellExec
8
+
9
+ # Shell exec a local command with the given params
10
+ #
11
+ # @param [Hash] params CommandSpec hash
12
+ # @option params [String] :repo
13
+ # @option params [String] :bundle
14
+ # @option params [String] :command
15
+ # @option params [String] :args
16
+ # @option params [String] :stdin
17
+ # @option params [String] :digest Expected bundle digest
18
+ # @option params [Hash] :env Hash of extra ENV key/values to pass to sub-shell
19
+ # @option params [String] :user User to run as
20
+ # @option params [String] :group Group to run as
21
+ #
22
+ # @return [CommandResponse]
23
+ #
24
+ # @raise [BundleNotFound] If bundle doesn't exist or digest does not match
25
+ # @raise [CommandNotFound] If command doesn't exist
26
+ def shell_exec(params)
27
+ digest = params.delete("digest") || params.delete(:digest)
28
+
29
+ spec = CommandSpec.new(params)
30
+ log.debug { "shell_exec:\n" + spec.to_s + "\n" }
31
+ spec.validate(digest)
32
+
33
+ cmd = "#{spec.command_file} #{spec.args}"
34
+
35
+ # Cleanup the ENV and execute
36
+ old_env = {}
37
+ %W{BUNDLE_BIN_PATH BUNDLE_GEMFILE}.each{ |r|
38
+ old_env[r] = ENV.delete(r) if ENV.include?(r) }
39
+
40
+ logger.debug("exec: #{cmd}")
41
+ shell = Mixlib::ShellOut.new(cmd, :input => spec.stdin,
42
+ :env => spec.env,
43
+ :user => uid(spec.user),
44
+ :group => gid(spec.group))
45
+
46
+ shell.run_command
47
+
48
+ old_env.each{ |k,v| ENV[k] = v } # reset the ENV
49
+
50
+ return CommandResponse.new({ :status => shell.exitstatus,
51
+ :stdout => shell.stdout,
52
+ :stderr => shell.stderr })
53
+ end
54
+
55
+
56
+ private
57
+
58
+ # Return uid of 'bixby' user, if it exists
59
+ #
60
+ # @param [String] user username to run as [Optional, default=bixby]
61
+ # @return [Fixnum]
62
+ def uid(user)
63
+ if Process.uid != 0 then
64
+ logger.warn("Can't change effective uid unless running as root")
65
+ return nil
66
+ end
67
+
68
+ if user then
69
+ begin
70
+ return Etc.getpwnam(user).uid
71
+ rescue ArgumentError => ex
72
+ logger.warn("Username '#{user}' was invalid: #{ex.message}")
73
+ end
74
+ end
75
+
76
+ begin
77
+ return Etc.getpwnam("bixby").uid
78
+ rescue ArgumentError
79
+ end
80
+ return nil
81
+ end
82
+
83
+ # Return uid of 'bixby' group, if it exists
84
+ #
85
+ # @param [String] group group to run as [Optional, default=bixby]
86
+ # @return [Fixnum]
87
+ def gid(group)
88
+ if Process.uid != 0 then
89
+ logger.warn("Can't change effective gid unless running as root")
90
+ return nil
91
+ end
92
+
93
+ if group then
94
+ begin
95
+ return Etc.getgrnam(group).gid
96
+ rescue ArgumentError => ex
97
+ logger.warn("Group '#{group}' was invalid: #{ex.message}")
98
+ end
99
+ end
100
+
101
+ begin
102
+ return Etc.getgrnam("bixby").gid
103
+ rescue ArgumentError
104
+ end
105
+ return nil
106
+ end
107
+
108
+ end # Exec
109
+
110
+ end # Agent
111
+ end # Bixby
@@ -0,0 +1,38 @@
1
+
2
+ module Bixby
3
+
4
+ class AgentHandler < Bixby::RpcHandler
5
+
6
+ include Bixby::Log
7
+ # include Bixby::CryptoUti
8
+
9
+ def initialize(request)
10
+ @request = request
11
+ end
12
+
13
+ def handle(json_req)
14
+
15
+ begin
16
+ cmd_res = Bixby.agent.shell_exec(json_req.params)
17
+ log.debug { cmd_res.to_s + "\n---\n\n\n" }
18
+ return cmd_res.to_json_response
19
+
20
+ rescue Exception => ex
21
+ if ex.kind_of? BundleNotFound then
22
+ log.debug(ex.message)
23
+ return JsonResponse.bundle_not_found(ex.message)
24
+
25
+ elsif ex.kind_of? CommandNotFound then
26
+ log.debug(ex.message)
27
+ return JsonResponse.command_not_found(ex.message)
28
+ end
29
+
30
+ log.error(ex)
31
+ return JsonResponse.new("fail", ex.message, nil, 500)
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,208 @@
1
+
2
+ require 'bixby-agent/websocket/client'
3
+ require 'bixby-agent/agent_handler'
4
+ require 'bixby-agent/app/cli'
5
+ require 'bixby-agent/help/system_time'
6
+
7
+ require 'daemons'
8
+ require 'highline/import'
9
+
10
+ module Bixby
11
+ class App
12
+
13
+ include CLI
14
+ include Bixby::Log
15
+
16
+ # Load Agent
17
+ #
18
+ # Load the agent from $BIXBY_HOME. If no existing configuration was found,
19
+ # try to register with the server if we have the correct parameters.
20
+ def load_agent
21
+
22
+ begin
23
+ agent = Agent.create(@config[:directory])
24
+ rescue Exception => ex
25
+ if ex.message =~ /manager URI/ then
26
+ # if unable to load from config and no/bad uri passed, bail!
27
+ $stderr.puts "ERROR: a valid manager URI is required on first run"
28
+ $stderr.puts
29
+ $stderr.puts @opt_parser.help()
30
+ exit 1
31
+ end
32
+ raise ex
33
+ end
34
+
35
+ # TODO disable mac detection for now; it doesn't work in certain cases
36
+ # e.g., when you stop/start an instance on EC2 a new mac is issued
37
+ #
38
+ # if not agent.new? and agent.mac_changed? then
39
+ # # loaded from config and mac has changed
40
+ # agent = Agent.create(opts, false)
41
+ # end
42
+
43
+ if agent.new? then
44
+
45
+ if !@config[:register] then
46
+ # --register not passed, bail out
47
+ if File.exists? agent.config_file then
48
+ $stderr.puts "Unable to load agent config from #{agent.config_file}; pass --register to reinitialize"
49
+ else
50
+ $stderr.puts "Unable to load agent from BIXBY_HOME=#{ENV['BIXBY_HOME']}; pass --register to initialize"
51
+ end
52
+ exit 1
53
+ end
54
+
55
+ # validate uri
56
+ uri = @argv.shift || @config[:register]
57
+ begin
58
+ if uri.nil? or URI.parse(uri).nil? or URI.join(uri, "/api").nil? then
59
+ raise ConfigException, "Missing manager URI", caller
60
+ end
61
+ rescue URI::Error => ex
62
+ raise ConfigException, "Bad manager URI: '#{uri}'"
63
+ end
64
+
65
+ tenant = @config[:tenant] || HighLine.new.ask("Tenant: ")
66
+ password = @config[:password] || HighLine.new.ask("Enter agent registration password: ") { |q| q.echo = "*" }
67
+
68
+ # register
69
+ $stdout.puts "Going to register with manager: #{uri}"
70
+ if (ret = agent.register_agent(uri, tenant, password, @config[:tags])).fail? then
71
+ $stderr.puts "error: failed to register with manager!"
72
+ $stderr.puts "reason:"
73
+ if ret.message =~ /900 seconds old/ then
74
+ Help::SystemTime.print()
75
+ else
76
+ $stderr.puts " #{ret.message}"
77
+ end
78
+ exit 1
79
+ end
80
+ agent.save_config()
81
+ ARGV.clear # make sure it's empty so daemon starts properly
82
+ $stdout.puts "Registration successful; launching bixby-agent into background"
83
+ end
84
+ agent
85
+ end
86
+
87
+ # Run the agent app!
88
+ #
89
+ # This is the main method. Will boot and configure the agent, connect to the
90
+ # server and start the daemon.
91
+ def run!
92
+ # load agent from config or cli opts
93
+ agent = load_agent()
94
+
95
+ fix_ownership()
96
+
97
+ # debug mode, stay in front
98
+ if @config[:debug] then
99
+ Logging::Logger.root.add_appenders("stdout")
100
+ return start_websocket_client()
101
+ end
102
+
103
+ # start daemon
104
+ validate_argv()
105
+ daemon_dir = Bixby.path("var")
106
+ ensure_state_dir(daemon_dir)
107
+ close_fds()
108
+
109
+ daemon_opts = {
110
+ :dir => daemon_dir,
111
+ :dir_mode => :normal,
112
+ :log_output => true,
113
+ :stop_proc => lambda { logger.info "Agent shutdown on service stop command" }
114
+ }
115
+
116
+ Daemons.run_proc("bixby-agent", daemon_opts) do
117
+ Logging.logger.root.clear_appenders
118
+ start_websocket_client()
119
+ end
120
+ end
121
+
122
+ # Open the WebSocket channel with the Manager
123
+ #
124
+ # NOTE: this call will not return!
125
+ def start_websocket_client
126
+ # make sure log level is still set correctly here
127
+ Bixby::Log.setup_logger(:level => Logging.appenders["file"].level)
128
+ logger.info "Started Bixby Agent"
129
+ @client = Bixby::WebSocket::Client.new(Bixby.agent.manager_ws_uri, AgentHandler)
130
+ trap_signals()
131
+ @client.start
132
+ end
133
+
134
+ def trap_signals
135
+ %w{INT QUIT TERM}.each do |sig|
136
+ Kernel.trap(sig) do
137
+ @client.stop()
138
+ puts # to get a blank line after the ^C in the term
139
+ reason = sig + (sig == "INT" ? " (^C)" : "")
140
+ logger.warn "caught #{reason} signal; exiting"
141
+ end
142
+ end
143
+ end
144
+
145
+ # If running as root, fix ownership of var and etc dirs
146
+ def fix_ownership
147
+ return if Process.uid != 0
148
+ begin
149
+ uid = Etc.getpwnam("bixby").uid
150
+ gid = Etc.getgrnam("bixby").gid
151
+ # user/group exists, chown
152
+ File.chown(uid, gid, Bixby.path("var"), Bixby.path("etc"))
153
+ rescue ArgumentError
154
+ end
155
+ end
156
+
157
+ # Validate ARGV
158
+ #
159
+ # If empty, default to "start", otherwise make sure we have a valid option
160
+ # for daemons.
161
+ #
162
+ # @raise [SystemExit] on invalid arg
163
+ def validate_argv
164
+ if ARGV.empty? then
165
+ ARGV << "start"
166
+ else
167
+ if not %w{start stop restart zap status}.include? ARGV.first then
168
+ $stderr.puts "ERROR: invalid command '#{ARGV.first}'"
169
+ $stderr.puts
170
+ $stderr.puts @opt_parser.help()
171
+ exit 1
172
+ end
173
+ end
174
+ end
175
+
176
+ # Ensure that the var dir exists and is writable
177
+ #
178
+ # @raise [SystemExit] on error
179
+ def ensure_state_dir(daemon_dir)
180
+ if not File.directory? daemon_dir then
181
+ begin
182
+ Dir.mkdir(daemon_dir)
183
+ rescue Exception => ex
184
+ $stderr.puts "Failed to create state dir: #{daemon_dir}; message:\n" + ex.message
185
+ exit 1
186
+ end
187
+ end
188
+ if not File.writable? daemon_dir then
189
+ $stderr.puts "State dir is not writable: #{daemon_dir}"
190
+ exit 1
191
+ end
192
+ end
193
+
194
+ # Copied from daemons gem. We hit a bug where closing FDs failed,
195
+ # probably related to prompting for a password. So close them
196
+ # all cleanly before daemonizing.
197
+ def close_fds
198
+ # don't close stdin/out/err (0/1/2)
199
+ 3.upto(8192).each do |i|
200
+ begin
201
+ IO.for_fd(i).close
202
+ rescue Exception
203
+ end
204
+ end
205
+ end
206
+
207
+ end # App
208
+ end # Bixby
@@ -0,0 +1,112 @@
1
+
2
+ require 'mixlib/cli'
3
+ require 'highline/import'
4
+
5
+ module Bixby
6
+ class App
7
+
8
+ module CLI
9
+
10
+ include Mixlib::CLI
11
+
12
+ def self.included(receiver)
13
+ receiver.extend(Mixlib::CLI::ClassMethods)
14
+ receiver.instance_variable_set(:@options, @options)
15
+ receiver.instance_variable_set(:@banner, @banner)
16
+ receiver.instance_variable_set(:@opt_parser, @opt_parser)
17
+ end
18
+
19
+ banner <<-EOF
20
+ Usage: #{$0} <command>
21
+
22
+ Run bixby-agent as a background daemon.
23
+
24
+ Where <command> is one of:
25
+ start start the agent
26
+ stop stop the agent
27
+ restart stop and start the agent
28
+ zap reset the PID file
29
+ status show status (PID) of the agent
30
+
31
+ To register with the manager:
32
+
33
+ #{$0} --register [URL] -t TENANT -P [PASSWORD] [--tags TAG1,TAG2]
34
+
35
+ Options:
36
+
37
+ EOF
38
+
39
+ # :nocov:
40
+
41
+ option :register,
42
+ :on => :head,
43
+ :long => "--register [URL]",
44
+ :description => "Register with the management server (optional URL, default: https://bixby.io)",
45
+ :proc => Proc.new { |url| url || "https://bixby.io" } # default URL
46
+
47
+ option :tenant,
48
+ :on => :head,
49
+ :short => "-t TENANT",
50
+ :long => "--tenant TENANT",
51
+ :description => "Tenant name"
52
+
53
+ option :password,
54
+ :on => :head,
55
+ :short => "-P [PASSWORD]",
56
+ :long => "--password [PASSWORD]",
57
+ :description => "Agent registration password (prompt if not supplied)",
58
+ :proc => Proc.new { |c|
59
+ if c then
60
+ c
61
+ else
62
+ HighLine.new.ask("Enter agent registration password: ") { |q| q.echo = "*" }
63
+ end
64
+ }
65
+
66
+ option :tags,
67
+ :on => :head,
68
+ :long => "--tags TAGS",
69
+ :description => "Comma separated tags to assign to this host (optional)"
70
+
71
+ option :directory,
72
+ :short => "-d DIRECTORY",
73
+ :long => "--directory DIRECTORY",
74
+ :description => "Root directory for Bixby (optional, default: /opt/bixby)"
75
+
76
+ option :debug,
77
+ :on => :tail,
78
+ :long => "--debug",
79
+ :description => "Enable debug mode (don't daemonize, extra logging)",
80
+ :boolean => true,
81
+ :proc => Proc.new { ENV["BIXBY_LOG"] = "debug" }
82
+
83
+ option :help,
84
+ :on => :tail,
85
+ :short => "-h",
86
+ :long => "--help",
87
+ :description => "Print this help",
88
+ :boolean => true,
89
+ :show_options => true,
90
+ :exit => 0
91
+
92
+ option :version,
93
+ :on => :tail,
94
+ :short => "-v",
95
+ :long => "--version",
96
+ :description => "Show version",
97
+ :proc => Proc.new { puts "Bixby v" + Bixby::Agent::VERSION },
98
+ :exit => 0
99
+
100
+ # :nocov:
101
+
102
+ def initialize
103
+ super
104
+ @argv = parse_options()
105
+ ARGV.clear << @argv
106
+ ARGV.flatten!
107
+ end
108
+
109
+ end # CLI
110
+
111
+ end # App
112
+ end # Bixby