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,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