emissary 1.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.
@@ -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