rightscale-nanite 0.4.1 → 0.4.1.1

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 (56) hide show
  1. data/lib/nanite.rb +71 -0
  2. data/lib/nanite/actor.rb +60 -0
  3. data/lib/nanite/actor_registry.rb +24 -0
  4. data/lib/nanite/admin.rb +153 -0
  5. data/lib/nanite/agent.rb +250 -0
  6. data/lib/nanite/amqp.rb +47 -0
  7. data/lib/nanite/cluster.rb +203 -0
  8. data/lib/nanite/config.rb +102 -0
  9. data/lib/nanite/console.rb +39 -0
  10. data/lib/nanite/daemonize.rb +13 -0
  11. data/lib/nanite/dispatcher.rb +90 -0
  12. data/lib/nanite/identity.rb +16 -0
  13. data/lib/nanite/job.rb +104 -0
  14. data/lib/nanite/local_state.rb +34 -0
  15. data/lib/nanite/log.rb +64 -0
  16. data/lib/nanite/log/formatter.rb +39 -0
  17. data/lib/nanite/mapper.rb +277 -0
  18. data/lib/nanite/mapper_proxy.rb +56 -0
  19. data/lib/nanite/packets.rb +231 -0
  20. data/lib/nanite/pid_file.rb +52 -0
  21. data/lib/nanite/reaper.rb +38 -0
  22. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  23. data/lib/nanite/security/certificate.rb +55 -0
  24. data/lib/nanite/security/certificate_cache.rb +66 -0
  25. data/lib/nanite/security/distinguished_name.rb +34 -0
  26. data/lib/nanite/security/encrypted_document.rb +46 -0
  27. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  28. data/lib/nanite/security/secure_serializer.rb +67 -0
  29. data/lib/nanite/security/signature.rb +40 -0
  30. data/lib/nanite/security/static_certificate_store.rb +35 -0
  31. data/lib/nanite/security_provider.rb +47 -0
  32. data/lib/nanite/serializer.rb +52 -0
  33. data/lib/nanite/state.rb +164 -0
  34. data/lib/nanite/streaming.rb +125 -0
  35. data/lib/nanite/util.rb +51 -0
  36. data/spec/actor_registry_spec.rb +62 -0
  37. data/spec/actor_spec.rb +59 -0
  38. data/spec/agent_spec.rb +235 -0
  39. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  40. data/spec/certificate_cache_spec.rb +49 -0
  41. data/spec/certificate_spec.rb +27 -0
  42. data/spec/cluster_spec.rb +300 -0
  43. data/spec/dispatcher_spec.rb +136 -0
  44. data/spec/distinguished_name_spec.rb +24 -0
  45. data/spec/encrypted_document_spec.rb +21 -0
  46. data/spec/job_spec.rb +219 -0
  47. data/spec/local_state_spec.rb +112 -0
  48. data/spec/packet_spec.rb +218 -0
  49. data/spec/rsa_key_pair_spec.rb +33 -0
  50. data/spec/secure_serializer_spec.rb +41 -0
  51. data/spec/serializer_spec.rb +107 -0
  52. data/spec/signature_spec.rb +30 -0
  53. data/spec/spec_helper.rb +23 -0
  54. data/spec/static_certificate_store_spec.rb +30 -0
  55. data/spec/util_spec.rb +63 -0
  56. metadata +62 -1
