rutty 1.0.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.
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
+