br-nanite 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +201 -0
- data/README.rdoc +356 -0
- data/Rakefile +74 -0
- data/TODO +24 -0
- data/bin/nanite-admin +59 -0
- data/bin/nanite-agent +30 -0
- data/bin/nanite-mapper +22 -0
- data/lib/nanite.rb +58 -0
- data/lib/nanite/actor.rb +64 -0
- data/lib/nanite/actor_registry.rb +25 -0
- data/lib/nanite/admin.rb +147 -0
- data/lib/nanite/agent.rb +163 -0
- data/lib/nanite/amqp.rb +47 -0
- data/lib/nanite/cluster.rb +110 -0
- data/lib/nanite/config.rb +80 -0
- data/lib/nanite/console.rb +39 -0
- data/lib/nanite/daemonize.rb +12 -0
- data/lib/nanite/dispatcher.rb +59 -0
- data/lib/nanite/identity.rb +16 -0
- data/lib/nanite/job.rb +50 -0
- data/lib/nanite/log.rb +23 -0
- data/lib/nanite/mapper.rb +214 -0
- data/lib/nanite/packets.rb +192 -0
- data/lib/nanite/reaper.rb +30 -0
- data/lib/nanite/serializer.rb +40 -0
- data/lib/nanite/streaming.rb +125 -0
- data/lib/nanite/util.rb +51 -0
- metadata +104 -0
data/Rakefile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require "spec/rake/spectask"
|
4
|
+
begin; require 'rubygems'; rescue LoadError; end
|
5
|
+
begin
|
6
|
+
require 'hanna/rdoctask'
|
7
|
+
rescue LoadError
|
8
|
+
require 'rake/rdoctask'
|
9
|
+
end
|
10
|
+
require 'rake/clean'
|
11
|
+
|
12
|
+
GEM = "br-nanite"
|
13
|
+
VER = "0.3.0"
|
14
|
+
AUTHOR = "Ezra Zygmuntowicz"
|
15
|
+
EMAIL = "ezra@engineyard.com"
|
16
|
+
HOMEPAGE = "http://github.com/ezmobius/nanite"
|
17
|
+
SUMMARY = "self assembling fabric of ruby daemons"
|
18
|
+
|
19
|
+
Dir.glob('tasks/*.rake').each { |r| Rake.application.add_import r }
|
20
|
+
|
21
|
+
spec = Gem::Specification.new do |s|
|
22
|
+
s.name = GEM
|
23
|
+
s.version = ::VER
|
24
|
+
s.platform = Gem::Platform::RUBY
|
25
|
+
s.has_rdoc = true
|
26
|
+
s.extra_rdoc_files = ["README.rdoc", "LICENSE", 'TODO']
|
27
|
+
s.summary = SUMMARY
|
28
|
+
s.description = s.summary
|
29
|
+
s.author = AUTHOR
|
30
|
+
s.email = EMAIL
|
31
|
+
s.homepage = HOMEPAGE
|
32
|
+
|
33
|
+
s.bindir = "bin"
|
34
|
+
s.executables = %w( nanite-agent nanite-mapper nanite-admin )
|
35
|
+
|
36
|
+
s.add_dependency "extlib"
|
37
|
+
s.add_dependency('amqp', '>= 0.6.0')
|
38
|
+
|
39
|
+
s.require_path = 'lib'
|
40
|
+
s.files = %w(LICENSE README.rdoc Rakefile TODO) + Dir.glob("{lib,bin,specs}/**/*")
|
41
|
+
end
|
42
|
+
|
43
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
44
|
+
pkg.gem_spec = spec
|
45
|
+
end
|
46
|
+
|
47
|
+
task :install => [:package] do
|
48
|
+
sh %{sudo gem install pkg/#{GEM}-#{VER}}
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Run unit specs"
|
52
|
+
Spec::Rake::SpecTask.new do |t|
|
53
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
54
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Generate RDoc documentation'
|
58
|
+
Rake::RDocTask.new do |rd|
|
59
|
+
rd.title = spec.name
|
60
|
+
rd.rdoc_dir = 'rdoc'
|
61
|
+
rd.main = "README.rdoc"
|
62
|
+
rd.rdoc_files.include("lib/**/*.rb", *spec.extra_rdoc_files)
|
63
|
+
end
|
64
|
+
CLOBBER.include(:clobber_rdoc)
|
65
|
+
|
66
|
+
desc 'Generate and open documentation'
|
67
|
+
task :docs => :rdoc do
|
68
|
+
case RUBY_PLATFORM
|
69
|
+
when /darwin/ ; sh 'open rdoc/index.html'
|
70
|
+
when /mswin|mingw/ ; sh 'start rdoc\index.html'
|
71
|
+
else
|
72
|
+
sh 'firefox rdoc/index.html'
|
73
|
+
end
|
74
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
TODO:
|
2
|
+
|
3
|
+
- The examples/crew.rb file is pointing towards a hard coded user dir. Needs to be
|
4
|
+
documented as part of a working example.
|
5
|
+
- examples/async_rack_front/async_rack_front.ru needs to be documented and verified working.
|
6
|
+
|
7
|
+
Ian:
|
8
|
+
- Sync Mapper/Agent#start with nanite-mapper/agent
|
9
|
+
- Update docs for Agent#start and Mapper#start
|
10
|
+
- Update docs in nanite-agent and nanite-mapper
|
11
|
+
- Ensure file transfer works
|
12
|
+
- Check secure stuff still works
|
13
|
+
- Check custom status_proc works
|
14
|
+
- Check documentation, only document public methods
|
15
|
+
- ensure the removal of threaded_actors option doesn't cause shit to block
|
16
|
+
|
17
|
+
- Look into using EM deferables for actors dispatch.
|
18
|
+
- Integration specs that spawn a small cluster of nanites
|
19
|
+
- Rename Ping to Status
|
20
|
+
- request/push should take *args for payload?
|
21
|
+
|
22
|
+
Maybe:
|
23
|
+
- Make mapper queue durable and Results respect :persistent flag on the request
|
24
|
+
- Add a global result received callback
|
data/bin/nanite-admin
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# To work without being installed as a gem:
|
4
|
+
libdir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
5
|
+
$:.unshift libdir unless $:.include? libdir
|
6
|
+
|
7
|
+
require 'nanite'
|
8
|
+
require 'nanite/admin'
|
9
|
+
require 'eventmachine'
|
10
|
+
require 'thin'
|
11
|
+
# IMPORTANT!
|
12
|
+
# You need raggi's patched async version of Thin at the moment to use
|
13
|
+
# the nanite-admin tool.
|
14
|
+
#
|
15
|
+
# raggi's Git repo contains a branch called 'async_for_rack' which contains the
|
16
|
+
# version of Thin you want to install. raggi has apparently removed the 'master'
|
17
|
+
# branch of his Git repo so you may see a warning like that shown below.
|
18
|
+
#
|
19
|
+
# git clone git://github.com/raggi/thin.git thin-raggi-async
|
20
|
+
# ...
|
21
|
+
# warning: remote HEAD refers to nonexistent ref, unable to checkout. <<== IGNORE THIS
|
22
|
+
#
|
23
|
+
# cd thin-raggi-async/
|
24
|
+
# git checkout --track -b async_for_rack origin/async_for_rack
|
25
|
+
# warning: You appear to be on a branch yet to be born. <<== IGNORE THIS
|
26
|
+
# warning: Forcing checkout of origin/async_for_rack. <<== IGNORE THIS
|
27
|
+
# Branch async_for_rack set up to track remote branch refs/remotes/origin/async_for_rack.
|
28
|
+
# Switched to a new branch "async_for_rack"
|
29
|
+
|
30
|
+
# run : 'rake install' to build and install the Thin gem
|
31
|
+
# cd <NANITE>
|
32
|
+
# ./bin/nanite-admin
|
33
|
+
|
34
|
+
# When you need to update this Thin install you should be able to do a 'git pull' on the
|
35
|
+
# "async_for_rack" branch.
|
36
|
+
|
37
|
+
require File.dirname(__FILE__) + '/../lib/nanite'
|
38
|
+
require 'yaml'
|
39
|
+
require "optparse"
|
40
|
+
|
41
|
+
include Nanite::CommonConfig
|
42
|
+
|
43
|
+
options = {}
|
44
|
+
|
45
|
+
opts = OptionParser.new do |opts|
|
46
|
+
opts.banner = "Usage: nanite-admin [-flags] [argument]"
|
47
|
+
opts.define_head "Nanite Admin: a basic control interface for your nanite cluster."
|
48
|
+
opts.separator '*'*80
|
49
|
+
|
50
|
+
setup_mapper_options(opts, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.parse!
|
54
|
+
|
55
|
+
EM.run do
|
56
|
+
Nanite.start_mapper(options)
|
57
|
+
puts "starting nanite-admin"
|
58
|
+
Rack::Handler::Thin.run(Nanite::Admin.new(Nanite.mapper), :Port => 4000)
|
59
|
+
end
|
data/bin/nanite-agent
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../lib/nanite'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
include Nanite::CommonConfig
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
opts = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: nanite-agent [-flag] [argument]"
|
12
|
+
opts.define_head "Nanite Agent: ruby process that acts upon messages passed to it by a mapper."
|
13
|
+
opts.separator '*'*80
|
14
|
+
|
15
|
+
setup_common_options(opts, options, 'agent')
|
16
|
+
|
17
|
+
opts.on("-n", "--nanite NANITE_ROOT", "Specify the root of your nanite agent project.") do |nanite|
|
18
|
+
options[:root] = nanite
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("--ping-time PINGTIME", "Specify how often the agents contacts the mapper") do |ping|
|
22
|
+
options[:ping_time] = ping
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.parse!
|
27
|
+
|
28
|
+
EM.run do
|
29
|
+
Nanite.start_agent(options)
|
30
|
+
end
|
data/bin/nanite-mapper
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../lib/nanite'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
include Nanite::CommonConfig
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
opts = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: nanite-mapper [-flags] [argument]"
|
12
|
+
opts.define_head "Nanite Mapper: clustered head unit for self assembling cluster of ruby processes."
|
13
|
+
opts.separator '*'*80
|
14
|
+
|
15
|
+
setup_mapper_options(opts, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.parse!
|
19
|
+
|
20
|
+
EM.run do
|
21
|
+
Nanite.start_mapper(options)
|
22
|
+
end
|
data/lib/nanite.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'amqp'
|
3
|
+
require 'mq'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
$:.unshift File.dirname(__FILE__)
|
9
|
+
require 'nanite/amqp'
|
10
|
+
require 'nanite/util'
|
11
|
+
require 'nanite/config'
|
12
|
+
require 'nanite/packets'
|
13
|
+
require 'nanite/identity'
|
14
|
+
require 'nanite/console'
|
15
|
+
require 'nanite/daemonize'
|
16
|
+
require 'nanite/job'
|
17
|
+
require 'nanite/mapper'
|
18
|
+
require 'nanite/actor'
|
19
|
+
require 'nanite/actor_registry'
|
20
|
+
require 'nanite/streaming'
|
21
|
+
require 'nanite/dispatcher'
|
22
|
+
require 'nanite/agent'
|
23
|
+
require 'nanite/cluster'
|
24
|
+
require 'nanite/reaper'
|
25
|
+
require 'nanite/serializer'
|
26
|
+
require 'nanite/log'
|
27
|
+
|
28
|
+
module Nanite
|
29
|
+
VERSION = '0.3.0' unless defined?(Nanite::VERSION)
|
30
|
+
|
31
|
+
class MapperNotRunning < StandardError; end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
attr_reader :mapper, :agent
|
35
|
+
|
36
|
+
def start_agent(options = {})
|
37
|
+
@agent = Nanite::Agent.start(options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def start_mapper(options = {})
|
41
|
+
@mapper = Nanite::Mapper.start(options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def request(*args, &blk)
|
45
|
+
ensure_mapper
|
46
|
+
@mapper.request(*args, &blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
def push(*args)
|
50
|
+
ensure_mapper
|
51
|
+
@mapper.push(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def ensure_mapper
|
55
|
+
raise MapperNotRunning.new('A mapper needs to be started via Nanite.start_mapper') unless @mapper
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/nanite/actor.rb
ADDED
@@ -0,0 +1,64 @@
|
|
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
|
+
if Nanite::Actor == self
|
44
|
+
raise 'Method name callbacks cannot be used on the Nanite::Actor superclass' if Symbol === proc || String === proc
|
45
|
+
@superclass_exception_callback = proc || blk
|
46
|
+
else
|
47
|
+
@instance_exception_callback = proc || blk
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def superclass_exception_callback
|
52
|
+
@superclass_exception_callback
|
53
|
+
end
|
54
|
+
|
55
|
+
def instance_exception_callback
|
56
|
+
@instance_exception_callback
|
57
|
+
end
|
58
|
+
end # ClassMethods
|
59
|
+
|
60
|
+
module InstanceMethods
|
61
|
+
end # InstanceMethods
|
62
|
+
|
63
|
+
end # Actor
|
64
|
+
end # Nanite
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Nanite
|
2
|
+
class ActorRegistry
|
3
|
+
attr_reader :actors, :log
|
4
|
+
|
5
|
+
def initialize(log)
|
6
|
+
@log = log
|
7
|
+
@actors = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(actor, prefix)
|
11
|
+
raise ArgumentError, "#{actor.inspect} is not a Nanite::Actor subclass instance" unless Nanite::Actor === actor
|
12
|
+
log.info("Registering #{actor.inspect} with prefix #{prefix.inspect}")
|
13
|
+
prefix ||= actor.class.default_prefix
|
14
|
+
actors[prefix.to_s] = actor
|
15
|
+
end
|
16
|
+
|
17
|
+
def services
|
18
|
+
actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
|
19
|
+
end
|
20
|
+
|
21
|
+
def actor_for(prefix)
|
22
|
+
actor = actors[prefix]
|
23
|
+
end
|
24
|
+
end # ActorRegistry
|
25
|
+
end # Nanite
|
data/lib/nanite/admin.rb
ADDED
@@ -0,0 +1,147 @@
|
|
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|
|
29
|
+
env['async.callback'].call [200, {'Content-Type' => 'text/html'}, [layout(ul(response))]]
|
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.map{|n,s| s[:services] }.flatten.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)
|
47
|
+
buf = "<ul>"
|
48
|
+
hash.each do |k,v|
|
49
|
+
buf << "<li><div class=\"nanite\">#{k}:</div><div class=\"response\">#{v.inspect}</div></li>"
|
50
|
+
end
|
51
|
+
buf << "</ul>"
|
52
|
+
buf
|
53
|
+
end
|
54
|
+
|
55
|
+
def layout(content=nil)
|
56
|
+
%Q{
|
57
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
58
|
+
<html xmlns='http://www.w3.org/1999/xhtml'>
|
59
|
+
<head>
|
60
|
+
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
|
61
|
+
<meta content='en' http-equiv='Content-Language' />
|
62
|
+
<meta content='Engineyard' name='author' />
|
63
|
+
<title>Nanite Control Tower</title>
|
64
|
+
|
65
|
+
<!-- Google AJAX Libraries API -->
|
66
|
+
<script src="http://www.google.com/jsapi"></script>
|
67
|
+
<script type="text/javascript">
|
68
|
+
google.load("jquery", "1");
|
69
|
+
</script>
|
70
|
+
|
71
|
+
<script type="text/javascript">
|
72
|
+
$(document).ready(function(){
|
73
|
+
|
74
|
+
// set the focus to the payload field
|
75
|
+
$("#payload").focus();
|
76
|
+
|
77
|
+
});
|
78
|
+
</script>
|
79
|
+
|
80
|
+
<style>
|
81
|
+
body {margin: 0; font-family: verdana; background-color: #fcfcfc;}
|
82
|
+
ul {margin: 0; padding: 0; margin-left: 10px}
|
83
|
+
li {list-style-type: none; margin-bottom: 6px}
|
84
|
+
li .nanite {font-weight: bold; font-size: 12px}
|
85
|
+
li .response {padding: 8px}
|
86
|
+
h1, h2, h3 {margin-top: none; padding: none; margin-left: 40px;}
|
87
|
+
h1 {font-size: 22px; margin-top: 40px; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 6px;
|
88
|
+
margin-right: 40px}
|
89
|
+
h2 {font-size: 16px;}
|
90
|
+
h3 {margin-left: 0; font-size: 14px}
|
91
|
+
.section {border: 1px solid #ccc; background-color: #fefefe; padding: 10px; margin: 20px 40px; padding: 20px;
|
92
|
+
font-size: 14px}
|
93
|
+
#footer {text-align: center; color: #AAA; font-size: 12px}
|
94
|
+
</style>
|
95
|
+
|
96
|
+
</head>
|
97
|
+
|
98
|
+
<body>
|
99
|
+
<div id="header">
|
100
|
+
<h1>Nanite Control Tower</h1>
|
101
|
+
</div>
|
102
|
+
|
103
|
+
<h2>#{@mapper.options[:vhost]}</h2>
|
104
|
+
<div class="section">
|
105
|
+
<form method="post" action="/">
|
106
|
+
<input type="hidden" value="POST" name="_method"/>
|
107
|
+
|
108
|
+
<label>Send</label>
|
109
|
+
<select name="type">
|
110
|
+
<option #{@selection == 'least_loaded' ? 'selected="true"' : ''} value="least_loaded">the least loaded nanite</option>
|
111
|
+
<option #{@selection == 'random' ? 'selected="true"' : ''} value="random">a random nanite</option>
|
112
|
+
<option #{@selection == 'all' ? 'selected="true"' : ''} value="all">all nanites</option>
|
113
|
+
<option #{@selection == 'rr' ? 'selected="true"' : ''} value="rr">a nanite chosen by round robin</option>
|
114
|
+
#{@mapper.cluster.nanites.map {|k,v| "<option #{@selection == k ? 'selected="true"' : ''} value='#{k}'>#{k}</option>" }.join}
|
115
|
+
</select>
|
116
|
+
|
117
|
+
<label>providing service</label>
|
118
|
+
#{services}
|
119
|
+
|
120
|
+
<label>the payload</label>
|
121
|
+
<input type="text" class="text" name="payload" id="payload"/>
|
122
|
+
|
123
|
+
<input type="submit" class="submit" value="Go!" name="submit"/>
|
124
|
+
</form>
|
125
|
+
|
126
|
+
#{"<h3>Responses</h3>" if content}
|
127
|
+
#{content}
|
128
|
+
</div>
|
129
|
+
|
130
|
+
<h2>Running nanites</h2>
|
131
|
+
<div class="section">
|
132
|
+
#{"No nanites online." if @mapper.cluster.nanites.size == 0}
|
133
|
+
<ul>
|
134
|
+
#{@mapper.cluster.nanites.map {|k,v| "<li>identity : #{k}<br />load : #{v[:status]}<br />services : #{v[:services].inspect}</li>" }.join}
|
135
|
+
</ul>
|
136
|
+
</div>
|
137
|
+
<div id="footer">
|
138
|
+
Nanite #{Nanite::VERSION}
|
139
|
+
<br />
|
140
|
+
© 2009 a bunch of random geeks
|
141
|
+
</div>
|
142
|
+
</body>
|
143
|
+
</html>
|
144
|
+
}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|