symphony 0.3.0.pre20140327204419
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.simplecov +9 -0
- data/ChangeLog +508 -0
- data/History.rdoc +15 -0
- data/Manifest.txt +30 -0
- data/README.rdoc +89 -0
- data/Rakefile +77 -0
- data/TODO.md +5 -0
- data/USAGE.rdoc +381 -0
- data/bin/symphony +8 -0
- data/bin/symphony-task +10 -0
- data/etc/config.yml.example +9 -0
- data/lib/symphony/daemon.rb +372 -0
- data/lib/symphony/metrics.rb +84 -0
- data/lib/symphony/mixins.rb +75 -0
- data/lib/symphony/queue.rb +313 -0
- data/lib/symphony/routing.rb +98 -0
- data/lib/symphony/signal_handling.rb +107 -0
- data/lib/symphony/task.rb +407 -0
- data/lib/symphony/tasks/auditor.rb +51 -0
- data/lib/symphony/tasks/failure_logger.rb +106 -0
- data/lib/symphony/tasks/pinger.rb +64 -0
- data/lib/symphony/tasks/simulator.rb +57 -0
- data/lib/symphony/tasks/ssh.rb +126 -0
- data/lib/symphony/tasks/sshscript.rb +168 -0
- data/lib/symphony.rb +56 -0
- data/spec/helpers.rb +36 -0
- data/spec/symphony/mixins_spec.rb +78 -0
- data/spec/symphony/queue_spec.rb +368 -0
- data/spec/symphony/task_spec.rb +147 -0
- data/spec/symphony_spec.rb +14 -0
- data.tar.gz.sig +0 -0
- metadata +332 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
require 'symphony/task' unless defined?( Symphony::Task )
|
5
|
+
|
6
|
+
|
7
|
+
### A base SSH class for connecting to remote hosts, running commands,
|
8
|
+
### and collecting output.
|
9
|
+
class Symphony::Task::SSH < Symphony::Task
|
10
|
+
extend MethodUtilities
|
11
|
+
|
12
|
+
### Create a new SSH task for the given +job+ and +queue+.
|
13
|
+
def initialize( queue, job )
|
14
|
+
super
|
15
|
+
|
16
|
+
# The default path to the ssh binary.
|
17
|
+
@path = self.options[:ssh_path] || '/usr/bin/ssh'
|
18
|
+
|
19
|
+
# Default ssh behavior arguments.
|
20
|
+
@ssh_args = self.options[:ssh_args] || [
|
21
|
+
'-e', 'none',
|
22
|
+
'-T',
|
23
|
+
'-x',
|
24
|
+
'-q',
|
25
|
+
'-o', 'CheckHostIP=no',
|
26
|
+
'-o', 'BatchMode=yes',
|
27
|
+
'-o', 'StrictHostKeyChecking=no'
|
28
|
+
]
|
29
|
+
|
30
|
+
# required arguments
|
31
|
+
@hostname = self.options[:hostname] or raise ArgumentError, "no hostname specified"
|
32
|
+
@command = self.options[:command] or raise ArgumentError, "no command specified"
|
33
|
+
|
34
|
+
# optional arguments
|
35
|
+
@port = self.options[:port] || 22
|
36
|
+
@user = self.options[:user] || 'root'
|
37
|
+
@key = self.options[:key]
|
38
|
+
|
39
|
+
@output = nil
|
40
|
+
@return_value = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
# The default path to the ssh binary.
|
44
|
+
attr_reader :path
|
45
|
+
|
46
|
+
# Default ssh behavior arguments.
|
47
|
+
attr_reader :ssh_args
|
48
|
+
|
49
|
+
# The hostname to connect to.
|
50
|
+
attr_reader :hostname
|
51
|
+
|
52
|
+
# The command to run on the remote host.
|
53
|
+
attr_reader :command
|
54
|
+
|
55
|
+
# The key to use for authentication.
|
56
|
+
attr_reader :key
|
57
|
+
|
58
|
+
# The remote ssh port.
|
59
|
+
attr_reader :port
|
60
|
+
|
61
|
+
# Connect to the remote host as this user. Defaults to 'root'.
|
62
|
+
attr_reader :user
|
63
|
+
|
64
|
+
|
65
|
+
### Call ssh and capture output.
|
66
|
+
def run
|
67
|
+
@return_value = self.open_connection do |reader, writer|
|
68
|
+
self.log.debug "Writing command #{self.command}..."
|
69
|
+
writer.puts( self.command )
|
70
|
+
self.log.debug " closing child's writer."
|
71
|
+
writer.close
|
72
|
+
self.log.debug " reading from child."
|
73
|
+
reader.read
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Emit the output from the remote ssh call
|
79
|
+
def on_completion
|
80
|
+
if @return_value
|
81
|
+
self.log.info "Remote exited with %d, output: %s" % [ @return_value.exitstatus, @output ]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
#########
|
87
|
+
protected
|
88
|
+
#########
|
89
|
+
|
90
|
+
### Call ssh and yield the remote IO objects to the caller,
|
91
|
+
### cleaning up afterwards.
|
92
|
+
def open_connection
|
93
|
+
raise LocalJumpError, "no block given" unless block_given?
|
94
|
+
|
95
|
+
fqdn = self.expand_hostname( self.hostname ).
|
96
|
+
find {|hostname| self.ping(hostname, self.port) } or
|
97
|
+
raise "Unable to find an on-network host for %s:%d" % [ self.hostname, self.port ]
|
98
|
+
|
99
|
+
cmd = []
|
100
|
+
cmd << self.path
|
101
|
+
cmd += self.ssh_args
|
102
|
+
cmd << '-p' << self.port.to_s
|
103
|
+
cmd << '-i' << self.key if self.key
|
104
|
+
cmd << '-l' << self.user
|
105
|
+
cmd << fqdn
|
106
|
+
cmd.flatten!
|
107
|
+
self.log.debug "Running SSH command with: %p" % [ Shellwords.shelljoin(cmd) ]
|
108
|
+
|
109
|
+
parent_reader, child_writer = IO.pipe
|
110
|
+
child_reader, parent_writer = IO.pipe
|
111
|
+
|
112
|
+
pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true )
|
113
|
+
child_writer.close
|
114
|
+
child_reader.close
|
115
|
+
|
116
|
+
self.log.debug "Yielding back to the run block."
|
117
|
+
@output = yield( parent_reader, parent_writer )
|
118
|
+
self.log.debug " run block done."
|
119
|
+
|
120
|
+
pid, status = Process.waitpid2( pid )
|
121
|
+
return status
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
end # class Symphony::Task::SSH
|
126
|
+
|
@@ -0,0 +1,168 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/sftp'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'inversion'
|
7
|
+
require 'symphony/task' unless defined?( Symphony::Task )
|
8
|
+
|
9
|
+
|
10
|
+
# A task to execute a script on a remote host via SSH.
|
11
|
+
class Symphony::Task::SSHScript < Symphony::Task
|
12
|
+
extend Loggability,
|
13
|
+
MethodUtilities
|
14
|
+
|
15
|
+
# Loggability API -- Log to symphony's logger
|
16
|
+
log_to :symphony
|
17
|
+
|
18
|
+
|
19
|
+
# Template config
|
20
|
+
TEMPLATE_OPTS = {
|
21
|
+
:ignore_unknown_tags => false,
|
22
|
+
:on_render_error => :propagate,
|
23
|
+
:strip_tag_lines => true
|
24
|
+
}
|
25
|
+
|
26
|
+
# The defaults to use when connecting via SSH
|
27
|
+
DEFAULT_SSH_OPTIONS = {
|
28
|
+
:auth_methods => [ "publickey" ],
|
29
|
+
:compression => true,
|
30
|
+
:config => false,
|
31
|
+
:keys_only => true,
|
32
|
+
# :logger => Loggability[ Net::SSH ],
|
33
|
+
:paranoid => false,
|
34
|
+
:timeout => 10.seconds,
|
35
|
+
# :verbose => :debug,
|
36
|
+
:global_known_hosts_file => '/dev/null',
|
37
|
+
:user_known_hosts_file => '/dev/null',
|
38
|
+
}
|
39
|
+
|
40
|
+
|
41
|
+
### Create a new SSH task for the given +job+ and +queue+.
|
42
|
+
def initialize( queue, job )
|
43
|
+
super
|
44
|
+
|
45
|
+
# required arguments
|
46
|
+
@hostname = self.options[:hostname] or raise ArgumentError, "no hostname specified"
|
47
|
+
@template = self.options[:template] or raise ArgumentError, "no script template specified"
|
48
|
+
@key = self.options[:key] or raise ArgumentError, "no private key specified"
|
49
|
+
|
50
|
+
# optional arguments
|
51
|
+
@port = self.options[:port] || 22
|
52
|
+
@user = self.options[:user] || 'root'
|
53
|
+
@attributes = self.options[:attributes] || {}
|
54
|
+
@nocleanup = self.options[:nocleanup] ? true : false
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
######
|
59
|
+
public
|
60
|
+
######
|
61
|
+
|
62
|
+
# The name of the host to connect to
|
63
|
+
attr_reader :hostname
|
64
|
+
|
65
|
+
# The path to the script template
|
66
|
+
attr_accessor :template
|
67
|
+
|
68
|
+
# The path to the SSH key to use for auth
|
69
|
+
attr_reader :key
|
70
|
+
|
71
|
+
# The SSH port to use
|
72
|
+
attr_reader :port
|
73
|
+
|
74
|
+
# The user to connect as
|
75
|
+
attr_reader :user
|
76
|
+
|
77
|
+
# Attributes that will be set on the script template.
|
78
|
+
attr_reader :attributes
|
79
|
+
|
80
|
+
# Flag that will cause the uploaded script to not be cleaned up after running. Useful for
|
81
|
+
# diagnostics.
|
82
|
+
attr_reader :nocleanup
|
83
|
+
|
84
|
+
|
85
|
+
### Load the script as an Inversion template, sending and executing
|
86
|
+
### it on the remote host.
|
87
|
+
def run
|
88
|
+
fqdn = self.expand_hostname( self.hostname ).
|
89
|
+
find {|hostname| self.ping(hostname, self.port) }
|
90
|
+
|
91
|
+
unless fqdn
|
92
|
+
self.log.debug "Unable to find an on-network host for %s:%d" %
|
93
|
+
[ self.hostname, self.port ]
|
94
|
+
return
|
95
|
+
end
|
96
|
+
|
97
|
+
remote_filename = self.make_remote_filename
|
98
|
+
source = self.generate_script
|
99
|
+
|
100
|
+
# Establish the SSH connection
|
101
|
+
ssh_options = DEFAULT_SSH_OPTIONS.merge( :port => self.port, :keys => [self.key] )
|
102
|
+
self.with_timeout do
|
103
|
+
Net::SSH.start( fqdn, self.user, ssh_options ) do |conn|
|
104
|
+
self.upload_script( conn, source, remote_filename )
|
105
|
+
self.run_script( conn, remote_filename )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
#########
|
112
|
+
protected
|
113
|
+
#########
|
114
|
+
|
115
|
+
### Return a human-readable description of details of the task.
|
116
|
+
def description
|
117
|
+
return "Running script '%s' on '%s:%d' as '%s'" % [
|
118
|
+
File.basename( self.template ),
|
119
|
+
self.hostname,
|
120
|
+
self.port,
|
121
|
+
self.user,
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Generate a unique filename for the script on the remote host.
|
127
|
+
def make_remote_filename
|
128
|
+
template = self.template
|
129
|
+
basename = File.basename( template, File.extname(template) )
|
130
|
+
|
131
|
+
tmpname = Dir::Tmpname.make_tmpname( basename, Process.pid )
|
132
|
+
|
133
|
+
return "/tmp/#{tmpname}"
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
### Generate a script by loading the script template, populating it with
|
138
|
+
### attributes, and rendering it.
|
139
|
+
def generate_script
|
140
|
+
tmpl = Inversion::Template.load( self.template, TEMPLATE_OPTS )
|
141
|
+
|
142
|
+
tmpl.attributes.merge!( self.attributes )
|
143
|
+
tmpl.task = self
|
144
|
+
|
145
|
+
return tmpl.render
|
146
|
+
end
|
147
|
+
|
148
|
+
### Render the given +template+ as script source, then use the specified +conn+ object
|
149
|
+
### to upload it.
|
150
|
+
def upload_script( conn, source, remote_filename )
|
151
|
+
self.log.debug "Uploading script (%d bytes) to %s:%s." %
|
152
|
+
[ source.bytesize, self.hostname, remote_filename ]
|
153
|
+
conn.sftp.file.open( remote_filename, "w", 0755 ) do |fh|
|
154
|
+
fh.print( source )
|
155
|
+
end
|
156
|
+
self.log.debug " done with the upload."
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
### Run the script on the remote host.
|
161
|
+
def run_script( conn, remote_filename )
|
162
|
+
output = conn.exec!( remote_filename )
|
163
|
+
self.log.debug "Output was:\n#{output}"
|
164
|
+
conn.exec!( "rm #{remote_filename}" ) unless self.nocleanup
|
165
|
+
end
|
166
|
+
|
167
|
+
end # class Symphony::Task::SSHScript
|
168
|
+
|
data/lib/symphony.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'loggability'
|
4
|
+
require 'configurability'
|
5
|
+
|
6
|
+
# Symphony -- an evented asynchronous thingie.
|
7
|
+
#
|
8
|
+
# See the README for additional guidance.
|
9
|
+
#
|
10
|
+
module Symphony
|
11
|
+
extend Loggability,
|
12
|
+
Configurability
|
13
|
+
|
14
|
+
# Library version constant
|
15
|
+
VERSION = '0.3.0'
|
16
|
+
|
17
|
+
# Version-control revision constant
|
18
|
+
REVISION = %q$Revision$
|
19
|
+
|
20
|
+
|
21
|
+
# The name of the environment variable to check for config file overrides
|
22
|
+
CONFIG_ENV = 'GROUNDCONTROL_CONFIG'
|
23
|
+
|
24
|
+
# The path to the default config file
|
25
|
+
DEFAULT_CONFIG_FILE = 'etc/config.yml'
|
26
|
+
|
27
|
+
|
28
|
+
# Loggability API -- set up symphony's logger
|
29
|
+
log_as :symphony
|
30
|
+
|
31
|
+
|
32
|
+
### Get the loaded config (a Configurability::Config object)
|
33
|
+
def self::config
|
34
|
+
Configurability.loaded_config
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
### Load the specified +config_file+, install the config in all objects with
|
39
|
+
### Configurability, and call any callbacks registered via #after_configure.
|
40
|
+
def self::load_config( config_file=nil, defaults=nil )
|
41
|
+
config_file ||= ENV[ CONFIG_ENV ] || DEFAULT_CONFIG_FILE
|
42
|
+
defaults ||= Configurability.gather_defaults
|
43
|
+
|
44
|
+
self.log.info "Loading config from %p with defaults for sections: %p." %
|
45
|
+
[ config_file, defaults.keys ]
|
46
|
+
config = Configurability::Config.load( config_file, defaults )
|
47
|
+
config.install
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
require 'symphony/mixins'
|
52
|
+
require 'symphony/queue'
|
53
|
+
require 'symphony/task'
|
54
|
+
|
55
|
+
end # module Symphony
|
56
|
+
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
# SimpleCov test coverage reporting; enable this using the :coverage rake task
|
5
|
+
require 'simplecov' if ENV['COVERAGE']
|
6
|
+
|
7
|
+
require 'loggability'
|
8
|
+
require 'loggability/spechelpers'
|
9
|
+
require 'configurability'
|
10
|
+
require 'configurability/behavior'
|
11
|
+
|
12
|
+
require 'rspec'
|
13
|
+
|
14
|
+
Loggability.format_with( :color ) if $stdout.tty?
|
15
|
+
|
16
|
+
|
17
|
+
### RSpec helper functions.
|
18
|
+
module Loggability::SpecHelpers
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
### Mock with RSpec
|
23
|
+
RSpec.configure do |config|
|
24
|
+
config.run_all_when_everything_filtered = true
|
25
|
+
config.filter_run :focus
|
26
|
+
config.order = 'random'
|
27
|
+
config.expect_with( :rspec )
|
28
|
+
config.mock_with( :rspec ) do |mock|
|
29
|
+
mock.syntax = :expect
|
30
|
+
end
|
31
|
+
|
32
|
+
config.include( Loggability::SpecHelpers )
|
33
|
+
end
|
34
|
+
|
35
|
+
# vim: set nosta noet ts=4 sw=4:
|
36
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env rspec -wfd
|
2
|
+
|
3
|
+
require_relative '../helpers'
|
4
|
+
|
5
|
+
require 'symphony/mixins'
|
6
|
+
|
7
|
+
|
8
|
+
describe Symphony, 'mixins' do
|
9
|
+
|
10
|
+
describe Symphony::MethodUtilities, 'used to extend a class' do
|
11
|
+
|
12
|
+
let!( :extended_class ) do
|
13
|
+
klass = Class.new
|
14
|
+
klass.extend( Symphony::MethodUtilities )
|
15
|
+
klass
|
16
|
+
end
|
17
|
+
|
18
|
+
it "can declare a class-level attribute reader" do
|
19
|
+
extended_class.singleton_attr_reader :foo
|
20
|
+
expect( extended_class ).to respond_to( :foo )
|
21
|
+
expect( extended_class ).to_not respond_to( :foo= )
|
22
|
+
expect( extended_class ).to_not respond_to( :foo? )
|
23
|
+
end
|
24
|
+
|
25
|
+
it "can declare a class-level attribute writer" do
|
26
|
+
extended_class.singleton_attr_writer :foo
|
27
|
+
expect( extended_class ).to_not respond_to( :foo )
|
28
|
+
expect( extended_class ).to respond_to( :foo= )
|
29
|
+
expect( extended_class ).to_not respond_to( :foo? )
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can declare a class-level attribute reader and writer" do
|
33
|
+
extended_class.singleton_attr_accessor :foo
|
34
|
+
expect( extended_class ).to respond_to( :foo )
|
35
|
+
expect( extended_class ).to respond_to( :foo= )
|
36
|
+
expect( extended_class ).to_not respond_to( :foo? )
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can declare a class-level alias" do
|
40
|
+
def extended_class.foo
|
41
|
+
return "foo"
|
42
|
+
end
|
43
|
+
extended_class.singleton_method_alias( :bar, :foo )
|
44
|
+
|
45
|
+
expect( extended_class.bar ).to eq( 'foo' )
|
46
|
+
end
|
47
|
+
|
48
|
+
it "can declare an instance attribute predicate method" do
|
49
|
+
extended_class.attr_predicate :foo
|
50
|
+
instance = extended_class.new
|
51
|
+
|
52
|
+
expect( instance ).to_not respond_to( :foo )
|
53
|
+
expect( instance ).to_not respond_to( :foo= )
|
54
|
+
expect( instance ).to respond_to( :foo? )
|
55
|
+
|
56
|
+
expect( instance.foo? ).to eq( false )
|
57
|
+
|
58
|
+
instance.instance_variable_set( :@foo, 1 )
|
59
|
+
expect( instance.foo? ).to eq( true )
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can declare an instance attribute predicate and writer" do
|
63
|
+
extended_class.attr_predicate_accessor :foo
|
64
|
+
instance = extended_class.new
|
65
|
+
|
66
|
+
expect( instance ).to_not respond_to( :foo )
|
67
|
+
expect( instance ).to respond_to( :foo= )
|
68
|
+
expect( instance ).to respond_to( :foo? )
|
69
|
+
|
70
|
+
expect( instance.foo? ).to eq( false )
|
71
|
+
|
72
|
+
instance.foo = 1
|
73
|
+
expect( instance.foo? ).to eq( true )
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|