murder 0.0.0.pre → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,6 +1,7 @@
1
- Murder by Larry Gadea and Matt Freels
1
+ Murder by Larry Gadea <lg@twitter.com> and Matt Freels <freels@twitter.com>
2
2
  Copyright 2010 Twitter Inc.
3
3
 
4
+
4
5
  DESCRIPTION
5
6
  -----------
6
7
 
@@ -11,6 +12,23 @@ centralized distribution systems wouldn't otherwise function. A "Murder" is
11
12
  normally used to refer to a flock of crows, which in this case applies to a
12
13
  bunch of servers doing something.
13
14
 
15
+
16
+ QUICK START
17
+ -----------
18
+
19
+ For the impatient, `gem install murder` and add these lines to your Capfile:
20
+
21
+ require 'murder'
22
+
23
+ set :deploy_via, :murder
24
+ after 'deploy:setup', 'murder:distribute_files'
25
+ before 'murder:start_seeding', 'murder:start_tracker'
26
+ after 'murder:stop_seeding', 'murder:stop_tracker'
27
+
28
+
29
+ HOW IT WORKS
30
+ ------------
31
+
14
32
  In order to do a Murder transfer, there are several components required to be
15
33
  set up beforehand -- many the result of BitTorrent nature of the system. Murder
16
34
  is based on BitTornado.
@@ -23,68 +41,99 @@ acceptable. To keep things simple tracker-less distribution (DHT) is currently
23
41
  not supported. The tracker is actually just a mini-httpd that hosts a
24
42
  /announce path which the Bittorrent clients update their state onto.
25
43
 
26
- - A seeder. This is the server which has the files that you'd like to deploy
27
- onto all other servers. For Twitter, this is the server that did the git diff.
28
- The files are placed into a directory that a torrent gets created from. Murder
29
- will tgz up the directory and create a .torrent file (a very small file
30
- containing basic hash information about the tgz file). This .torrent file lets
31
- the peers know what they're downloading. The tracker keeps track of which
32
- .torrent files are currently being distributed. Once a Murder transfer is
33
- started, the seeder will be the first server many machines go to to get
34
- pieces. These pieces will then be distributed in a tree-fashion to the rest of
35
- the network, but without necessarily getting the parts from the seeder.
44
+ - A seeder. This is the server which has the files that you'd like to
45
+ deploy onto all other servers. The files are placed into a directory
46
+ that a torrent gets created from. Murder will tgz up the directory and
47
+ create a .torrent file (a very small file containing basic hash
48
+ information about the tgz file). This .torrent file lets the peers
49
+ know what they're downloading. The tracker keeps track of which
50
+ .torrent files are currently being distributed. Once a Murder transfer
51
+ is started, the seeder will be the first server many machines go to to
52
+ get pieces. These pieces will then be distributed in a tree-fashion to
53
+ the rest of the network, but without necessarily getting the parts
54
+ from the seeder.
36
55
 
37
56
  - Peers. This is the group of servers (hundreds to tens of thousands) which
38
57
  will be receiving the files and distributing the pieces amongst themselves.
39
58
  Once a peer is done downloading the entire tgz file, it will continue seeding
40
59
  for a while to prevent a hotspot effect on the seeder.
41
60
 
42
- MURDER TRANSFER PROCESS
61
+
62
+ CONFIGURATION AND USAGE
43
63
  -----------------------
44
64
 
45
- 1. Configure the list of servers and general settings in config.rb. (one time)
46
- 2. Distribute the Murder files to all your servers: (one time)
65
+ Murder integrates with Capistrano. The most simple way to use it is as
66
+ a deploy strategy, by setting `:deploy_via` to `:murder`. By default,
67
+ murder makes the same assumptions that cap makes. All servers without
68
+ `:no_release => true` will act as peers. Additionally, murder will
69
+ automatically use the first peer as both tracker and seeder. you may
70
+ redefine the `tracker`, `seeder` and `peer` roles yourself to change
71
+ these defaults, for instance, if you want to set up a dedicated
72
+ tracker.
73
+
74
+ All involved servers must have python installed and the related murder
75
+ support files (BitTornado, etc.). To upload the support files to the
76
+ tracker, seeder, and peers, run:
77
+
47
78
  cap murder:distribute_files
48
- 3. Start the tracker: (one time)
79
+
80
+ By default, these will go in `shared/murder` in your apps deploy
81
+ directory. Override this by setting the variable
82
+ `remote_murder_path`. For convenience, you can add an after hook to
83
+ run this on `deploy:setup`:
84
+
85
+ after 'deploy:setup', 'murder:distribute_files'
86
+
87
+ Before deploying, you must start the tracker:
88
+
49
89
  cap murder:start_tracker
