rutty 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (10) hide show
  1. data/.gitignore +21 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +28 -0
  4. data/LICENSE +20 -0
  5. data/README.md +35 -0
  6. data/Rakefile +25 -0
  7. data/VERSION +1 -0
  8. data/bin/rutty +299 -0
  9. data/rutty.gemspec +64 -0
  10. metadata +133 -0
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :gemcutter
2
+
3
+ gem "jeweler"
4
+ gem "commander"
5
+ gem "net-ssh"
6
+ gem "net-scp"
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ commander (4.0.3)
5
+ highline (>= 1.5.0)
6
+ gemcutter (0.5.0)
7
+ json_pure
8
+ git (1.2.5)
9
+ highline (1.6.1)
10
+ jeweler (1.4.0)
11
+ gemcutter (>= 0.1.0)
12
+ git (>= 1.2.5)
13
+ rubyforge (>= 2.0.0)
14
+ json_pure (1.4.3)
15
+ net-scp (1.0.4)
16
+ net-ssh (>= 1.99.1)
17
+ net-ssh (2.0.23)
18
+ rubyforge (2.0.4)
19
+ json_pure (>= 1.1.7)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ commander
26
+ jeweler
27
+ net-scp
28
+ net-ssh
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Josh Lindsey at Cloudspace
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ RuTTY
2
+ =====
3
+
4
+ RuTTY is a DSH implementation in Ruby.
5
+
6
+ Requirements
7
+ ------------
8
+
9
+ * Ruby >= 1.8.7 (Not tested on 1.9.x)
10
+ * Rubygems >= 1.3.7
11
+ * Bundler >= 1.0.0
12
+
13
+ Installation
14
+ ------------
15
+
16
+ $ sudo gem install bundler
17
+ $ git clone git://github.com/jlindsey/rutty.git
18
+ $ cd rutty
19
+ $ bundle install
20
+ $ ./rutty --help
21
+
22
+ TODO
23
+ ----
24
+
25
+ * Cleanup dsh action code
26
+ * Expand the tag searching semantics to allow for AND and/or OR queries
27
+ * Add a special printout for > 0 exit codes on dsh action
28
+ * Implement delete_node command
29
+ * Make better printouts
30
+ * Documentation
31
+
32
+ Copyright
33
+ ---------
34
+
35
+ Copyright (c) 2010 Josh Lindsey at Cloudspace. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rutty"
8
+ gem.summary = %Q{A DSH implementation in Ruby}
9
+ gem.description = %Q{
10
+ RuTTY is a DSH (distributed / dancer's shell) written in Ruby. It's used to run commands
11
+ on multiple remote servers at once, based on a tagging system. Also allows for multiple
12
+ SCP-style uploads.
13
+ }
14
+ gem.email = "josh@cloudspace.com"
15
+ gem.homepage = "http://github.com/jlindsey/rutty"
16
+ gem.authors = ["Josh Lindsey"]
17
+ gem.add_development_dependency "bundler", ">= 1.0.0"
18
+ gem.add_dependency "commander", ">= 0"
19
+ gem.add_dependency "net-ssh", ">= 0"
20
+ gem.add_dependency "net-scp", ">= 0"
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
25
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/rutty ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Constants
4
+ CONF_DIR = File.join(ENV['HOME'], '.rutty')
5
+ GENERAL_CONF = File.join(CONF_DIR, 'defaults.yaml')
6
+ NODES_CONF = File.join(CONF_DIR, 'nodes.yaml')
7
+
8
+ # Gems
9
+ require 'rubygems'
10
+ require 'bundler/setup'
11
+ require 'commander/import'
12
+ require 'yaml'
13
+
14
+ # Helpers
15
+ add_filter_options = lambda { |c|
16
+ c.option('--keypath PATH', String, 'Path to a private key')
17
+ c.option('--user NAME', String, 'User login')
18
+ c.option('--port NUMBER', Integer, 'SSH port number')
19
+ c.option('--tags TAG1[,TAG2,...]', Array, 'Comma-separated list of tags')
20
+ }
21
+
22
+ def update_node_list &block
23
+ ary = YAML.load(File.open(NODES_CONF).read)
24
+
25
+ yield ary
26
+
27
+ File.open(NODES_CONF, 'w') do |f|
28
+ YAML.dump(ary, f)
29
+ end
30
+
31
+ @node_list = false # Force the next call to get_node_list to read from file again
32
+ end
33
+
34
+ def get_node_list
35
+ @node_list ||= YAML.load(File.open(NODES_CONF).read)
36
+ end
37
+
38
+ def get_defaults_config
39
+ @config ||= YAML.load(File.open(GENERAL_CONF).read)
40
+ end
41
+
42
+ def check_installed!
43
+ unless File.exists? CONF_DIR
44
+ raise "Can't find conf directory at #{CONF_DIR}. Run `rutty init' first. (Or rutty --help for usage)"
45
+ end
46
+ end
47
+
48
+ def filter_nodes nodes, options
49
+ return nodes if options.a === true
50
+
51
+ nodes.delete_if { |node| node[:keypath].nil? or node[:keypath] != options.keypath } unless options.keypath.nil?
52
+ nodes.delete_if { |node| node[:user].nil? or node[:user] != options.user } unless options.user.nil?
53
+ nodes.delete_if { |node| node[:port].nil? or node[:port] != options.port } unless options.port.nil?
54
+ nodes.delete_if { |node| node[:tags].nil? or (node[:tags] & options.tags).empty? } unless options.tags.nil?
55
+ end
56
+
57
+ def options_for_node node
58
+ hash = node
59
+
60
+ hash[:user] ||= get_defaults_config[:user]
61
+ hash[:port] ||= get_defaults_config[:port]
62
+ hash[:keypath] ||= get_defaults_config[:keypath]
63
+ # :host will always be set on node
64
+
65
+ hash
66
+ end
67
+
68
+ # Commander config
69
+ program :name, 'rutty'
70
+ program :description, 'A DSH implementation in Ruby'
71
+ program :version, '1.0.0'
72
+ program :help_formatter, Commander::HelpFormatter::TerminalCompact
73
+
74
+ default_command :dsh
75
+
76
+ # Commander commands
77
+ command :init do |c|
78
+ c.syntax = "rutty init"
79
+ c.summary = "Creates the default file structure for rutty."
80
+ c.when_called do
81
+ if File.exists? CONF_DIR
82
+ log "exists", CONF_DIR
83
+ else
84
+ log "create", CONF_DIR
85
+ Dir.mkdir CONF_DIR
86
+ end
87
+
88
+ if File.exists? GENERAL_CONF
89
+ log "exists", GENERAL_CONF
90
+ else
91
+ log "create", GENERAL_CONF
92
+
93
+ defaults_hash = {
94
+ :user => 'root',
95
+ :keypath => File.join(ENV['HOME'], '.ssh', 'id_rsa'),
96
+ :port => 22
97
+ }
98
+
99
+ File.open(GENERAL_CONF, 'w') do |f|
100
+ YAML.dump(defaults_hash, f)
101
+ end
102
+ end
103
+
104
+ if File.exists? NODES_CONF
105
+ log "exists", NODES_CONF
106
+ else
107
+ log "create", NODES_CONF
108
+
109
+ default_node = [
110
+ {
111
+ :host => 'localhost',
112
+ :port => 2222,
113
+ :keypath => File.join(ENV['HOME'], '.ssh', 'my_key.pem'),
114
+ :user => 'nobody',
115
+ :tags => %w(delete_me example localhost)
116
+ }
117
+ ]
118
+
119
+ File.open(NODES_CONF, 'w') do |f|
120
+ YAML.dump(default_node, f)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ command :add_node do |c|
127
+ c.syntax = "rutty add_node <HOST> [options]"
128
+ c.summary = "Add a node to the pool."
129
+ c.description = "#{c.summary} This is just a convenience method, you can also add a node directly to $HOME/.rutty/nodes.yaml"
130
+
131
+ c.example "Add the 'localhost' node with just a keypath", "rutty add_node localhost --keypath ~/.ssh/my_key.pem"
132
+ c.example "Add a remote IP with all the options",
133
+ "rutty add_node 192.168.1.100 --keypath ~/.ssh/my_key.pem --user root --tags local,funtimes,awesome"
134
+
135
+ add_filter_options.call(c)
136
+
137
+ c.when_called do |args, options|
138
+ check_installed!
139
+ raise "Must supply a hostname or IP address. See `rutty help add_node' for usage" if args.empty?
140
+
141
+ hash = { :host => args.first }
142
+ hash[:keypath] = options.keypath unless options.keypath.nil?
143
+ hash[:user] = options.user unless options.user.nil?
144
+ hash[:port] = options.port unless options.port.nil?
145
+ hash[:tags] = options.tags unless options.tags.nil?
146
+
147
+ update_node_list { |nodes| nodes << hash }
148
+ end
149
+ end
150
+
151
+ command :list_nodes do |c|
152
+ # TODO: Make tag searching AND or OR, not just OR
153
+ c.syntax = "rutty list_nodes [options]"
154
+ c.summary = "List nodes according to [options]."
155
+ c.description = "#{c.summary} Options are the same as add_node"
156
+
157
+ c.example "List all nodes", "rutty list_nodes"
158
+ c.example "List all nodes that are tagged with 'web' OR 'db'", "rutty list_nodes --tags web,db"
159
+ c.example "List all nodes configued to SSH into port 2222", "rutty list_nodes --port 2222"
160
+
161
+ add_filter_options.call(c)
162
+
163
+ c.when_called do |args, options|
164
+ check_installed!
165
+ require 'pp'
166
+
167
+ nodes = get_node_list
168
+ filter_nodes nodes, options
169
+
170
+ pp nodes
171
+ end
172
+ end
173
+
174
+ command :dsh do |c|
175
+ c.syntax = "rutty [dsh] [options] COMMAND"
176
+ c.summary = "Runs the specified COMMAND across all nodes that match [options]"
177
+
178
+ c.example "Get a list of all users logged in to all your web and app nodes", "rutty --tags web,app w"
179
+ c.example "See all your nodes' current memory footprint", "rutty -a \"free -m\""
180
+
181
+ c.option('--tags TAG1[,TAG2,...]', Array, 'Comma-separated list of tags to run the command on')
182
+ c.option('-a', 'Run the command on ALL nodes')
183
+
184
+ c.when_called do |args, options|
185
+ # TODO: Clean this up, it's pretty hard to read and follow
186
+
187
+ check_installed!
188
+ raise "Must supply a command to run. See `rutty help dsh' for usage" if args.empty?
189
+ raise "One of -a or --tags must be passed" if options.a.nil? and options.tags.nil?
190
+ raise "Use either -a or --tags, not both" if !options.a.nil? and !options.tags.nil?
191
+ raise "Multi-word commands must be enclosed in quotes (ex. rutty -a \"ps -ef | grep httpd\")" if args.length > 1
192
+
193
+ com_str = args.pop
194
+
195
+ nodes = get_node_list
196
+ filter_nodes nodes, options
197
+
198
+ require 'logger'
199
+ require 'net/ssh'
200
+ require 'pp'
201
+
202
+ @returns = {}
203
+ connections = []
204
+
205
+ # This is necessary in order to capture exit codes and/or signals,
206
+ # which are't passed through when using just the ssh.exec!() semantics.
207
+ exec_command = lambda { |ssh|
208
+ ssh.open_channel do |channel|
209
+ channel.exec(com_str) do |ch, success|
210
+ unless success
211
+ abort "FAILED: couldn't execute command (ssh.channel.exec
212
+ failure)"
213
+ end
214
+
215
+ channel.on_data do |ch, data| # stdout
216
+ @returns[ssh.host][:out] << data
217
+ end
218
+
219
+ channel.on_extended_data do |ch, type, data|
220
+ next unless type == 1 # only handle stderr
221
+ @returns[ssh.host][:out] << data
222
+ end
223
+
224
+ channel.on_request("exit-status") do |ch, data|
225
+ exit_code = data.read_long
226
+ @returns[ssh.host][:exit] = exit_code
227
+ end
228
+
229
+ channel.on_request("exit-signal") do |ch, data|
230
+ @returns[ssh.host][:sig] = data.read_long
231
+ end
232
+ end
233
+ end
234
+ ssh.loop
235
+ }
236
+
237
+ nodes.each do |node|
238
+ params = options_for_node(node)
239
+ @returns[node[:host]] = { :out => '' }
240
+ connections << Net::SSH.start(params[:host], params[:user], :port => params[:port], :paranoid => false,
241
+ :user_known_hosts_file => '/dev/null', :keys => [params[:keypath]], :logger => Logger.new($stderr),
242
+ :verbose => Logger::FATAL)
243
+ end
244
+
245
+ connections.each { |ssh| exec_command.call(ssh) }
246
+
247
+ loop do
248
+ connections.delete_if { |ssh| !ssh.process(0.1) { |s| s.busy? } }
249
+ break if connections.empty?
250
+ end
251
+
252
+ # TODO: Print this out in a better way
253
+ # TODO: Print a special alert for exit codes > 0
254
+
255
+ pp @returns
256
+ end
257
+ end
258
+
259
+ command :scp do |c|
260
+ c.syntax = "rutty scp [options] LOCAL_PATH REMOTE_PATH"
261
+ c.summary = "Uploads a file to the nodes according to [options]."
262
+ c.description = "#{c.summary} Unlike the actual scp command, this action is one-way: upload only."
263
+
264
+ c.option('--tags TAG1[,TAG2,...]', Array, 'Comma-separated list of tags to run the command on')
265
+ c.option('-a', 'Run the command on ALL nodes')
266
+
267
+ c.when_called do |args, options|
268
+ check_installed!
269
+ raise "Must supply a local path and a remote path" unless args.length == 2
270
+ raise "One of -a or --tags must be passed" if options.a.nil? and options.tags.nil?
271
+ raise "Use either -a or --tags, not both" if !options.a.nil? and !options.tags.nil?
272
+
273
+ require 'logger'
274
+ require 'net/ssh'
275
+ require 'net/scp'
276
+ require 'pp'
277
+
278
+ connections = []
279
+
280
+ remote_path = args.pop
281
+ local_path = args.pop
282
+
283
+ nodes = filter_nodes get_node_list, options
284
+
285
+ nodes.each do |node|
286
+ params = options_for_node(node)
287
+ connections << Net::SSH.start(params[:host], params[:user], :port => params[:port], :paranoid => false,
288
+ :user_known_hosts_file => '/dev/null', :keys => [params[:keypath]], :logger => Logger.new($stderr),
289
+ :verbose => Logger::FATAL)
290
+ end
291
+
292
+ connections.each { |ssh| ssh.scp.upload! local_path, remote_path }
293
+
294
+ loop do
295
+ connections.delete_if { |ssh| !ssh.process(0.1) { |s| s.busy? } }
296
+ break if connections.empty?
297
+ end
298
+ end
299
+ end
data/rutty.gemspec ADDED
@@ -0,0 +1,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{rutty}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Josh Lindsey"]
12
+ s.date = %q{2010-10-13}
13
+ s.default_executable = %q{rutty}
14
+ s.description = %q{
15
+ RuTTY is a DSH (distributed / dancer's shell) written in Ruby. It's used to run commands
16
+ on multiple remote servers at once, based on a tagging system. Also allows for multiple
17
+ SCP-style uploads.
18
+ }
19
+ s.email = %q{josh@cloudspace.com}
20
+ s.executables = ["rutty"]
21
+ s.extra_rdoc_files = [
22
+ "LICENSE",
23
+ "README.md"
24
+ ]
25
+ s.files = [
26
+ ".gitignore",
27
+ "Gemfile",
28
+ "Gemfile.lock",
29
+ "LICENSE",
30
+ "README.md",
31
+ "Rakefile",
32
+ "VERSION",
33
+ "bin/rutty",
34
+ "rutty.gemspec"
35
+ ]
36
+ s.homepage = %q{http://github.com/jlindsey/rutty}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.7}
40
+ s.summary = %q{A DSH implementation in Ruby}
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_development_dependency(%q<bundler>, [">= 1.0.0"])
48
+ s.add_runtime_dependency(%q<commander>, [">= 0"])
49
+ s.add_runtime_dependency(%q<net-ssh>, [">= 0"])
50
+ s.add_runtime_dependency(%q<net-scp>, [">= 0"])
51
+ else
52
+ s.add_dependency(%q<bundler>, [">= 1.0.0"])
53
+ s.add_dependency(%q<commander>, [">= 0"])
54
+ s.add_dependency(%q<net-ssh>, [">= 0"])
55
+ s.add_dependency(%q<net-scp>, [">= 0"])
56
+ end
57
+ else
58
+ s.add_dependency(%q<bundler>, [">= 1.0.0"])
59
+ s.add_dependency(%q<commander>, [">= 0"])
60
+ s.add_dependency(%q<net-ssh>, [">= 0"])
61
+ s.add_dependency(%q<net-scp>, [">= 0"])
62
+ end
63
+ end
64
+
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rutty
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Josh Lindsey
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-13 00:00:00 -04:00
19
+ default_executable: rutty
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: bundler
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 1
32
+ - 0
33
+ - 0
34
+ version: 1.0.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: commander
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: net-ssh
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: net-scp
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 3
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ type: :runtime
78
+ version_requirements: *id004
79
+ description: "\n RuTTY is a DSH (distributed / dancer's shell) written in Ruby. It's used to run commands \n on multiple remote servers at once, based on a tagging system. Also allows for multiple\n SCP-style uploads.\n "
80
+ email: josh@cloudspace.com
81
+ executables:
82
+ - rutty
83
+ extensions: []
84
+
85
+ extra_rdoc_files:
86
+ - LICENSE
87
+ - README.md
88
+ files:
89
+ - .gitignore
90
+ - Gemfile
91
+ - Gemfile.lock
92
+ - LICENSE
93
+ - README.md
94
+ - Rakefile
95
+ - VERSION
96
+ - bin/rutty
97
+ - rutty.gemspec
98
+ has_rdoc: true
99
+ homepage: http://github.com/jlindsey/rutty
100
+ licenses: []
101
+
102
+ post_install_message:
103
+ rdoc_options:
104
+ - --charset=UTF-8
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.3.7
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: A DSH implementation in Ruby
132
+ test_files: []
133
+