symphony-ssh 0.1.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,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: []