50
- 4. Create a torrent file from a remote directory of files (on seeder):
51
- cap murder:create_torrent tag="Deploy20100101" files_path="~/files"
52
- 5. Start seeding the files:
53
- cap murder:start_seeding tag="Deploy20100101"
54
- 6. Distribute the files to all servers:
55
- cap murder:peer tag="Deploy20100101" destination_path="/tmp/out"
56
90
 
57
- Once completed, all files will be in /tmp/out/Deploy20091015/ on all servers.
91
+ To have this happen automatically during a deploy, add the following hooks:
58
92
 
59
- EXAMPLE DEPLOY
60
- --------------
93
+ before 'murder:start_seeding', 'murder:start_tracker'
94
+ after 'murder:stop_seeding', 'murder:stop_tracker'
95
+
96
+ At this point you should be able to deploy normally:
97
+
98
+ cap deploy
99
+
100
+
101
+ MANUAL USAGE (murder without a deploy strategy)
102
+ -----------------------------------------------
103
+
104
+ Murder can also be used as a general mechanism to distribute files
105
+ across a generic set of servers. To do so create a Capfile, require
106
+ murder, and manually define roles:
107
+
108
+ require 'rubygems'
109
+ require 'murder'
110
+
111
+ set :remote_murder_path, '/opt/local/murder' # or some other directory
112
+
113
+ role :peer, 'host1', 'host2', 'host3', 'host4', 'host5', host6', host7'
114
+ role :seeder, 'host1'
115
+ role :tracker, 'host1'
116
+
117
+ To distribute a directory of files, first make sure that murder is set
118
+ up on all hosts, then manually run the murder cap tasks:
119
+
120
+ 1. Start the tracker:
121
+ cap murder:start_tracker
122
+
123
+ 2. Create a torrent from a directory of files on the seeder, and start seeding:
124
+ scp -r ./files host1:~/files
125
+ cap murder:create_torrent tag="Deploy1" files_path="~/files"
126
+ cap murder:start_seeding tag="Deploy1"
127
+
128
+ 3. Distribute the torrent to all peers:
129
+ cap murder:peer tag="Deploy1" destination_path="/tmp"
130
+
131
+ 4. Stop the seeder and tracker:
132
+ cap murder:stop_seeding
133
+ cap murder:stop_tracker
134
+
135
+ When this finishes, all peers will have the files in /tmp/Deploy1
61
136
 
