symphony-ssh 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ecf234436b7966c1062b9d13dbfe4c71015c29fe
4
+ data.tar.gz: 89636ab1155f42de42d0bc9f74e45f6b51299e18
5
+ SHA512:
6
+ metadata.gz: a6b1681a726bbb2a554f2429c5dd11f3695e149c1341991088ad4deac99f0edd7728fcaca02480362cce98218dbe7b1e13dbfc1bcc01ef468df8199f3d9746df
7
+ data.tar.gz: bcc024b3faed110b348714ee2925d13544448da998f5a9d145ff1d017ed69c8b244047fe18019c6234aa8143c05aa3db1d5803e9430da9724cbd3808bf6d1df6
@@ -0,0 +1,113 @@
1
+
2
+ = symphony-ssh
3
+
4
+ == Description
5
+
6
+ This is a small collection of base classes used for interacting with
7
+ remote machines over ssh. With them, you can use AMQP (via Symphony) to
8
+ run batch commands, execute templates as scripts, and perform any
9
+ batch/remoting stuff you can think of without the need of a separate
10
+ client agent.
11
+
12
+ These classes assume you have a user that can connect and login to
13
+ remote machines using a password-less ssh keypair. They are not meant
14
+ to be used directly. Subclass them!
15
+
16
+ See the rdoc for additional information and examples.
17
+
18
+
19
+ == Options
20
+
21
+ Symphony-ssh uses
22
+ Configurability[https://rubygems.org/gems/configurability] to determine
23
+ behavior. The configuration is a YAML[http://www.yaml.org/] file.
24
+
25
+ symphony_ssh:
26
+ path: /usr/bin/ssh
27
+ user: root
28
+ key: /path/to/a/private_key.rsa
29
+ opts:
30
+ - -e
31
+ - none
32
+ - -T
33
+ - -x
34
+ - -o
35
+ - CheckHostIP=no'
36
+ - -o
37
+ - BatchMode=yes'
38
+ - -o
39
+ - StrictHostKeyChecking=no
40
+
41
+
42
+
43
+ === path
44
+
45
+ The absolute path to the ssh binary.
46
+
47
+ === user
48
+
49
+ The default user to connect to remote hosts with. This can be
50
+ changes per connection in the AMQP payload.
51
+
52
+ === key
53
+
54
+ An absolute path to a password-less ssh private key.
55
+
56
+ === opts
57
+
58
+ SSH client options, passed to the ssh binary on the command line. Note
59
+ that the defaults have been tested fairly extensively, these are just
60
+ exposed if you have very specific needs and you know what you're doing.
61
+
62
+
63
+ == Installation
64
+
65
+ gem install symphony-ssh
66
+
67
+
68
+ == Contributing
69
+
70
+ You can check out the current development source with Mercurial via its
71
+ {project page}[http://bitbucket.org/mahlon/symphony-ssh].
72
+
73
+ After checking out the source, run:
74
+
75
+ $ rake
76
+
77
+ This task will run the tests/specs and generate the API documentation.
78
+
79
+ If you use {rvm}[http://rvm.io/], entering the project directory will
80
+ install any required development dependencies.
81
+
82
+
83
+ == License
84
+
85
+ Copyright (c) 2014, Mahlon E. Smith and Michael Granger
86
+ All rights reserved.
87
+
88
+ Redistribution and use in source and binary forms, with or without
89
+ modification, are permitted provided that the following conditions are met:
90
+
91
+ * Redistributions of source code must retain the above copyright notice,
92
+ this list of conditions and the following disclaimer.
93
+
94
+ * Redistributions in binary form must reproduce the above copyright notice,
95
+ this list of conditions and the following disclaimer in the documentation
96
+ and/or other materials provided with the distribution.
97
+
98
+ * Neither the name of the author/s, nor the names of the project's
99
+ contributors may be used to endorse or promote products derived from this
100
+ software without specific prior written permission.
101
+
102
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
103
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
104
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
105
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
106
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
107
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
108
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
109
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
110
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
111
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
112
+
113
+
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+
4
+ require 'shellwords'
5
+ require 'symphony/task' unless defined?( Symphony::Task )
6
+
7
+
8
+ ### A base class for connecting to remote hosts, running arbitrary
9
+ ### commands, and collecting output.
10
+ ###
11
+ ### This isn't designed to be used directly. To use this in your
12
+ ### environment, you'll want to subclass it, add the behaviors
13
+ ### that make sense for you, then super() back to the parent in the
14
+ ### #work method.
15
+ ###
16
+ ### It expects the payload to contain the following keys:
17
+ ###
18
+ ### host: (required) The hostname to connect to
19
+ ### command: (required) The command to run on the remote host
20
+ ### port: (optional) The port to connect to (defaults to 22)
21
+ ### opts: (optional) Explicit SSH client options
22
+ ### user: (optional) The user to connect as (defaults to root)
23
+ ### key: (optional) The path to an SSH private key
24
+ ###
25
+ ###
26
+ ### Additionally, this class responds to the 'symphony_ssh' configurability
27
+ ### key. Currently, you can set the 'path' argument, which is the
28
+ ### full path to the local ssh binary (defaults to '/usr/bin/ssh') and
29
+ ### override the default ssh user, key, and client opts.
30
+ ###
31
+ ### Textual output of the command is stored in the @output instance variable.
32
+ ###
33
+ ###
34
+ ### require 'symphony'
35
+ ### require 'symphony/tasks/ssh'
36
+ ###
37
+ ### class YourTask < Symphony::Task::SSH
38
+ ### timeout 5
39
+ ### subscribe_to 'ssh.command'
40
+ ###
41
+ ### def work( payload, metadata )
42
+ ### status = super
43
+ ### puts "Remote host said: %s" % [ @output ]
44
+ ### return status.success?
45
+ ### end
46
+ ### end
47
+ ###
48
+ class Symphony::Task::SSH < Symphony::Task
49
+ extend Configurability
50
+ config_key :symphony_ssh
51
+
52
+ # SSH default options.
53
+ #
54
+ CONFIG_DEFAULTS = {
55
+ :path => '/usr/bin/ssh',
56
+ :opts => [
57
+ '-e', 'none',
58
+ '-T',
59
+ '-x',
60
+ '-q',
61
+ '-o', 'CheckHostIP=no',
62
+ '-o', 'BatchMode=yes',
63
+ '-o', 'StrictHostKeyChecking=no'
64
+ ],
65
+ :user => 'root',
66
+ :key => nil
67
+ }
68
+
69
+ # SSH "informative" stdout output that should be cleaned from the
70
+ # command output.
71
+ SSH_CLEANUP = %r/Warning: no access to tty|Thus no job control in this shell/
72
+
73
+ class << self
74
+ # The full path to the ssh binary.
75
+ attr_reader :path
76
+
77
+ # A default set of ssh client options when connecting
78
+ # to remote hosts.
79
+ attr_reader :opts
80
+
81
+ # The default user to use when connecting. If unset, 'root' is used.
82
+ attr_reader :user
83
+
84
+ # An absolute path to a password-free ssh private key.
85
+ attr_reader :key
86
+ end
87
+
88
+ ### Configurability API.
89
+ ###
90
+ def self::configure( config=nil )
91
+ config = self.defaults.merge( config || {} )
92
+ @path = config.delete( :path )
93
+ @opts = config.delete( :opts )
94
+ @user = config.delete( :user )
95
+ @key = config.delete( :key )
96
+ super
97
+ end
98
+
99
+
100
+ ### Perform the ssh connection, passing the command to the pipe
101
+ ### and retreiving any output from the remote end.
102
+ ###
103
+ def work( payload, metadata )
104
+ command = payload[ 'command' ]
105
+ raise ArgumentError, "Missing required option 'command'" unless command
106
+ raise ArgumentError, "Missing required option 'host'" unless payload[ 'host' ]
107
+
108
+ exitcode = self.open_connection( payload, metadata ) do |reader, writer|
109
+ self.log.debug "Writing command #{command}..."
110
+ writer.puts( command )
111
+ self.log.debug " closing child's writer."
112
+ writer.close
113
+ self.log.debug " reading from child."
114
+ reader.read
115
+ end
116
+
117
+ self.log.debug "SSH exited: %d" % [ exitcode ]
118
+ return exitcode
119
+ end
120
+
121
+
122
+ #########
123
+ protected
124
+ #########
125
+
126
+ ### Call ssh and yield the remote IO objects to the caller,
127
+ ### cleaning up afterwards.
128
+ ###
129
+ def open_connection( payload, metadata=nil )
130
+ raise LocalJumpError, "no block given" unless block_given?
131
+ @output = ''
132
+
133
+ port = payload[ 'port' ] || 22
134
+ opts = payload[ 'opts' ] || Symphony::Task::SSH.opts
135
+ user = payload[ 'user' ] || Symphony::Task::SSH.user
136
+ key = payload[ 'key' ] || Symphony::Task::SSH.key
137
+
138
+ cmd = []
139
+ cmd << Symphony::Task::SSH.path
140
+ cmd += Symphony::Task::SSH.opts
141
+
142
+ cmd << '-p' << port.to_s
143
+ cmd << '-i' << key if key
144
+ cmd << '-l' << user
145
+ cmd << payload[ 'host' ]
146
+ cmd.flatten!
147
+ self.log.debug "Running SSH command with: %p" % [ Shellwords.shelljoin(cmd) ]
148
+
149
+ parent_reader, child_writer = IO.pipe
150
+ child_reader, parent_writer = IO.pipe
151
+
152
+ pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true )
153
+ child_writer.close
154
+ child_reader.close
155
+
156
+ self.log.debug "Yielding back to the run block."
157
+ @output = yield( parent_reader, parent_writer )
158
+ @output = @output.split("\n").reject{|l| l =~ SSH_CLEANUP }.join
159
+ self.log.debug " run block done."
160
+
161
+ pid, status = Process.waitpid2( pid )
162
+ return status
163
+ end
164
+
165
+ end # Symphony::Task::SSH
166
+
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+
4
+ require 'net/ssh'
5
+ require 'net/sftp'
6
+ require 'tmpdir'
7
+ require 'inversion'
8
+ require 'symphony'
9
+ require 'symphony/task'
10
+
11
+
12
+ ### A base class for connecting to a remote host, then uploading and
13
+ ### executing an Inversion templated script.
14
+ ###
15
+ ### This isn't designed to be used directly. To use this in your
16
+ ### environment, you'll want to subclass it, add the behaviors
17
+ ### that make sense for you, then super() back to the parent in the
18
+ ### #work method.
19
+ ###
20
+ ### It expects the payload to contain the following keys:
21
+ ###
22
+ ### host: (required) The hostname to connect to
23
+ ### template: (required) A path to the Inversion templated script
24
+ ### port: (optional) The port to connect to (defaults to 22)
25
+ ### user: (optional) The user to connect as (defaults to root)
26
+ ### key: (optional) The path to an SSH private key
27
+ ### attributes: (optional) Additional data to attach to the template
28
+ ### nocleanup: (optional) Leave the remote script after execution? (default to false)
29
+ ###
30
+ ###
31
+ ### Additionally, this class responds to the 'symphony_ssh' configurability
32
+ ### key. Currently, you can override the default ssh user and private key.
33
+ ###
34
+ ### Textual output of the command is stored in the @output instance variable.
35
+ ###
36
+ ###
37
+ ### require 'symphony'
38
+ ### require 'symphony/tasks/sshscript'
39
+ ###
40
+ ### class YourTask < Symphony::Task::SSHScript
41
+ ### timeout 30
42
+ ### subscribe_to 'ssh.script.*'
43
+ ###
44
+ ### def work( payload, metadata )
45
+ ### status = super
46
+ ### puts "Remote script said: %s" % [ @output ]
47
+ ### return status.success?
48
+ ### end
49
+ ### end
50
+ ###
51
+ class Symphony::Task::SSHScript < Symphony::Task
52
+ extend Configurability
53
+ config_key :symphony_ssh
54
+
55
+ # Template config
56
+ #
57
+ TEMPLATE_OPTS = {
58
+ :ignore_unknown_tags => false,
59
+ :on_render_error => :propagate,
60
+ :strip_tag_lines => true
61
+ }
62
+
63
+ # The defaults to use when connecting via SSH
64
+ #
65
+ DEFAULT_SSH_OPTIONS = {
66
+ :auth_methods => [ 'publickey' ],
67
+ :compression => true,
68
+ :config => false,
69
+ :keys_only => true,
70
+ :paranoid => false,
71
+ :global_known_hosts_file => '/dev/null',
72
+ :user_known_hosts_file => '/dev/null'
73
+ }
74
+
75
+ # SSH default options.
76
+ #
77
+ CONFIG_DEFAULTS = {
78
+ :user => 'root',
79
+ :key => nil
80
+ }
81
+
82
+ class << self
83
+ # The default user to use when connecting. If unset, 'root' is used.
84
+ attr_reader :user
85
+
86
+ # An absolute path to a password-free ssh private key.
87
+ attr_reader :key
88
+ end
89
+
90
+ ### Configurability API.
91
+ ###
92
+ def self::configure( config=nil )
93
+ config = self.defaults.merge( config || {} )
94
+ @user = config.delete( :user )
95
+ @key = config.delete( :key )
96
+ super
97
+ end
98
+
99
+
100
+ ### Perform the ssh connection, render the template, send it, and
101
+ ### execute it.
102
+ ###
103
+ def work( payload, metadata )
104
+ template = payload[ 'template' ]
105
+ attributes = payload[ 'attributes' ] || {}
106
+ port = payload[ 'port' ] || 22
107
+ user = payload[ 'user' ] || Symphony::Task::SSHScript.user
108
+ key = payload[ 'key' ] || Symphony::Task::SSHScript.key
109
+ nocleanup = payload[ 'nocleanup' ]
110
+
111
+ raise ArgumentError, "Missing required option 'command'" unless template
112
+ raise ArgumentError, "Missing required option 'host'" unless payload[ 'host' ]
113
+
114
+ remote_filename = self.make_remote_filename( template )
115
+ source = self.generate_script( template, attributes )
116
+
117
+ ssh_options = DEFAULT_SSH_OPTIONS.merge( :port => port, :keys => [key] )
118
+ ssh_options.merge!(
119
+ :logger => Loggability[ Net::SSH ],
120
+ :verbose => :debug
121
+ ) if payload[ 'debug' ]
122
+
123
+ Net::SSH.start( payload['host'], user, ssh_options ) do |conn|
124
+ self.log.debug "Uploading script (%d bytes) to %s:%s." %
125
+ [ source.bytesize, payload['host'], remote_filename ]
126
+ self.upload_script( conn, source, remote_filename )
127
+ self.log.debug " done with the upload."
128
+
129
+ self.run_script( conn, remote_filename, nocleanup )
130
+ self.log.debug "Output was:\n#{@output}"
131
+ end
132
+
133
+ return true
134
+ end
135
+
136
+
137
+ #########
138
+ protected
139
+ #########
140
+
141
+ ### Generate a unique filename for the script on the remote host,
142
+ ### based on +template+ name.
143
+ ###
144
+ def make_remote_filename( template )
145
+ basename = File.basename( template, File.extname(template) )
146
+ tmpname = Dir::Tmpname.make_tmpname( basename, rand(10000) )
147
+
148
+ return "/tmp/#{tmpname}"
149
+ end
150
+
151
+
152
+ ### Generate a script by loading the script +template+, populating it with
153
+ ### +attributes+, and returning the rendered output.
154
+ ###
155
+ def generate_script( template, attributes )
156
+ tmpl = Inversion::Template.load( template, TEMPLATE_OPTS )
157
+ tmpl.attributes.merge!( attributes )
158
+ tmpl.task = self
159
+
160
+ return tmpl.render
161
+ end
162
+
163
+
164
+ ### Upload the templated +source+ via the ssh +conn+ to an
165
+ ### executable file named +remote_filename+.
166
+ ###
167
+ def upload_script( conn, source, remote_filename )
168
+ conn.sftp.file.open( remote_filename, "w", 0755 ) do |fh|
169
+ fh.print( source )
170
+ end
171
+ end
172
+
173
+
174
+ ### Run the +remote_filename+ via the ssh +conn+. The script
175
+ ### will be deleted automatically unless +nocleanup+ is true.
176
+ ###
177
+ def run_script( conn, remote_filename, nocleanup=false )
178
+ @output = conn.exec!( remote_filename )
179
+ conn.exec!( "rm #{remote_filename}" ) unless nocleanup
180
+ end
181
+
182
+ end # Symphony::Task::SSHScript
183
+
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: symphony-ssh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mahlon E. Smith <mahlon@martini.nu>
8
+ - Michael Granger <ged@faeriemud.org>
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-05-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: symphony
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '0.6'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: '0.6'
28
+ - !ruby/object:Gem::Dependency
29
+ name: net-ssh
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: '2.9'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: '2.9'
42
+ - !ruby/object:Gem::Dependency
43
+ name: net-sftp
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '2.1'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: '2.1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '3.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: simplecov
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '0.8'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ~>
82
+ - !ruby/object:Gem::Version
83
+ version: '0.8'
84
+ description: |
85
+ A small collection of base classes used for interacting with remote
86
+ machines over ssh. With them, you can use AMQP (via Symphony) to
87
+ run batch commands, execute templates as scripts, and perform any
88
+ batch/remoting stuff you can think of without the need of separate
89
+ client agents.
90
+ email: mahlon@martini.nu
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - README.rdoc
96
+ - lib/symphony/tasks/ssh.rb
97
+ - lib/symphony/tasks/sshscript.rb
98
+ homepage: http://projects.martini.nu/ruby-modules
99
+ licenses:
100
+ - BSD
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: 2.0.0
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - '>='
114
+ - !ruby/object:Gem::Version
115
+ version: 2.0.3
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.2.2
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Base classes for using Symphony with ssh.
122
+ test_files: []