rudy 0.3.2 → 0.4.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.
data/CHANGES.txt CHANGED
@@ -1,9 +1,60 @@
1
1
  RUDY, CHANGES
2
2
 
3
+ TODO: Look into RubyGems error
4
+ /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/gempackagetask.rb:13:Warning: Gem::manage_gems is deprecated and will be removed on or after March 2009.
5
+ rake aborted!
6
+ RubyGem version error: rdoc(2.4.0 not ~> 2.3.0)
7
+
8
+ TODO: Remove aws_swb
9
+
10
+
11
+ #### 0.4.1 (2009-03-??) ###############################
12
+
13
+ * CHANGE: Recommend keypair config to be in ~/.rudy/config
14
+ * FIX: Rudy now checks for user keys specified by env-role, env, and global
15
+ * FIX: gemspec dependency net-ssh-multi
16
+ * NEW: rerelease command
17
+
18
+ #### 0.4 (2009-03-12) ###############################
19
+
20
+ NOTE: This is a significant re-write from 0.3
21
+
22
+ * CHANGE: Mostly re-written bin/ruby, moving validation to Command classes
23
+ * CHANGE: upgrade to Drydock 0.5
24
+ * CHANGE: Moved generic EC2 commands to bin/rudy-ec2
25
+ * CHANGE: Removed ambiguity of pluralized command names.
26
+ * OLD: backups, disks, configs
27
+ * NEW: backup, disk, config
28
+ * NEW: ssh and scp commands for connecting to and copying files to/from machines
29
+ * NEW: New dependencies (trying out net-ssh)
30
+ * NEW: Domain specific language for configuration
31
+ * NEW: "Routines" for handling common actions. Starting, stopping, releasing, deploying.
32
+ * NEW: "rudy release" will create a release from the current working copy, start an instance,
33
+ checkout the release, run routines. It needs some work still, but this already functions as
34
+ a single command release process.
35
+ * NEW: "rudy start|destroy|restart|update|status". Routines allow us to have generic commands
36
+ that can be used for any machine group. These commands relate to starting new instances.
37
+ * NEW: Extra caution when running destructive commands
38
+ * NEW: Default ~/.rudy/config created if it doesn't exist.
39
+
40
+
41
+ #### 0.3 (2009-02-26) ###############################
42
+
43
+ NOTE: This is a significant re-write from 0.2
44
+
45
+ * CHANGE: Re-written support/rudy-ec2-startup
46
+ * CHANGE: upgrade to Drydock 0.4
47
+ * NEW: More functionality for disks and backups
48
+ * NEW: config commands
49
+ * NEW: Per machine configuration (via ~/.rudy)
50
+
51
+
3
52
  #### 0.2 (2009-02-23) ###############################
4
53
 
5
54
  NOTE: This is a complete re-write from 0.1
6
55
 
56
+ * CHANGE: Added Environment variables
57
+ * CHANGE: upgrade to drydock 0.3.3
7
58
  * NEW: All time references are converted to UTC
8
59
  * NEW: Safer "Are you sure?". Number of characters to enter is
9
60
  commiserate with amount of danger.
@@ -16,10 +67,9 @@ NOTE: This is a complete re-write from 0.1
16
67
  * NEW: Partial support for regions and zones
17
68
  * NEW: Manage system based on security groups.
18
69
  * NEW: "rudy groups" overhaul. Display, creates, destroys groups.
19
- * CHANGE: Added Environment variables
20
- * UPGRADE: drydock 0.3.3
70
+
21
71
 
22
72
 
23
73
  #### 0.1 (2009-02-06) ###############################
24
74
 
25
- * Initial release
75
+ * Initial public release
data/README.rdoc CHANGED
@@ -1,12 +1,13 @@
1
- = Rudy - v0.3 BETA!
1
+ = Rudy - v0.4 ALPHA!
2
2
 
3
3
  Rudy is a handy staging and deployment tool for EC2.
4
4
 
5
- NOTE: Rudy is not ready for general consumption. Come back in Q2 2009!
5
+ NOTE: Rudy will be ready for general consumption in Q2 2009.
6
6
 
7
7
  == Installation
8
8
 
9
- # Soon!
9
+ * Soon!
10
+ * ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION' (See net/ssh docs)
10
11
 
11
12
  == More Info
12
13
 
@@ -23,8 +24,11 @@ NOTE: Rudy is not ready for general consumption. Come back in Q2 2009!
23
24
 
24
25
  == Thanks
25
26
 
26
- * The Rilli[http://rilli.com/] team, for the initial use case and support.
27
- * Alicia Eyre Vasconcelos, for the name ("H-e-l-l-o R.U.D.Y")
27
+ * The Rilli.com team -- for the initial use case, the ongoing feedback and support, and the good times!
28
+ * Adam Bognar
29
+ * Andrew Simpson
30
+ * Caleb Buxton
31
+ * Colin Brumelle
28
32
 
29
33
 
30
34
  == License
data/bin/rudy CHANGED
@@ -4,348 +4,171 @@
4
4
  #
5
5
  # See rudy -h for usage
6
6
  #
7
- # AWS commands need the access identifiers. If you don't want
8
- # to include them as arguments, you can set the following variables
9
- # in your environment (replace **** with the literal value):
10
- #
11
-
12
- #
13
- # No Ruby 1.9.1 support. Only 1.8.x for now :[
14
- unless RUBY_VERSION < "1.9"
15
- puts "Sorry! We're using the right_aws gem and it doesn't support Ruby 1.9 yet."
16
- exit 1
17
- end
18
7
 
19
- RUDY_HOME = File.join(File.dirname(__FILE__))
20
- RUDY_LIB = File.join(RUDY_HOME, '..', 'lib')
8
+ RUDY_HOME = File.join(File.dirname(__FILE__), '..')
9
+ RUDY_LIB = File.join(RUDY_HOME, 'lib')
21
10
  $:.unshift RUDY_LIB # Put our local lib in first place
22
11
 
23
- require 'rubygems'
24
- require 'openssl'
25
-
12
+ require 'rubygems' if RUBY_VERSION < "1.9"
13
+ require 'date'
26
14
  require 'drydock'
27
- require 'rudy'
28
15
  extend Drydock
29
16
 
30
-
31
-
32
- default :commands
33
-
34
-
35
- global_usage "rudy [global options] COMMAND [command options]"
36
- global_option :A, :access_key, "AWS Access Key", String
37
- global_option :S, :secret_key, "AWS Secret Access Key", String
38
- global_option :R, :region, String, "Connect to a specific EC2 region (default: #{Rudy::DEFAULT_REGION})"
39
-
40
- #global_option :z, :zone, String, "Connect to a specific EC2 zone (default: #{Rudy::DEFAULT_ZONE})"
41
- global_option :e, :environment, String, "Connect to the specified environment (default: #{Rudy::DEFAULT_ENVIRONMENT})"
42
- global_option :r, :role, String, "Connect to a machine with the specified role (defalt: #{Rudy::DEFAULT_ROLE})"
43
- global_option :p, :position, String, "Position in the machine in its group (default: #{Rudy::DEFAULT_POSITION})"
44
-
45
- global_option :u, :user, String, "Provide a username (default: #{Rudy::DEFAULT_USER})"
46
-
47
- #global_option :c, :config, String, "Specify the config file to read (default: #{Rudy::RUDY_CONFIG})"
48
-
49
-
50
- global_option :V, :version, "Display version number" do
51
- puts "Rudy version: #{Rudy::VERSION}"
52
- exit 0
53
- end
54
- global_option :v, :verbose, "Increase verbosity of output (i.e. -v or -vv or -vvv)" do
17
+ project "Rudy" # This also runs require 'ruby'
18
+
19
+ global :A, :accesskey, String, "AWS Access Key"
20
+ global :S, :secretkey, String, "AWS Secret Access Key"
21
+ #global :R, :region, String, "Connect to a specific EC2 region (ie: #{Rudy::DEFAULT_REGION})"
22
+ global :f, :config, String, "Specify another configuration file to read (ie: #{Rudy::RUDY_CONFIG_FILE})"
23
+ global :z, :zone, String, "Connect to a specific EC2 zone (ie: #{Rudy::DEFAULT_ZONE})"
24
+ global :e, :environment, String, "Connect to the specified environment (ie: #{Rudy::DEFAULT_ENVIRONMENT})"
25
+ global :r, :role, String, "Connect to a machine with the specified role (ie: #{Rudy::DEFAULT_ROLE})"
26
+ global :p, :position, String, "Position in the machine in its group (ie: #{Rudy::DEFAULT_POSITION})"
27
+ global :u, :user, String, "Provide a username (ie: #{Rudy::DEFAULT_USER})"
28
+ global :q, :quiet, "Run with less output"
29
+ global :v, :verbose, "Increase verbosity of output (i.e. -v or -vv or -vvv)" do
55
30
  @verbose ||= 0
56
31
  @verbose += 1
57
32
  end
58
- global_option :q, :quiet, "Run with less output"
59
-
60
- command :commands => Rudy::Command::Metadata do |obj|
61
- obj.print_header
62
-
63
- puts "Rudy can do all of these things:", ""
64
- command_names.sort.each do |cmd|
65
- puts " %16s" % cmd
66
- end
67
- puts
68
- puts "Try: rudy -h OR rudy COMMAND -h"
69
- puts
33
+ global :V, :version, "Display version number" do
34
+ puts "Rudy version: #{Rudy::VERSION}"
35
+ exit 0
70
36
  end
71
37
 
72
- debug :off
38
+ #desc "Run this the first time you use Rudy (it's immutable so running it again does no harm)."
39
+ #command :setup => Rudy::Command::Metadata
73
40
 
74
- usage "rudy init"
75
- command :init => Rudy::Command::Metadata do |obj|
76
- obj.setup
77
- end
78
41
 
79
- usage "rudy info"
80
- command :info => Rudy::Command::Metadata do |obj|
81
- obj.info
82
- end
42
+ # ------------------------------------ RUDY INFO COMMANDS --------
43
+ # ------------------------------------------------------------------
83
44
 
84
- option :a, :all, "Display config settings for all machines"
85
- option :d, :defaults, "Display the default value for the supplied parameter"
86
- usage "rudy [-f config-file] config [param-name]"
87
- command :config => Rudy::Command::Environment do |obj, argv|
88
- obj.config(argv.first)
89
- end
45
+ #usage "rudy info"
46
+ #desc "Displays info about the current Rudy configuration"
47
+ #command :info => Rudy::Command::Metadata
90
48
 
49
+ usage "rudy [-f config-file] config [param-name]"
50
+ desc "Check Rudy configuration."
51
+ option :l, :all, "Display config settings for all machines"
52
+ option :d, :defaults, "Display the default value for the supplied parameter"
53
+ argv :name
54
+ command :config => Rudy::Command::Config
91
55
 
56
+ usage "rudy myaddress [-i] [-e]"
57
+ desc "Displays you current internal and external IP addresses"
92
58
  option :e, :external, "Display only external IP address"
93
59
  option :i, :internal, "Display only internal IP address"
94
- usage "rudy myaddress [-i] [-e]"
95
60
  command :myaddress do |obj|
96
- ea = Rudy::Utils::external_ip_address
97
- ia = Rudy::Utils::internal_ip_address
98
- puts "%10s: %s" % ['Internal', ia] unless obj.external && !obj.internal
99
- puts "%10s: %s" % ['External', ea] unless obj.internal && !obj.external
100
- end
101
-
102
-
103
- option :D, :destroy, "Destroy all metadata stored in SimpleDB"
104
- option :u, :update, "Update the role or environment metadata for the given instance-IDs"
105
- usage "rudy [global options] metadata instance-ID"
106
- command :metadata => Rudy::Command::Metadata do |obj, argv|
107
- obj.print_header
108
-
109
- if obj.update
110
- raise "No instance ID!" if argv.empty?
111
- raise "Nothing to change (see global options -r or -e)" unless obj.role || obj.environment
112
- obj.update_metadata(argv.first)
113
- elsif obj.destroy
114
- exit unless are_you_sure?
115
- obj.destroy_metadata
116
- else
117
- obj.print_metadata(argv.first)
118
- end
119
- end
120
-
121
-
122
- option :i, :instance, String, "Instance ID to associate the id"
123
- option :A, :associate, "Associate an address to a running instance"
124
- usage "rudy [global options] addresses [-A -i instance ID] [address]"
125
- command :addresses => Rudy::Command::Addresses do |obj, argv|
126
- obj.print_header
127
-
128
- if obj.associate
129
- obj.associate_address(argv.first)
61
+ ea = Rudy::Utils::external_ip_address || ''
62
+ ia = Rudy::Utils::internal_ip_address || ''
63
+ if obj.global.quiet
64
+ puts ia unless obj.option.external && !obj.option.internal
65
+ puts ea unless obj.option.internal && !obj.option.external
130
66
  else
131
- obj.print_addresses
132
- end
133
- end
134
-
135
-
136
-
137
- option :p, :print, "Only print the SSH command, don't connect"
138
- usage "rudy [-e env] [-u user] connect [-p]"
139
- command :connect => Rudy::Command::Environment do |obj|
140
- capture(:stderr) do
141
- obj.print_header
142
- obj.connect
67
+ puts "%10s: %s" % ['Internal', ia] unless obj.option.external && !obj.option.internal
68
+ puts "%10s: %s" % ['External', ea] unless obj.option.internal && !obj.option.external
143
69
  end
144
70
  end
145
71
 
146
72
 
147
- option :r, :remote, "Copy FROM the remote machine to the local machine"
148
- option :p, :print, "Only print the SSH command, don't connect"
149
- usage "rudy [-e env] [-u user] copy [-p] -r [from path] [to path]"
150
- command :copy => Rudy::Command::Environment do |obj, argv|
151
- capture(:stderr) do
152
- obj.print_header
153
- raise "No path specified (rudy copy FROM-PATH [FROM-PATH ...] TO-PATH)" unless argv.size >= 2
154
- obj.copy(argv)
155
- end
156
- end
157
73
 
74
+ # ----------------------------- RUDY MAINTENANCE COMMANDS --------
75
+ # ------------------------------------------------------------------
158
76
 
159
- option :all, "Display all disk definitions"
77
+ usage "#{$/} [global options] disks [-C -p path -d device -s size] [-A] [-D] [disk name]"
78
+ desc "Manage Disks"
79
+ option :l, :all, "Display all disk definitions"
160
80
  option :p, :path, String, "The filesystem path to use as the mount point"
161
81
  option :d, :device, String, "The device id (default: /dev/sdh)"
162
82
  option :s, :size, Integer, "The size of disk (in GB)"
163
- option :C, :create, "Create a disk definition"
164
- option :D, :destroy, "Destroy a disk definition"
165
- option :A, :attach, "Attach a disk"
166
- option :N, :unattach, "Unattach a disk"
167
- usage "rudy [global options] disks [-C -p path -d device -s size] [-A] [-D] [disk name]"
168
- command :disks => Rudy::Command::Disks do |obj, argv|
169
- capture(:stderr) do
170
- obj.print_header
171
- if obj.create
172
- raise "No filesystem path specified" unless obj.path
173
- raise "No size specified" unless obj.size
174
- obj.create_disk
175
- elsif obj.destroy
176
- raise "No disk specified" if argv.empty?
177
- exit unless are_you_sure?(5)
178
- obj.destroy_disk(argv.first)
179
- elsif obj.unattach
180
- raise "No disk specified" if argv.empty?
181
- exit unless are_you_sure?(4)
182
- obj.unattach_disk(argv.first)
183
- elsif obj.attach
184
- raise "No disk specified" if argv.empty?
185
- obj.attach_disk(argv.first)
186
- else
187
- obj.print_disks
188
- end
189
- obj.print_footer
190
- end
191
- end
83
+ action :C, :create, "Create a disk definition"
84
+ action :D, :destroy, "Destroy a disk definition"
85
+ action :A, :attach, "Attach a disk"
86
+ action :N, :unattach, "Unattach a disk"
87
+ argv :diskname
88
+ command :disk => Rudy::Command::Disks
192
89
 
193
- option :s, :snapshot, String, "Create a backup entry from an existing snapshot"
194
- option :Z, :synchronize, "Check for and delete backup metadata with no snapshot. DOES NOT delete snapshots."
195
- #option :T, :tidy, "Tidy existing backups"
196
- option :D, :destroy, "Destroy a backup"
197
- option :C, :create, "Create a backup"
198
- usage "rudy [global options] backups [-C] [disk name]"
199
- command :backups => Rudy::Command::Disks do |obj, argv|
200
- capture(:stderr) do
201
- obj.print_header
202
- if obj.create
203
- obj.create_backup(argv.first)
204
- elsif obj.synchronize
205
- unless argv.empty?
206
- puts "The disk you specified will be ignored."
207
- argv.clear
208
- end
209
- obj.sync_backups
210
- elsif obj.destroy
211
- raise "No backup specified" if argv.empty?
212
- #exit unless are_you_sure?
213
- obj.destroy_backup(argv.first)
214
- else
215
- obj.print_backups
216
- end
217
- end
218
- end
219
90
 
91
+ usage "rudy [global options] backups [-C] [disk name]"
92
+ desc "Manage Backups"
93
+ option :s, :snapshot, String, "Create a backup entry from an existing snapshot"
94
+ action :Z, :sync, "Check for and delete backup metadata with no snapshot. DOES NOT delete snapshots."
95
+ #action :T, :tidy, "Tidy existing backups"
96
+ action :D, :destroy, "Destroy a backup and DELETE its snapshots."
97
+ action :C, :create, "Create a backup"
98
+ argv :disk
99
+ command :'backup' => Rudy::Command::Backups
100
+ command_alias :backup, :bu
220
101
 
221
- option :D, :destroy, "Destroy a volume"
222
- command :volumes => Rudy::Command::Volumes do |obj, argv|
223
- capture(:stderr) do
224
- obj.print_header
225
-
226
- if obj.destroy
227
- exit unless are_you_sure? 4
228
- obj.destroy_volume(argv.first)
229
- else
230
- obj.print_volumes
231
- end
232
- end
233
- end
234
102
 
235
- option :all, "Display all instances"
236
- option :a, :address, String, "Amazon elastic IP"
237
- option :i, :image, String, "Amazon machine image ID (ami)"
238
- option :v, :volume, String, "Amazon volume ID"
239
- option :D, :destroy, "Destroy the given instance IDs. All data will be lost!"
240
- option :S, :start, "Start an instance"
241
- usage "rudy [global options] instances [-D] [-S -i image ID] [instance ID OR group name]"
242
- command :instances => Rudy::Command::Instances do |obj, argv|
243
- capture(:stderr) do
244
- obj.print_header
245
- if obj.destroy
246
- exit unless are_you_sure?
247
- obj.destroy_instances(argv.first)
248
- elsif obj.start
249
- exit unless are_you_sure?
250
- obj.start_instance
251
- else
252
- obj.print_instances(argv.first)
253
- end
254
- obj.print_footer
255
- end
256
- end
103
+ usage "rudy [global options] metadata instance-ID"
104
+ desc "Display Rudy metadata."
105
+ command :metadata => Rudy::Command::Metadata
106
+ command_alias :metadata, :md
257
107
 
258
108
 
259
- option :a, :account, String, "Your Amazon Account Number"
260
- option :i, :image_name, String, "The name of the image"
261
- option :b, :bucket_name, String, "The name of the bucket that will store the image"
262
- option :C, :create, "Create an image"
263
- option :D, :destroy, "Deregister an image (currently _does not_ remove images files from S3)"
264
- usage "rudy images [-C -i name -b bucket -a account] [-D AMI-ID]"
265
- command :images => Rudy::Command::Images do |obj, argv|
266
- capture(:stderr) do
267
- obj.print_header
268
-
269
- if obj.create
270
- puts "Make sure the machine is clean. I don't want archive no crud!"
271
- exit unless are_you_sure?
272
- obj.create_image
273
-
274
- elsif obj.destroy
275
- obj.deregister(argv.first)
276
-
277
- else
278
- obj.print_images
279
- end
280
-
281
- end
282
- end
109
+ desc "Machine Group Status"
110
+ command :status => Rudy::Command::Machines
283
111
 
284
112
 
113
+ #desc "Update a Machine Group with the current version of Rudy"
114
+ #command :update => Rudy::Command::Machines
285
115
 
286
- command :stage => Rudy::Command::Stage do |obj|
287
- capture(:stderr) do
288
- obj.print_header
289
-
290
- raise "No SCM defined. Set RUDY_SVN_BASE or RUDY_GIT_BASE." unless obj.scm
291
-
292
- exit unless are_you_sure?
293
- obj.push_to_stage
294
- end
295
- end
296
116
 
117
+ usage "rudy [-e env] [-u user] connect [-p] [cmd]"
118
+ desc "Open an SSH connection"
119
+ option :p, :print, "Only print the SSH command, don't connect"
120
+ argv :cmd
121
+ command :connect => Rudy::Command::Environment
122
+ command_alias :connect, :ssh
297
123
 
124
+ usage "rudy [-e env] [-u user] copy [-p] -r [from path] [to path]"
125
+ desc "Copy files to or from machines. NOTE: You must use quotes when using a tilda for your remote dir ('~/')."
126
+ option :r, :remote, "Copy FROM the remote machine to the local machine"
127
+ option :p, :print, "Only print the SSH command, don't connect"
128
+ argv :from, :to
129
+ command :copy => Rudy::Command::Environment
130
+ command_alias :copy, :scp
131
+ command_alias :copy, :upload
132
+ command_alias :copy, :download
298
133
 
299
134
 
300
- option :all, "Display all security groups"
301
- option :r, :protocols, Array, "Comma-separated list of protocols. One of: tcp (default), udp, icmp"
302
- option :p, :ports, Array, "List of comma-separated ports to authorize (default: 22,80,443)"
303
- option :a, :addresses, Array, "List of comma-separated IP addresses to authorize (default: your external IP)"
304
- option :C, :create, "Create a security group"
305
- option :D, :destroy, "Destroy a security group"
306
- option :M, :modify, "Modify a security group"
307
- usage "rudy [global options] groups [-C] [-a IP addresses] [-p ports] [group name]"
308
- command :groups => Rudy::Command::Groups do |obj, argv|
309
- capture(:stderr) do
310
- obj.print_header
311
-
312
- if obj.create
313
- obj.create_group(argv.first)
314
-
315
- elsif obj.modify
316
- obj.modify_group(argv.first)
317
-
318
- elsif obj.destroy
319
- exit unless are_you_sure?
320
- obj.destroy_group(argv.first)
321
-
322
- else
323
- obj.print_groups(argv.first)
324
- end
325
- end
326
- end
135
+ # -------------------------------- RUDY ROUTINES COMMANDS --------
136
+ # ------------------------------------------------------------------
327
137
 
138
+ desc "Shutdown a Machine Group"
139
+ command :shutdown => Rudy::Command::Machines
328
140
 
329
- __END__
141
+ desc "Start a Machine Group"
142
+ command :startup => Rudy::Command::Machines
143
+ command_alias :startup, :start
330
144
 
331
- # Create the SimpleDB domain for storing metadata. Check environment variables.
332
- rudy init
333
- rudy create-group -a address -i ami-1111111 -v vol-2222222
145
+ desc "Restart a Machine Group"
146
+ command :restart => Rudy::Command::Machines
334
147
 
335
- # Connect to stage-app-01 as root
336
- rudy --user root connect
148
+ desc "Release to a machine group"
149
+ option :s, :switch, "Switch to the release branch/tag"
150
+ option :m, :msg, String, "A short release note"
151
+ command :release => Rudy::Command::Release
337
152
 
338
- # Zone, environment, role and position are implied.
339
- rudy disks -C -p /rilli/app -d /dev/sdh -s 100
340
- rudy disks -D disk-us-east-1b-stage-app-01-rilli-db222
153
+ desc "Update the release currently running in a machine group"
154
+ command :rerelease => Rudy::Command::Release
155
+ command_alias :rerelease, :rere
341
156
 
342
- # Create an image from an existing instance
343
- rudy -e prod images -C -b rilli-ami-us -i rilli-app-32-r3
157
+ #desc "Deploy disk snapshots from one machine to another"
158
+ #command :deploy => Rudy::Command::Deploy
344
159
 
345
- # Create an instance
346
- rudy -e prod instances -S -i ami-11111
347
160
 
348
161
 
349
- rudy release
350
162
 
163
+ # ------------------------------------------- UGLY STUFFS --------
164
+ # ------------------------------------------------------------------
165
+ debug :on
166
+ capture :stderr
167
+ before do
168
+ @start = Time.now
169
+ end
170
+ after do
171
+ @elapsed = Time.now - @start
172
+ puts $/, "Elapsed: %.2f seconds" % @elapsed.to_f if @elapsed > 0.1
173
+ end
351
174