62
- cap murder:distribute_files
63
- cap murder:start_tracker
64
- cap murder:create_torrent tag="Deploy20100101"
65
- files_path="/usr/local/twitter/production/current"
66
- cap murder:start_seeding tag="Deploy20100101"
67
- time cap murder:peer tag="Deploy20100101"
68
- destination_path="/usr/local/twitter/releases"
69
-
70
- All the files have been transferred to all servers at this point. To clean up
71
- use:
72
-
73
- cap murder:stop_seeding
74
- cap murder:stop_tracker
75
-
76
- HOW TO LEARN MURDER INCREMENTALLY
77
- ---------------------------------
78
-
79
- - Skim dist/*.py to see what Murder does behind the scenes
80
- - Experiment with the python scripts
81
- - Read Capfile
82
- - Read murder_config.rb
83
- - Read murder_admin.rb
84
- - Read murder.rb
85
- - Read the reference below
86
- - Experiment with the Capistrano deploy methods
87
- - Read and experiment with the Murder Capistrano strategy
88
137
 
89
138
  TASK REFERENCE
90
139
  --------------
@@ -151,15 +200,11 @@ stop_all_peering:
151
200
  command will forcibly kill all "murder_client.py peer" commands that are
152
201
  running.
153
202
 
154
- CONFIG REFERENCE:
155
-
156
- user:
157
- Provided by Capistrano to specify if to use a different username when sshing
158
- into machines.
203
+ CONFIG REFERENCE
204
+ ----------------
159
205
 
160
- host_suffix:
161
- For the tracker_host, seeder_host and peers servers, this suffix will always
162
- be added to the end when trying to connect to them.
206
+ Variables
207
+ ---------
163
208
 
164
209
  default_tag:
165
210
  A tag name to use by default such that a tag parameter doesn't need to be
@@ -174,51 +219,19 @@ default_destination_path:
174
219
  A path on the peers' file system where the files that were distributed
175
220
  should be decompressed into.
176
221
 
177
- tracker_host:
178
- The hostname of a single tracker server. If you're using a host_suffix, do
179
- not specify a suffix here.
222
+ remote_murder_path:
223
+ A path where murder will look for its support files on each host. `cap
224
+ murder:distribute_files` will upload murder support files here.
225
+
226
+
227
+ Roles
228
+ -----
180
229
 
181
- tracker_port:
182
- This is the port on which the mini-web-server tracker that the Bittorrent
183
- libraries run is hosted on. Must be 8998 for now.
230
+ tracker:
231
+ Host on which to run the BitTorrent tracker
184
232
 
185
- seeder_host:
186
- The hostname of a single seeder server. This server is the one that has all
187
- the files that the peers want. If you're using a host_suffix, do not specify a
188
- suffix here.
233
+ seeder:
234
+ Host which will be the source of the files to be distributed via BitTorrent
189
235
 
190
236
  peers:
191
- A list of peers which will be receiving the files and distributing them
192
- amongst themselves. If you're using a host_suffix, do not specify a suffix
193
- here.
194
-
195
- CAPISTRANO COPY STRATEGY
196
- ------------------------
197
-
198
- In addition to being usable from both the commandline and Capistrano commands,
199
- an optional Capistrano copy strategy is included to help easily retrofit
200
- existing deploy environments you might already have.
201
-
202
- Requirements to use build.rb
203
- - Add a require line for build.rb in your Capfile. This will override the
204
- default build strategy. There are some other parameters you must now
205
- specify though.
206
- - To enable it, add the following to your Capfile:
207
- load 'deploy'
208
- set :strategy, Capistrano::Deploy::Strategy::Build.new(self)
209
- - Add "set :murder, true" to your Capfile
210
- - Make sure your release_name is correct (should be already)
211
- - If you're distribution is a file instead of a directory,
212
- Add "distribution_is_a_file" to your Capfile
213
- - Add the following to your Capfile:
214
- set :build_task do
215
- "true"
216
- end
217
- set :copy_compression, :gz
218
- set :package_name do
219
- "#{release_name}-#{real_revision[0, 8]}.tar.#{copy_compression}"
220
- end
221
- set :branch, variables[:branch] || 'deploy'
222
-
223
- The requirements are a bit hefty for now, though hopefully this will get easier
224
- in the future.
237
+ All hosts to which files should be distributed
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ begin
9
9
  gem.description = %Q{Large scale server deploys using BitTorrent and the BitTornado library}
10
10
  gem.email = "lg@twitter.com"
11
11
  gem.homepage = "http://github.com/lg/murder"
12
- gem.authors = ["Larry Gadea"]
12
+ gem.authors = ["Larry Gadea", "Matt Freels"]
13
13
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
14
  end
15
15
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0.pre
1
+ 0.1.0
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
@@ -27,7 +28,7 @@ if PSYCO.psyco:
27
28
  psyco.full()
28
29
  except:
29
30
  pass
30
-
31
+
31
32
  from BitTornado.download_bt1 import BT1Download, defaults, parse_params, get_usage, get_response
32
33
  from BitTornado.RawServer import RawServer, UPnP_ERROR
33
34
  from random import seed
@@ -91,17 +92,17 @@ class HeadlessDisplayer:
91
92
 
92
93
  def finished(self):
93
94
  global doneFlag
94
-
95
+
95
96
  self.done = True
96
97
  self.percentDone = '100'
97
98
  self.timeEst = 'Download Succeeded!'
98
99
  self.downRate = ''
99
100
  #self.display()
100
-
101
+
101
102
  global isPeer
102
-
103
+
103
104
  print "done and done"
104
-
105
+
105
106
  if isPeer:
106
107
  if os.fork():
107
108
  os._exit(0)
@@ -118,7 +119,7 @@ class HeadlessDisplayer:
118
119
 
119
120
  t = threading.Timer(30.0, ok_close_now)
120
121
  t.start()
121
-
122
+
122
123
  def failed(self):
123
124
  self.done = True
124
125
  self.percentDone = '0'
@@ -135,12 +136,12 @@ class HeadlessDisplayer:
135
136
  print errormsg
136
137
  doneFlag.set()
137
138
 
138
- def display(self, dpflag = Event(), fractionDone = None, timeEst = None,
139
+ def display(self, dpflag = Event(), fractionDone = None, timeEst = None,
139
140
  downRate = None, upRate = None, activity = None,
140
141
  statistics = None, **kws):
141
142
  if self.last_update_time + 0.1 > clock() and fractionDone not in (0.0, 1.0) and activity is not None:
142
143
  return
143
- self.last_update_time = clock()
144
+ self.last_update_time = clock()
144
145
  if fractionDone is not None:
145
146
  self.percentDone = str(float(int(fractionDone * 1000)) / 10)
146
147
  if timeEst is not None:
@@ -174,7 +175,7 @@ class HeadlessDisplayer:
174
175
  #print 'seed status: ', self.seedStatus
175
176
  #print 'peer status: ', self.peerStatus
176
177
  #stdout.flush()
177
- dpflag.set()
178
+ dpflag.set()
178
179
 
179
180
  def chooseFile(self, default, size, saveas, dir):
180
181
  self.file = '%s (%.1f MB)' % (default, float(size) / (1 << 20))
@@ -212,7 +213,7 @@ def run(params):
212
213
 
213
214
  myid = createPeerID()
214
215
  seed(myid)
215
-
216
+
216
217
  global doneFlag
217
218
  doneFlag = Event()
218
219
  def disp_exception(text):
@@ -245,7 +246,7 @@ def run(params):
245
246
  dow = BT1Download(h.display, h.finished, h.error, disp_exception, doneFlag,
246
247
  config, response, infohash, myid, rawserver, listen_port,
247
248
  configdir)
248
-
249
+
249
250
  if not dow.saveAs(h.chooseFile, h.newpath):
250
251
  break
251
252
 
@@ -283,9 +284,9 @@ if __name__ == '__main__':
283
284
  sys.exit(1)
284
285
 
285
286
  argv = ["--responsefile", sys.argv[2],
286
- "--saveas", sys.argv[3],
287
+ "--saveas", sys.argv[3],
287
288
  "--ip", sys.argv[4]]
288
-
289
+
289
290
  isPeer = sys.argv[1] == "peer"
290
-
291
+
291
292
  run(argv[1:])
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
data/lib/murder.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
data/lib/murder/admin.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
@@ -14,6 +15,9 @@
14
15
  # limitations under the License.
15
16
 
16
17
  namespace :murder do
18
+ desc <<-DESC
19
+ SCPs a compressed version of all files from ./dist (the python Bittorrent library and custom scripts) to all server. The entire directory is sent, regardless of the role of each individual server. The path on the server is specified by remote_murder_path and will be cleared prior to transferring files over.
20
+ DESC
17
21
  task :distribute_files, :roles => [:tracker, :seeder, :peer] do
18
22
  dist_path = File.expand_path('../../dist', __FILE__)
19
23
 
@@ -29,18 +33,22 @@ namespace :murder do
29
33
  system "rm /tmp/murder_dist.tgz"
30
34
  end
31
35
 
36
+ desc "Starts the Bittorrent tracker (essentially a mini-web-server) listening on port 8998."
32
37
  task :start_tracker, :roles => :tracker do
33
38
  run("screen -dms murder_tracker python #{remote_murder_path}/murder_tracker.py && sleep 0.2", :pty => true)
34
39
  end
35
40
 
41
+ desc "If the Bittorrent tracker is running, this will kill the process. Note that if it is not running you will receive an error."
36
42
  task :stop_tracker, :roles => :tracker do
37
43
  run("pkill -f 'SCREEN.*murder_tracker.py'")
38
44
  end
39
45
 
46
+ desc "Identical to stop_seeding, except this will kill all seeding processes. No 'tag' argument is needed."
40
47
  task :stop_all_seeding, :roles => :seeder do
41
48
  run("pkill -f \"SCREEN.*seeder-\"")
42
49
  end
43
50
 
51
+ desc 'Sometimes peers can go on forever (usually because of an error). This command will forcibly kill all "murder_client.py peer" commands that are running.'
44
52
  task :stop_all_peering, :roles => :peer do
45
53
  run("pkill -f \"murder_client.py peer\"")
46
54
  end
data/lib/murder/murder.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright 2010 Twitter, Inc.
2
2
  # Copyright 2010 Larry Gadea <lg@twitter.com>
3
+ # Copyright 2010 Matt Freels <freels@twitter.com>
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
6
  # not use this file except in compliance with the License. You may obtain
@@ -13,15 +14,10 @@
13
14
  # See the License for the specific language governing permissions and
14
15
  # limitations under the License.
15
16
 
16
- # no default defaults...
17
- set :default_tag, ''
18
- set :default_seeder_files_path, ''
19
- set :default_destination_path, ''
20
-
21
- # default remote dist path in app shared directory
22
- set(:remote_murder_path) { "#{shared_path}/murder" }
23
-
24
17
  namespace :murder do
18
+ desc <<-DESC
19
+ Compresses the directory specified by the passed-in argument 'files_path' and creates a .torrent file identified by the 'tag' argument. Be sure to use the same 'tag' value with any following commands. Any .git directories will be skipped. Once completed, the .torrent will be downloaded to your local /tmp/TAG.tgz.torrent.
20
+ DESC
25
21
  task :create_torrent, :roles => :seeder do
26
22
  require_tag
27
23
  if !(seeder_files_path = (default_seeder_files_path if default_seeder_files_path != "") || ENV['files_path'])
@@ -44,21 +40,33 @@ namespace :murder do
44
40
  download_torrent unless ENV['do_not_download_torrent']
45
41
  end
46
42
 
43
+ desc <<-DESC
44
+ Although not necessary to run, if the file from create_torrent was lost, you can redownload it from the seeder using this task. You must specify a valid 'tag' argument.
45
+ DESC
47
46
  task :download_torrent, :roles => :seeder do
48
47
  require_tag
49
48
  download("#{filename}.torrent", "#{filename}.torrent", :via => :scp)
50
49
  end
51
50
 
51
+ desc <<-DESC
52
+ Will cause the seeder machine to connect to the tracker and start seeding. The ip address returned by the 'host' bash command will be announced to the tracker. The server will not stop seeding until the stop_seeding task is called. You must specify a valid 'tag' argument (which identifies the .torrent in /tmp to use)
53
+ DESC
52
54
  task :start_seeding, :roles => :seeder do
53
55
  require_tag
54
56
  run "screen -dms 'seeder-#{tag}' python #{remote_murder_path}/murder_client.py seeder '#{filename}.torrent' '#{filename}' `host $HOSTNAME | awk '{print $4}'`"
55
57
  end
56
58
 
59
+ desc <<-DESC
60
+ If the seeder is currently seeding, this will kill the process. Note that if it is not running, you will receive an error. If a peer was downloading from this seed, the peer will find another host to receive any remaining data. You must specify a valid 'tag' argument.
61
+ DESC
57
62
  task :stop_seeding, :roles => :seeder do
58
63
  require_tag
59
64
  run("pkill -f \"SCREEN.*seeder-#{tag}\"")
60
65
  end
61
66
 
67
+ desc <<-DESC
68
+ Instructs all the peer servers to connect to the tracker and start download and spreading pieces and files amongst themselves. You must specify a valid 'tag' argument. Once the download is complete on a server, that server will fork the download process and seed for 30 seconds while returning control to Capistrano. Cap will then extract the files to the passed in 'destination_path' argument to destination_path/TAG/*. To not create this tag named directory, pass in the 'no_tag_directory=1' argument. If the directory is empty, this command will fail. To clean it, pass in the 'unsafe_please_delete=1' argument. The compressed tgz in /tmp is never removed. When this task completes, all files have been transferred and moved into the requested directory.
69
+ DESC
62
70
  task :peer, :roles => :peer do
63
71
  require_tag
64
72
 
data/murder.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{murder}
8
- s.version = "0.0.0.pre"
8
+ s.version = "0.1.0"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Larry Gadea"]
12
- s.date = %q{2010-03-15}
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Larry Gadea", "Matt Freels"]
12
+ s.date = %q{2010-03-17}
13
13
  s.description = %q{Large scale server deploys using BitTorrent and the BitTornado library}
14
14
  s.email = %q{lg@twitter.com}
15
15
  s.extra_rdoc_files = [
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: murder
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: true
4
+ prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 1
7
8
  - 0
8
- - 0
9
- - pre
10
- version: 0.0.0.pre
9
+ version: 0.1.0
11
10
  platform: ruby
12
11
  authors:
13
12
  - Larry Gadea
13
+ - Matt Freels
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-03-15 00:00:00 -07:00
18
+ date: 2010-03-17 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -111,13 +111,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
111
  version: "0"
112
112
  required_rubygems_version: !ruby/object:Gem::Requirement
113
113
  requirements:
114
- - - ">"
114
+ - - ">="
115
115
  - !ruby/object:Gem::Version
116
116
  segments:
117
- - 1
118
- - 3
119
- - 1
120
- version: 1.3.1
117
+ - 0
118
+ version: "0"
121
119
  requirements: []
122
120
 
123
121
  rubyforge_project: