emissary 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'digest/md5'
17
+ require 'base64'
18
+ require 'etc'
19
+
20
+ module Emissary
21
+ class Agent::Sshkeys < Agent
22
+ AUTH_KEY_FILE = '.ssh/authorized_keys'
23
+
24
+ attr_reader :user
25
+
26
+ def valid_methods
27
+ [ :add, :delete, :update ]
28
+ end
29
+
30
+ def post_init
31
+ begin
32
+ @user = Etc.getpwnam(args.shift)
33
+ rescue ArgumentError => e
34
+ if e.message =~ /can't find user/
35
+ raise "User '#{args.first}' does not exist on this system"
36
+ else
37
+ raise "Unhandled error attempting to retrieve data on user [#{args.first}]: #{e.message}"
38
+ end
39
+ end
40
+ end
41
+
42
+ def add pubkey
43
+ raise "Missing 'key_uri' argument - can't download key!" if pubkey.nil?
44
+
45
+ pubkey_name = Digest::MD5.hexdigest(pubkey.split(/\s+/).join(' ').chomp)
46
+
47
+ begin
48
+ keys = get_keys(user)
49
+ if not keys.has_key?(pubkey_name)
50
+ keys[pubkey_name] = pubkey
51
+ write_keys(user, keys)
52
+ result = "Successfully added key [#{pubkey_name}] to user [#{user.name}]"
53
+ else
54
+ result = "Could not add key [#{pubkey_name}] to user [#{user.name}] - key already exists!"
55
+ end
56
+ rescue Exception => e
57
+ raise Exception, 'Possibly unable to add user key - error was: ' + e.message, caller
58
+ end
59
+
60
+ return result
61
+ end
62
+
63
+ def delete pubkey
64
+ return 'No authorized_keys file - nothing changed' if not File.exists?(File.join(user.dir, AUTH_KEY_FILE))
65
+
66
+ keyname = Digest::MD5.hexdigest(pubkey)
67
+ begin
68
+ keys = get_keys(user)
69
+ if keys.has_key?(keyname)
70
+ keys.delete(keyname)
71
+ write_keys(user, keys)
72
+ result = "Successfully removed key [#{keyname}] from user [#{user.name}]"
73
+ else
74
+ result = "Could not remove key [#{keyname}] from user [#{user.name}] - key not there!"
75
+ end
76
+ rescue Exception => e
77
+ raise "Possibly unable to remove key #{keyname} for user #{user.name} - error was: #{e.message}"
78
+ end
79
+
80
+ return result
81
+ end
82
+
83
+ def update oldkey, newkey
84
+ return 'Not implemented'
85
+ end
86
+
87
+ private
88
+
89
+ def write_keys(user, keys)
90
+ auth_file_do(user, 'w') do |fp|
91
+ fp << keys.values.join("\n")
92
+ end
93
+ end
94
+
95
+ def get_keys(user)
96
+ ::Emissary.logger.debug "retreiving ssh keys from file #{File.join(user.dir, AUTH_KEY_FILE)}"
97
+
98
+ keys = {}
99
+ auth_file_do(user, 'r') do |fp|
100
+ fp.readlines.each do |line|
101
+ keys[Digest::MD5.hexdigest(line.chomp)] = line.chomp
102
+ end
103
+ end
104
+
105
+ return keys
106
+ end
107
+
108
+ def auth_file_setup(user)
109
+ user_auth_file = File.join(user.dir, AUTH_KEY_FILE)
110
+ user_ssh_dir = File.dirname(user_auth_file)
111
+
112
+ begin
113
+ if not File.directory?(user_ssh_dir)
114
+ Dir.mkdir(user_ssh_dir)
115
+ File.open(user_ssh_dir) do |f|
116
+ f.chown(user.uid, user.gid)
117
+ f.chmod(0700)
118
+ end
119
+ end
120
+ if not File.exists?(user_auth_file)
121
+ File.open(user_auth_file, 'a') do |f|
122
+ f.chown(user.uid, user.gid)
123
+ f.chmod(0600)
124
+ end
125
+ end
126
+ rescue Exception => e
127
+ raise "Error creating #{user_auth_file} -- #{e.message}"
128
+ end
129
+
130
+ return user_auth_file
131
+ end
132
+
133
+ def auth_file_do(user, mode = 'r', &block)
134
+ begin
135
+ auth_file = auth_file_setup(user)
136
+ File.open(auth_file, mode) do |f|
137
+ f.flock File::LOCK_EX
138
+ yield(f)
139
+ f.flock File::LOCK_UN
140
+ end
141
+ rescue Exception => e
142
+ case mode
143
+ when 'r': mode = 'reading from'
144
+ when 'a': mode = 'appending to'
145
+ when 'w': mode = 'writing to'
146
+ else mode = "unknown operation #{mode} for File.open() on "
147
+ end
148
+ raise "Error #{mode} file '#{auth_file}' -- #{e.message}"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,96 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::Stats < Agent
18
+ STATISTIC_TYPES = [ :cpu, :network, :disk ]
19
+
20
+ begin
21
+ require 'sys/cpu'
22
+ rescue LoadError
23
+ STATISTIC_TYPES.delete(:cpu)
24
+ ::Emissary.logger.warning "Ruby Gem 'sys-cpu' doesn't appear to be present - removing statistic gather for cpu."
25
+ end
26
+
27
+ begin
28
+ require 'ifconfig'
29
+ rescue LoadError
30
+ STATISTIC_TYPES.delete(:network)
31
+ ::Emissary.logger.warning "Ruby Gem 'ifconfig' doesn't appear to be present - removing statistic gather for network."
32
+ end
33
+
34
+ def valid_methods
35
+ [ :gather ]
36
+ end
37
+
38
+ def gather
39
+ message.recipient = "#{config[:stats][:queue_base]}:#{message.exchange_type.to_s}"
40
+ message.args = STATISTIC_TYPES.inject([]) do |args, type|
41
+ unless (data = self.__send__(type)).nil?
42
+ args << { type => data }
43
+ end
44
+ args
45
+ end
46
+
47
+ throw :skip_implicit_response unless not message.args.empty?
48
+ return message
49
+ end
50
+
51
+ def disk
52
+ @cmd = "/usr/bin/env df -B K -P -T -x devfs -x tmpfs | /usr/bin/env tail -n +2"
53
+
54
+ data = IO.popen(@cmd){ |f| f.readlines }.collect { |l| l.split(/\s+/) }
55
+ data.inject([]) { |data,line|
56
+ device = Hash[[:device, :type, :size, :used, :avail, :percent, :mount].zip(line.collect!{|v| v =~ /^\d+/ ? v[/^(\d+)/].to_i : v })]
57
+
58
+ ::Emissary.logger.notice("[statistics] Disk#%s: type:%s mount:%s size:%d used:%d in-use:%d%%",
59
+ device[:device], device[:type], device[:mount], device[:size], device[:used], device[:percent]
60
+ )
61
+
62
+ data << device
63
+ }
64
+ end
65
+
66
+ def cpu
67
+ load_average = Sys::CPU.load_avg
68
+ ::Emissary.logger.notice "[statistics] CPU: #{load_average.join ', '}"
69
+ load_average
70
+ end
71
+
72
+ def network
73
+ interfaces = (ifconfig = IfconfigWrapper.new.parse).interfaces.inject([]) do |interfaces, name|
74
+ interfaces << (interface = {
75
+ :name => name,
76
+ :tx => ifconfig[name].tx.symbolize,
77
+ :rx => ifconfig[name].rx.symbolize,
78
+ :up => ifconfig[name].status,
79
+ :ips => ifconfig[name].addresses('inet').collect { |ip| ip.to_s }
80
+ })
81
+
82
+ ::Emissary.logger.notice("[statistics] Network#%s: state:%s tx:%d rx:%d inet:%s",
83
+ name,
84
+ (interface[:up] ? 'up' : 'down'),
85
+ interface[:tx][:bytes],
86
+ interface[:rx][:bytes],
87
+ interface[:ips].join(',')
88
+ ) unless interface.try(:[], :tx).nil?
89
+
90
+ interfaces
91
+ end
92
+
93
+ return interfaces
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,40 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::Test < Agent
18
+ def valid_methods
19
+ [:test_raise]
20
+ end
21
+
22
+ def test_raise klass, *args
23
+ ::Emissary.logger.debug "TEST AGENT: #test(#{klass}, #{args.inspect})"
24
+
25
+ exception = nil
26
+ begin
27
+ e_klass = ::Emissary.klass_const(klass)
28
+ unless not e_klass.try(:new).try(:is_a?, Exception)
29
+ raise e_klass, *args
30
+ else
31
+ raise Exception, "#{e_klass.name.to_s rescue e_klass.to_s} is not a valid exception name!"
32
+ end
33
+ rescue Exception => e
34
+ exception = e
35
+ end
36
+
37
+ message.error exception
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,231 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'inifile'
17
+
18
+ #
19
+ # This class represents the INI file and can be used to parse INI files.
20
+ # Derived from IniFile gem, found on http://rubyforge.org/projects/inifile/
21
+ #
22
+ module Emissary
23
+ class ConfigParseError < ::Emissary::Error
24
+ def initialize(message)
25
+ super(Exception, message)
26
+ end
27
+ end
28
+
29
+ class ConfigValidationError < ::Emissary::Error
30
+ def initialize(message)
31
+ super(Exception, message)
32
+ end
33
+ end
34
+
35
+ class ConfigFile < IniFile
36
+
37
+ attr_reader :ini
38
+ def initialize( filename, opts = {} )
39
+ @line_number = 0
40
+ @fn = filename
41
+ @comment = opts[:comment] || '#'
42
+ @param = opts[:parameter] || '='
43
+ @debug = !!opts[:debug]
44
+ @ini = Hash.new {|h,k| h[k] = Hash.new}
45
+
46
+ @rgxp_comment = /^\s*$|^\s*[#{@comment}]/
47
+ @rgxp_section = /^\s*\[([^\]]+)\]/
48
+ @rgxp_param = /^([^#{@param}]+)#{@param}(.*)$/
49
+
50
+ @rgxp_dict_start = /^([^#{@param}]+)#{@param}\s*\{\s*$/
51
+ @rgxp_dict_stop = /^\s*\}\s*$/
52
+ @dict_stack = []
53
+
54
+ @rgxp_list_start = /^([^#{@param}]+)#{@param}\s*\[\s*$/
55
+ @rgxp_list_line = /^([^#{@param}]+)#{@param}\s*\[\s*([^\]]+)\]\s*$/
56
+ @rgxp_list_stop = /^\s*\]\s*$/
57
+ @list_items = []
58
+ @in_list_name = nil
59
+
60
+ super filename, opts
61
+
62
+ yield self if block_given?
63
+ end
64
+
65
+ #
66
+ # call-seq:
67
+ # ini_file[section]
68
+ #
69
+ # Get the hash of parameter/value pairs for the given _section_.
70
+ #
71
+ def []( section )
72
+ return nil if section.nil?
73
+ @ini[section.to_sym]
74
+ end
75
+
76
+ #
77
+ # call-seq:
78
+ # has_section?( section )
79
+ #
80
+ # Returns +true+ if the named _section_ exists in the INI file.
81
+ #
82
+ def has_section?( section )
83
+ @ini.has_key? section.to_sym
84
+ end
85
+
86
+ #
87
+ # call-seq:
88
+ # parse
89
+ #
90
+ # Loops over each line of the file, passing it off to the parse_line method
91
+ #
92
+ def parse
93
+ return unless ::Kernel.test ?f, @fn
94
+ @section_name = nil
95
+ ::File.open(@fn, 'r') do |f|
96
+ while line = f.gets
97
+ @line_number += 1
98
+ parse_line line.chomp
99
+ end
100
+ end
101
+ @section_name = nil
102
+ @line_number = 0
103
+ return
104
+ end
105
+
106
+ #
107
+ # call-seq:
108
+ # set_vall( key, value) => value
109
+ #
110
+ # Sets the value of the given key taking the current stack level into account
111
+ #
112
+ def set_value key, value
113
+ begin
114
+ p = @ini[@section_name]
115
+ @dict_stack.map { |d| p = (p[d]||={}) }
116
+ p[key] = value
117
+ rescue NoMethodError
118
+ raise ConfigParseError, "sectionless parameter declaration encountered at line #{@line_number}"
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ #
125
+ # call-seq:
126
+ # current_state (param = nil) => state
127
+ #
128
+ # Used for outputing the current parameter hash heirarchy in debug mode
129
+ #
130
+ def current_state param = nil
131
+ state = "@ini[:#{@section_name}]"
132
+ state << @dict_stack.collect { |c| "[:#{c}]" }.join unless @dict_stack.empty?
133
+ state << "[:#{@in_list_name}]" unless @in_list_name.nil?
134
+ state << "[:#{param}]" unless param.nil?
135
+ state
136
+ end
137
+
138
+ #
139
+ # call-seq:
140
+ # parse_line(line)
141
+ #
142
+ # Parses the given line
143
+ #
144
+ def parse_line line
145
+ line.gsub!(/\s+#.*$/, '') # strip comments
146
+
147
+ # replace __FILE__ with the file being parsed
148
+ line.gsub!('__FILE__', File.expand_path(@fn))
149
+
150
+ # replace __DIR__ with the path of the file being parsed
151
+ line.gsub!('__DIR__', File.dirname(File.expand_path(@fn)))
152
+
153
+ # replace __ID_<METHOD>__ with Emissary.identity.<method>
154
+ [ :name, :instance_id, :server_id, :cluster_id, :account_id ].each do |id_method|
155
+ line.gsub!("__ID_#{id_method.to_s.upcase}__", Emissary.identity.__send__(id_method).to_s)
156
+ end
157
+
158
+ if not @in_list_name.nil? and line !~ @rgxp_list_stop
159
+ line = line.strip.split(/\s*,\s*/).compact.reject(&:blank?)
160
+ Emissary.logger.debug " ---> LIST ITEM #{current_state} << #{line.inspect}" if @debug
161
+ # then we're in the middle of a list item, so add to it
162
+ @list_items = @list_items | line
163
+ return
164
+ end
165
+
166
+ case line
167
+ # ignore blank lines and comment lines
168
+ when @rgxp_comment: return
169
+
170
+ # this is a section declaration
171
+ when @rgxp_section
172
+ Emissary.logger.debug "SECTION: #{line}" if @debug
173
+
174
+ unless @in_dict_name.nil?
175
+ raise ConfigParseError, "dictionary '#{@in_dict_name}' crosses section '#{$1.strip.downcase}' boundary at line #{@line_number}"
176
+ end
177
+
178
+ @section_name = $1.strip.downcase.to_sym
179
+ @ini[@section_name] ||= {}
180
+
181
+ when @rgxp_dict_start
182
+ @dict_stack << $1.strip.downcase.to_sym
183
+ Emissary.logger.debug " ---> DICT_BEG: #{@dict_stack.last}" if @debug
184
+
185
+ when @rgxp_dict_stop
186
+ raise ConfigParseError, "end of dictionary found without beginning at line #{@line_number}" if @dict_stack.empty?
187
+ Emissary.logger.debug " ---> DICT_END: #{@dict_stack.last}" if @debug
188
+ @dict_stack.pop
189
+ return
190
+
191
+ when @rgxp_list_line
192
+ list_name = $1.strip.downcase.to_sym
193
+ list_items = $2.strip.split(/\s*,\s*/).compact.reject(&:blank?)
194
+
195
+ unless not @debug
196
+ Emissary.logger.debug " ---> LIST_BEG: #{list_name}"
197
+ list_items.each do |li|
198
+ Emissary.logger.debug " ---> LIST_ITEM: #{current_state list_name} << [\"#{li}\"]"
199
+ end
200
+ Emissary.logger.debug " ---> LIST_END: #{list_name}"
201
+ end
202
+
203
+ set_value list_name, list_items
204
+
205
+ when @rgxp_list_start
206
+ Emissary.logger.debug " ---> LIST_BEG: #{line}" if @debug
207
+ @in_list_name = $1.strip.downcase.to_sym
208
+
209
+ when @rgxp_list_stop
210
+ Emissary.logger.debug " ---> LIST_END: #{@in_list_name} - #{@list_items.inspect}" if @debug
211
+ raise ConfigParseError, "end of list found without beginning at line #{@line_number}" if @in_list_name.nil?
212
+ set_value @in_list_name, @list_items
213
+
214
+ @in_list_name = nil
215
+ @list_items = []
216
+
217
+ when @rgxp_param
218
+ val = $2.strip
219
+ val = val[1..-2] if val[0..0] == "'" || val[-1..-1] == '"'
220
+
221
+ key = $1.strip.downcase.to_sym
222
+ Emissary.logger.debug " ---> PARAM: #{current_state key} = #{val}" if @debug
223
+ set_value key, val
224
+
225
+ else
226
+ raise Exception, "Unable to parse line #{@line_number}: #{line}"
227
+ end
228
+ return true
229
+ end
230
+ end
231
+ end