@@ -0,0 +1,71 @@
1
+ require 'rubygems'
2
+ require 'amqp'
3
+ require 'mq'
4
+ require 'json'
5
+ require 'logger'
6
+ require 'yaml'
7
+ require 'openssl'
8
+
9
+ $:.unshift File.dirname(__FILE__)
10
+ require 'nanite/amqp'
11
+ require 'nanite/util'
12
+ require 'nanite/config'
13
+ require 'nanite/packets'
14
+ require 'nanite/identity'
15
+ require 'nanite/console'
16
+ require 'nanite/daemonize'
17
+ require 'nanite/pid_file'
18
+ require 'nanite/job'
19
+ require 'nanite/mapper'
20
+ require 'nanite/actor'
21
+ require 'nanite/actor_registry'
22
+ require 'nanite/streaming'
23
+ require 'nanite/dispatcher'
24
+ require 'nanite/agent'
25
+ require 'nanite/cluster'
26
+ require 'nanite/reaper'
27
+ require 'nanite/log'
28
+ require 'nanite/mapper_proxy'
29
+ require 'nanite/security_provider'
30
+ require 'nanite/security/cached_certificate_store_proxy'
31
+ require 'nanite/security/certificate'
32
+ require 'nanite/security/certificate_cache'
33
+ require 'nanite/security/distinguished_name'
34
+ require 'nanite/security/encrypted_document'
35
+ require 'nanite/security/rsa_key_pair'
36
+ require 'nanite/security/secure_serializer'
37
+ require 'nanite/security/signature'
38
+ require 'nanite/security/static_certificate_store'
39
+ require 'nanite/serializer'
40
+
41
+ module Nanite
42
+ VERSION = '0.4.0' unless defined?(Nanite::VERSION)
43
+
44
+ class MapperNotRunning < StandardError; end
45
+
46
+ class << self
47
+ attr_reader :mapper, :agent
48
+
49
+ def start_agent(options = {})
50
+ @agent = Nanite::Agent.start(options)
51
+ end
52
+
53
+ def start_mapper(options = {})
54
+ @mapper = Nanite::Mapper.start(options)
55
+ end
56
+
57
+ def request(*args, &blk)
58
+ ensure_mapper
59
+ @mapper.request(*args, &blk)
60
+ end
61
+
62
+ def push(*args)
63
+ ensure_mapper
64
+ @mapper.push(*args)
65
+ end
66
+
67
+ def ensure_mapper
68
+ raise MapperNotRunning.new('A mapper needs to be started via Nanite.start_mapper') unless @mapper
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,60 @@
1
+ module Nanite
2
+ # This mixin provides Nanite actor functionality.
3
+ #
4
+ # To use it simply include it your class containing the functionality to be exposed:
5
+ #
6
+ # class Foo
7
+ # include Nanite::Actor
8
+ # expose :bar
9
+ #
10
+ # def bar(payload)
11
+ # # ...
12
+ # end
13
+ #
14
+ # end
15
+ module Actor
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ include Nanite::Actor::InstanceMethods
20
+ extend Nanite::Actor::ClassMethods
21
+ end # base.class_eval
22
+ end # self.included
23
+
24
+ module ClassMethods
25
+ def default_prefix
26
+ to_s.to_const_path
27
+ end
28
+
29
+ def expose(*meths)
30
+ @exposed ||= []
31
+ meths.each do |meth|
32
+ @exposed << meth unless @exposed.include?(meth)
33
+ end
34
+ end
35
+
36
+ def provides_for(prefix)
37
+ return [] unless @exposed
38
+ @exposed.map {|meth| "/#{prefix}/#{meth}".squeeze('/')}
39
+ end
40
+
41
+ def on_exception(proc = nil, &blk)
42
+ raise 'No callback provided for on_exception' unless proc || blk
43
+ @exception_callback = proc || blk
44
+ end
45
+
46
+ def exception_callback
47
+ @exception_callback
48
+ end
49
+
50
+ end # ClassMethods
51
+
52
+ module InstanceMethods
53
+ # send nanite request to another agent (through the mapper)
54
+ def request(*args, &blk)
55
+ MapperProxy.instance.request(*args, &blk)
56
+ end
57
+ end # InstanceMethods
58
+
59
+ end # Actor
60
+ end # Nanite
@@ -0,0 +1,24 @@
1
+ module Nanite
2
+ class ActorRegistry
3
+ attr_reader :actors
4
+
5
+ def initialize
6
+ @actors = {}
7
+ end
8
+
9
+ def register(actor, prefix)
10
+ raise ArgumentError, "#{actor.inspect} is not a Nanite::Actor subclass instance" unless Nanite::Actor === actor
11
+ Nanite::Log.info("Registering #{actor.inspect} with prefix #{prefix.inspect}")
12
+ prefix ||= actor.class.default_prefix
13
+ actors[prefix.to_s] = actor
14
+ end
15
+
16
+ def services
17
+ actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
18
+ end
19
+
20
+ def actor_for(prefix)
21
+ actor = actors[prefix]
22
+ end
23
+ end # ActorRegistry
24
+ end # Nanite
@@ -0,0 +1,153 @@
1
+ require 'rack'
2
+
3
+ module Nanite
4
+ # This is a Rack app for nanite-admin. You need to have an async capable
5
+ # version of Thin installed for this to work. See bin/nanite-admin for install
6
+ # instructions.
7
+ class Admin
8
+ def initialize(mapper)
9
+ @mapper = mapper
10
+ end
11
+
12
+ AsyncResponse = [-1, {}, []].freeze
13
+
14
+ def call(env)
15
+ req = Rack::Request.new(env)
16
+ if cmd = req.params['command']
17
+ @command = cmd
18
+ @selection = req.params['type'] if req.params['type']
19
+
20
+ options = {}
21
+ case @selection
22
+ when 'least_loaded', 'random', 'all', 'rr'
23
+ options[:selector] = @selection
24
+ else
25
+ options[:target] = @selection
26
+ end
27
+
28
+ @mapper.request(cmd, req.params['payload'], options) do |response, responsejob|
29
+ env['async.callback'].call [200, {'Content-Type' => 'text/html'}, [layout(ul(response, responsejob))]]
30
+ end
31
+ AsyncResponse
32
+ else
33
+ [200, {'Content-Type' => 'text/html'}, layout]
34
+ end
35
+ end
36
+
37
+ def services
38
+ buf = "<select name='command'>"
39
+ @mapper.cluster.nanites.all_services.each do |srv|
40
+ buf << "<option value='#{srv}' #{@command == srv ? 'selected="true"' : ''}>#{srv}</option>"
41
+ end
42
+ buf << "</select>"
43
+ buf
44
+ end
45
+
46
+ def ul(hash, job)
47
+ buf = "<ul>"
48
+ hash.each do |k,v|
49
+ buf << "<li><div class=\"nanite\">#{k}:</div><div class=\"response\">#{v.inspect}</div>"
50
+ if job.intermediate_state && job.intermediate_state[k]
51
+ buf << "<div class=\"intermediatestates\"><span class=\"statenote\">intermediate state:</span> #{job.intermediate_state[k].inspect}</div>"
52
+ end
53
+ buf << "</li>"
54
+ end
55
+ buf << "</ul>"
56
+ buf
57
+ end
58
+
59
+ def layout(content=nil)
60
+ %Q{
61
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
62
+ <html xmlns='http://www.w3.org/1999/xhtml'>
63
+ <head>
64
+ <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
65
+ <meta content='en' http-equiv='Content-Language' />
66
+ <meta content='Engineyard' name='author' />
67
+ <title>Nanite Control Tower</title>
68
+
69
+ <!-- Google AJAX Libraries API -->
70
+ <script src="http://www.google.com/jsapi"></script>
71
+ <script type="text/javascript">
72
+ google.load("jquery", "1");
73
+ </script>
74
+
75
+ <script type="text/javascript">
76
+ $(document).ready(function(){
77
+
78
+ // set the focus to the payload field
79
+ $("#payload").focus();
80
+
81
+ });
82
+ </script>
83
+
84
+ <style>
85
+ body {margin: 0; font-family: verdana; background-color: #fcfcfc;}
86
+ ul {margin: 0; padding: 0; margin-left: 10px}
87
+ li {list-style-type: none; margin-bottom: 6px}
88
+ li .nanite {font-weight: bold; font-size: 12px}
89
+ li .response {padding: 8px}
90
+ li .intermediatestates {padding: 8px; font-size: 10px;}
91
+ li .intermediatestates span.statenote {font-style: italic;}
92
+ h1, h2, h3 {margin-top: none; padding: none; margin-left: 40px;}
93
+ h1 {font-size: 22px; margin-top: 40px; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 6px;
94
+ margin-right: 40px}
95
+ h2 {font-size: 16px;}
96
+ h3 {margin-left: 0; font-size: 14px}
97
+ .section {border: 1px solid #ccc; background-color: #fefefe; padding: 10px; margin: 20px 40px; padding: 20px;
98
+ font-size: 14px}
99
+ #footer {text-align: center; color: #AAA; font-size: 12px}
100
+ </style>
101
+
102
+ </head>
103
+
104
+ <body>
105
+ <div id="header">
106
+ <h1>Nanite Control Tower</h1>
107
+ </div>
108
+
109
+ <h2>#{@mapper.options[:vhost]}</h2>
110
+ <div class="section">
111
+ <form method="post" action="/">
112
+ <input type="hidden" value="POST" name="_method"/>
113
+
114
+ <label>Send</label>
115
+ <select name="type">
116
+ <option #{@selection == 'least_loaded' ? 'selected="true"' : ''} value="least_loaded">the least loaded nanite</option>
117
+ <option #{@selection == 'random' ? 'selected="true"' : ''} value="random">a random nanite</option>
118
+ <option #{@selection == 'all' ? 'selected="true"' : ''} value="all">all nanites</option>
119
+ <option #{@selection == 'rr' ? 'selected="true"' : ''} value="rr">a nanite chosen by round robin</option>
120
+ #{@mapper.cluster.nanites.map {|k,v| "<option #{@selection == k ? 'selected="true"' : ''} value='#{k}'>#{k}</option>" }.join}
121
+ </select>
122
+
123
+ <label>providing service</label>
124
+ #{services}
125
+
126
+ <label>the payload</label>
127
+ <input type="text" class="text" name="payload" id="payload"/>
128
+
129
+ <input type="submit" class="submit" value="Go!" name="submit"/>
130
+ </form>
131
+
132
+ #{"<h3>Responses</h3>" if content}
133
+ #{content}
134
+ </div>
135
+
136
+ <h2>Running nanites</h2>
137
+ <div class="section">
138
+ #{"No nanites online." if @mapper.cluster.nanites.size == 0}
139
+ <ul>
140
+ #{@mapper.cluster.nanites.map {|k,v| "<li>identity : #{k}<br />load : #{v[:status]}<br />services : #{v[:services].to_a.inspect}</li>" }.join}
141
+ </ul>
142
+ </div>
143
+ <div id="footer">
144
+ Nanite #{Nanite::VERSION}
145
+ <br />
146
+ &copy; 2009 a bunch of random geeks
147
+ </div>
148
+ </body>
149
+ </html>
150
+ }
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,250 @@
1
+ module Nanite
2
+ class Agent
3
+ include AMQPHelper
4
+ include FileStreaming
5
+ include ConsoleHelper
6
+ include DaemonizeHelper
7
+
8
+ attr_reader :identity, :options, :serializer, :dispatcher, :registry, :amq, :tags
9
+ attr_accessor :status_proc
10
+
11
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({:user => 'nanite', :ping_time => 15,
12
+ :default_services => []}) unless defined?(DEFAULT_OPTIONS)
13
+
14
+ # Initializes a new agent and establishes AMQP connection.
15
+ # This must be used inside EM.run block or if EventMachine reactor
16
+ # is already started, for instance, by a Thin server that your Merb/Rails
17
+ # application runs on.
18
+ #
19
+ # Agent options:
20
+ #
21
+ # identity : identity of this agent, may be any string
22
+ #
23
+ # status_proc : a callable object that returns agent load as a string,
24
+ # defaults to load averages string extracted from `uptime`
25
+ # format : format to use for packets serialization. One of the three:
26
+ # :marshall, :json, or :yaml. Defaults to
27
+ # Ruby's Marshall format. For interoperability with
28
+ # AMQP clients implemented in other languages, use JSON.
29
+ #
30
+ # Note that Nanite uses JSON gem,
31
+ # and ActiveSupport's JSON encoder may cause clashes
32
+ # if ActiveSupport is loaded after JSON gem.
33
+ #
34
+ # root : application root for this agent, defaults to Dir.pwd
35
+ #
36
+ # log_dir : path to directory where agent stores it's log file
37
+ # if not given, app_root is used.
38
+ #
39
+ # file_root : path to directory to files this agent provides
40
+ # defaults to app_root/files
41
+ #
42
+ # ping_time : time interval in seconds between two subsequent heartbeat messages
43
+ # this agent broadcasts. Default value is 15.
44
+ #
45
+ # console : true tells Nanite to start interactive console
46
+ #
47
+ # daemonize : true tells Nanite to daemonize
48
+ #
49
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
50
+ # defaults to the root or the current working directory.
51
+ #
52
+ # services : list of services provided by this agent, by default
53
+ # all methods exposed by actors are listed
54
+ #
55
+ # single_threaded: Run all operations in one thread
56
+ #
57
+ # Connection options:
58
+ #
59
+ # vhost : AMQP broker vhost that should be used
60
+ #
61
+ # user : AMQP broker user
62
+ #
63
+ # pass : AMQP broker password
64
+ #
65
+ # host : host AMQP broker (or node of interest) runs on,
66
+ # defaults to 0.0.0.0
67
+ #
68
+ # port : port AMQP broker (or node of interest) runs on,
69
+ # this defaults to 5672, port used by some widely
70
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
71
+ #
72
+ # On start Nanite reads config.yml, so it is common to specify
73
+ # options in the YAML file. However, when both Ruby code options
74
+ # and YAML file specify option, Ruby code options take precedence.
75
+ def self.start(options = {})
76
+ agent = new(options)
77
+ agent.run
78
+ agent
79
+ end
80
+
81
+ def initialize(opts)
82
+ set_configuration(opts)
83
+ @tags = []
84
+ @tags << opts[:tag]
85
+ @tags.flatten!
86
+ @options.freeze
87
+ end
88
+
89
+ def run
90
+ log_path = false
91
+ if @options[:daemonize]
92
+ log_path = (@options[:log_dir] || @options[:root] || Dir.pwd)
93
+ end
94
+ Log.init(@identity, log_path)
95
+ Log.level = @options[:log_level] if @options[:log_level]
96
+ @serializer = Serializer.new(@options[:format])
97
+ @status_proc = lambda { parse_uptime(`uptime`) rescue 'no status' }
98
+ pid_file = PidFile.new(@identity, @options)
99
+ pid_file.check
100
+ if @options[:daemonize]
101
+ daemonize
102
+ pid_file.write
103
+ at_exit { pid_file.remove }
104
+ end
105
+ @amq = start_amqp(@options)
106
+ @registry = ActorRegistry.new
107
+ @dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @options)
108
+ setup_mapper_proxy
109
+ load_actors
110
+ setup_traps
111
+ setup_queue
112
+ advertise_services
113
+ setup_heartbeat
114
+ at_exit { un_register } unless $TESTING
115
+ start_console if @options[:console] && !@options[:daemonize]
116
+ end
117
+
118
+ def register(actor, prefix = nil)
119
+ registry.register(actor, prefix)
120
+ end
121
+
122
+ # Can be used in agent's initialization file to register a security module
123
+ # This security module 'authorize' method will be called back whenever the
124
+ # agent receives a request and will be given the corresponding deliverable.
125
+ # It should return 'true' for the request to proceed.
126
+ # Requests will return 'deny_token' or the string "Denied" by default when
127
+ # 'authorize' does not return 'true'.
128
+ def register_security(security, deny_token = "Denied")
129
+ @security = security
130
+ @deny_token = deny_token
131
+ end
132
+
133
+ protected
134
+
135
+ def set_configuration(opts)
136
+ @options = DEFAULT_OPTIONS.clone
137
+ root = opts[:root] || @options[:root]
138
+ custom_config = if root
139
+ file = File.expand_path(File.join(root, 'config.yml'))
140
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
141
+ else
142
+ {}
143
+ end
144
+ opts.delete(:identity) unless opts[:identity]
145
+ @options.update(custom_config.merge(opts))
146
+ @options[:file_root] ||= File.join(@options[:root], 'files')
147
+ return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
148
+ token = Identity.generate
149
+ @identity = "nanite-#{token}"
150
+ File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
151
+ fd.write(YAML.dump(custom_config.merge(:identity => token)))
152
+ end
153
+ end
154
+
155
+ def load_actors
156
+ return unless options[:root]
157
+ actors_dir = @options[:actors_dir] || "#{@options[:root]}/actors"
158
+ actors = @options[:actors]
159
+ Dir["#{actors_dir}/*.rb"].each do |actor|
160
+ next if actors && !actors.include?(File.basename(actor, ".rb"))
161
+ Nanite::Log.info("loading actor: #{actor}")
162
+ require actor
163
+ end
164
+ init_path = @options[:initrb] || File.join(options[:root], 'init.rb')
165
+ instance_eval(File.read(init_path), init_path) if File.exist?(init_path)
166
+ end
167
+
168
+ def receive(packet)
169
+ case packet
170
+ when Advertise
171
+ Nanite::Log.debug("handling Advertise: #{packet.inspect}")
172
+ advertise_services
173
+ when Request, Push
174
+ Nanite::Log.debug("handling Request: #{packet.inspect}")
175
+ if @security && !@security.authorize(packet)
176
+ if packet.kind_of?(Request)
177
+ r = Result.new(packet.token, packet.reply_to, @deny_token, identity)
178
+ amq.queue(packet.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
179
+ end
180
+ else
181
+ dispatcher.dispatch(packet)
182
+ end
183
+ when Result
184
+ Nanite::Log.debug("handling Result: #{packet.inspect}")
185
+ @mapper_proxy.handle_result(packet)
186
+ when IntermediateMessage
187
+ Nanite::Log.debug("handling Intermediate Result: #{packet.inspect}")
188
+ @mapper_proxy.handle_intermediate_result(packet)
189
+ end
190
+ end
191
+
192
+ def tag(*tags)
193
+ tags.each {|t| @tags << t}
194
+ @tags.uniq!
195
+ end
196
+
197
+ def setup_queue
198
+ amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
199
+ begin
200
+ info.ack
201
+ packet = serializer.load(msg)
202
+ receive(packet)
203
+ rescue Exception => e
204
+ Nanite::Log.error("Error handling packet: #{e.message}")
205
+ end
206
+ end
207
+ end
208
+
209
+ def setup_heartbeat
210
+ EM.add_periodic_timer(options[:ping_time]) do
211
+ amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
212
+ end
213
+ end
214
+
215
+ def setup_mapper_proxy
216
+ @mapper_proxy = MapperProxy.new(identity, options)
217
+ end
218
+
219
+ def setup_traps
220
+ ['INT', 'TERM'].each do |sig|
221
+ old = trap(sig) do
222
+ un_register
223
+ amq.instance_variable_get('@connection').close do
224
+ EM.stop
225
+ old.call if old.is_a? Proc
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ def un_register
232
+ unless @unregistered
233
+ @unregistered = true
234
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(UnRegister.new(identity)))
235
+ end
236
+ end
237
+
238
+ def advertise_services
239
+ Nanite::Log.debug("advertise_services: #{registry.services.inspect}")
240
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(Register.new(identity, registry.services, status_proc.call, self.tags)))
241
+ end
242
+
243
+ def parse_uptime(up)
244
+ if up =~ /load averages?: (.*)/
245
+ a,b,c = $1.split(/\s+|,\s+/)
246
+ (a.to_f + b.to_f + c.to_f) / 3
247
+ end
248
+ end
249
+ end
250
+ end