Flucti-flucti-cli 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE +7 -0
  2. data/README.mdown +24 -0
  3. data/Rakefile +56 -0
  4. data/TODO.txt +19 -0
  5. data/bin/flucti +4 -0
  6. data/flucti-cli.gemspec +46 -0
  7. data/lib/flucti.rb +20 -0
  8. data/lib/flucti/api_access.rb +56 -0
  9. data/lib/flucti/cli.rb +107 -0
  10. data/lib/flucti/parameters.rb +37 -0
  11. data/lib/flucti/resources.rb +41 -0
  12. data/lib/flucti/resources/account.rb +7 -0
  13. data/lib/flucti/resources/app_type.rb +18 -0
  14. data/lib/flucti/resources/backend.rb +28 -0
  15. data/lib/flucti/resources/basic_resource.rb +16 -0
  16. data/lib/flucti/resources/container.rb +15 -0
  17. data/lib/flucti/resources/db_server.rb +38 -0
  18. data/lib/flucti/resources/domain.rb +8 -0
  19. data/lib/flucti/resources/general.rb +14 -0
  20. data/lib/flucti/resources/mail_client.rb +7 -0
  21. data/lib/flucti/resources/mail_server.rb +7 -0
  22. data/lib/flucti/resources/port_forwarding.rb +15 -0
  23. data/lib/flucti/resources/port_forwarding/services +13921 -0
  24. data/lib/flucti/resources/ssh_details.rb +96 -0
  25. data/lib/flucti/resources/webserver.rb +9 -0
  26. data/lib/flucti/resources/website.rb +9 -0
  27. data/lib/flucti/tasks.rb +3 -0
  28. data/lib/flucti/tasks/apikey_tasks.rb +57 -0
  29. data/lib/flucti/tasks/apptype_tasks.rb +264 -0
  30. data/lib/flucti/tasks/connect_pack.rb +161 -0
  31. data/lib/flucti/tasks/db_tasks.rb +158 -0
  32. data/lib/flucti/tasks/mail_tasks.rb +104 -0
  33. data/lib/flucti/tasks/miscellaneous_tasks.rb +6 -0
  34. data/lib/flucti/tasks/progress_tasks.rb +24 -0
  35. data/lib/flucti/tasks/sshkey_tasks.rb +151 -0
  36. data/lib/flucti/tasks/vps/firewall_tasks.rb +84 -0
  37. data/lib/flucti/tasks/vps_tasks.rb +37 -0
  38. data/lib/flucti/tasks/webserver_tasks.rb +154 -0
  39. data/lib/flucti/tasks/website/apptype_tasks.rb +42 -0
  40. data/lib/flucti/tasks/website/backends/instances_tasks.rb +37 -0
  41. data/lib/flucti/tasks/website/backends_tasks.rb +254 -0
  42. data/lib/flucti/tasks/website/capfile_tasks.rb +41 -0
  43. data/lib/flucti/tasks/website/domains_tasks.rb +107 -0
  44. data/lib/flucti/tasks/website_tasks.rb +221 -0
  45. data/lib/flucti/utilities.rb +20 -0
  46. data/lib/flucti/utilities/connection_error_handling.rb +50 -0
  47. data/lib/flucti/utilities/core_ext.rb +35 -0
  48. data/lib/flucti/utilities/list_displayer.rb +57 -0
  49. data/lib/flucti/utilities/miscellaneous.rb +65 -0
  50. data/lib/flucti/utilities/progress_bar.rb +17 -0
  51. data/lib/flucti/utilities/table.rb +25 -0
  52. data/lib/flucti/utilities/task_packing.rb +10 -0
  53. data/lib/flucti/utilities/user_interface.rb +117 -0
  54. data/lib/vendor/ruby-progressbar-0.9/lib/ChangeLog +113 -0
  55. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.en.rd +103 -0
  56. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.ja.rd +100 -0
  57. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.rb +236 -0
  58. data/lib/vendor/ruby-progressbar-0.9/lib/test.rb +105 -0
  59. data/test/flucti/resources_test.rb +32 -0
  60. data/test/flucti/tasks_test.rb +28 -0
  61. data/test/flucti/utilities/miscellaneous_test.rb +54 -0
  62. data/test/flucti/utilities/table_test.rb +28 -0
  63. data/test/flucti/utilities/user_interface_test.rb +161 -0
  64. data/test/test_helper.rb +5 -0
  65. metadata +221 -0
@@ -0,0 +1,96 @@
1
+ module Flucti
2
+ module Resources
3
+ class SshDetails < BasicResource
4
+ def as(login)
5
+ AccountBinding.new(self, login)
6
+ end
7
+
8
+ def connection
9
+ as(login)
10
+ end
11
+
12
+ private
13
+
14
+ class AccountBinding < Struct.new(:details, :login)
15
+ delegate :host, :port, :to => :details
16
+
17
+ def connection_command
18
+ "ssh -p #{port} #{login}@#{host}"
19
+ end
20
+
21
+ def connect
22
+ check_for_presence_of_ssh do
23
+ command = connection_command
24
+ $stderr.puts "Connecting to VPS with: `#{command}'"
25
+ exec(command)
26
+ end
27
+ end
28
+
29
+ def execute_command(command)
30
+ %(#{connection_command} 'echo "#{command.gsub /"/, '\"'}" | bash -l -s')
31
+ end
32
+
33
+ def execute(command)
34
+ check_for_presence_of_ssh do
35
+ command = execute_command(command)
36
+ $stderr.puts "Executing: #{command}"
37
+
38
+ # For some reason, `exec' won't work, it throws the following error:
39
+ # `Operation not supported' (Errno::E045). `system' causes this too
40
+ # sometimes, but so much less often that it's bearable.
41
+ system(command)
42
+ end
43
+ end
44
+ alias run :execute
45
+
46
+ def upload(*files)
47
+ check_for_presence_of_scp do
48
+ command = %(scp -P #{port} #{files * ' '} #{login}@#{host}:~)
49
+ $stderr.puts "Executing: #{command}"
50
+ system(command)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def check_for_presence_of_ssh
57
+ if has_command? 'ssh'
58
+ yield
59
+ else
60
+ suggest_windows_alternative "ssh", "PuTTY", "http://www.chiark.greenend.org.uk/~sgtatham/putty"
61
+ end
62
+ end
63
+
64
+ def check_for_presence_of_scp
65
+ if has_command? 'scp'
66
+ yield
67
+ else
68
+ suggest_windows_alternative "scp", "WinSCP", "http://winscp.net"
69
+ end
70
+ end
71
+
72
+ def has_command?(command)
73
+ case RUBY_PLATFORM
74
+ when /mswin/
75
+ false
76
+ else
77
+ system("which ssh > /dev/null 2>&1")
78
+ end
79
+ end
80
+
81
+ def suggest_windows_alternative(missing, alternative, url)
82
+ Utilities.error! <<-MSG
83
+ The necessary `#{missing}' utility seems missing on your system. You
84
+ should connect to the VPS manually with #{alternative} (for Windows,
85
+ available at #{url}) using the following connection details :
86
+
87
+ * Host: #{host}
88
+ * Port: #{port}
89
+ * Login: #{login}
90
+ * Password: (not needed -- specify your public SSH key instead)
91
+ MSG
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,9 @@
1
+ module Flucti
2
+ module Resources
3
+ class Webserver < BasicResource
4
+ belongs_to :container
5
+ has_many :backends
6
+ has_one :ssh_details
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flucti
2
+ module Resources
3
+ class Website < BasicResource
4
+ self.attribute_for_to_s = :name
5
+ has_many :domains
6
+ has_many :backends
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ Dir.glob(File.dirname(__FILE__) + "/tasks/**/*_tasks.rb") do |task_file|
2
+ load(task_file)
3
+ end
@@ -0,0 +1,57 @@
1
+ namespace :apikey do
2
+ desc <<-DESC
3
+ Reset your API key and generate a new one. The new key will be sent to you
4
+ by e-mail.
5
+ DESC
6
+ task :reset do
7
+ APIKey.put
8
+
9
+ puts_title "API key reset"
10
+ puts_long <<-MSG
11
+ Your API key has been reset. You will receive an e-mail with your new
12
+ API key shortly. After that, you will need to reconfigure this utility
13
+ to use the new key, by running #{qcommand "apikey:switch"}.
14
+ MSG
15
+ end
16
+
17
+ desc <<-DESC
18
+ Change the current API key. You should use this task after #{qcommand "apikey:reset"}
19
+ to use a newly generated API key.
20
+ DESC
21
+ task :switch do
22
+ puts_title "API key"
23
+ puts_long <<-MSG
24
+ In order to use this utility, you must provide your API key that must
25
+ have been sent to you in an e-mail after creating your account. Please
26
+ paste it at the prompt below and press <enter>.
27
+ MSG
28
+ prompt_and_store_key
29
+ end
30
+
31
+ def prompt_and_store_key
32
+ loop do
33
+ puts
34
+ print "API key: "
35
+ entered = $stdin.gets.strip
36
+ begin
37
+ APIKey.site = APIAccess.site_with_api_key(entered)
38
+ begin
39
+ APIKey.get
40
+ ensure
41
+ APIKey.site = nil
42
+ end
43
+ rescue WebService::UnauthorizedAccess
44
+ puts
45
+ puts_long <<-MSG
46
+ The key you entered apears to be invalid. Please double check it
47
+ and retry, or let us help you: #{SUPPORT_EMAIL_ADDR}.
48
+ MSG
49
+ else
50
+ Parameters.store(:api_key, entered)
51
+ puts
52
+ puts "* Key valid and saved."
53
+ break
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,264 @@
1
+ namespace :apptype do
2
+ task(:default) { list }
3
+
4
+ desc <<-DESC
5
+ Display a list of all the stock application types.
6
+
7
+ These are only suggestions that a website can be automatically configured
8
+ for running. Of course, you can configure your VPS's to run any kind of
9
+ site you want, either by customizing existing types (see #{qcommand \
10
+ 'apptype:import'} and #{qcommand 'apptype:push'}), or by configuring
11
+ everything manually.
12
+ DESC
13
+ task :list do
14
+ puts_title "Available application types"
15
+ puts_list AppType.all, :id => :short, :table => true do |t|
16
+ t.col("Name", :name)
17
+ t.col("Stock?") { |t| '*' if t.stock? }
18
+ end
19
+ end
20
+
21
+ desc <<-DESC
22
+ Display brief details about an application type.
23
+ DESC
24
+ task :show do
25
+ id = require_id!
26
+ display(AppType.find(id))
27
+ end
28
+
29
+ desc <<-DESC
30
+ Import files from an existing type into the current working directory.
31
+
32
+ This is usually the first step in creating a custom type. It's usually
33
+ followed by the customisation of the imported files, and finalised with a
34
+ #{qcommand 'apptype:push'}.
35
+ DESC
36
+ task :import do
37
+ id = require_id!
38
+
39
+ type = AppType.find(id)
40
+ puts_title "Importing type #{q type}"
41
+
42
+ # (utilities)
43
+ secure_write = lambda do |path, contents|
44
+ path = path.gsub(/\{(.*?)\}/) { $1.split(',').first }
45
+ if File.exist?(path) || File.exist?(path.downcase)
46
+ $stderr.puts "Warning: skipping #{q path} as it already exists"
47
+ else
48
+ puts "Writing #{q path}"
49
+ File.open(path, 'w') { |f| f << contents }
50
+ end
51
+ end
52
+
53
+ # Files
54
+ file_map_for_type.each do |attribute, glob|
55
+ secure_write[glob, type.send(attribute)]
56
+ end
57
+
58
+ # Metadata
59
+ filter_out = ['id', 'stock?', *file_map_for_type.stringify_keys.keys]
60
+ data =
61
+ type.attributes.
62
+ stringify_keys.
63
+ except(*filter_out).
64
+ merge('name' => name_of_current, 'short' => name_of_current)
65
+ secure_write["meta.yml", YAML.dump(data)]
66
+
67
+ # Services
68
+ type.service_configs.each do |service|
69
+ file_map_for_service.each do |attribute, glob|
70
+ path = ([service.name, glob] - %w(main)).join('_')
71
+ data = service.send(attribute)
72
+ secure_write[path, data]
73
+ end
74
+ end
75
+ end
76
+
77
+ desc <<-DESC
78
+ Create a new application type or update an existing one. The files in the
79
+ current working directory are used to build a new or updated type which is
80
+ then registered under the name of that directory.
81
+
82
+ Creating a new type usually goes somewhat like this:
83
+ $ mkdir foo-type && cd foo-type
84
+ $ #{command "apptype:import"} ID=sinatra
85
+ # ...Edit the imported files...
86
+ $ #{command "apptype:push"}
87
+ # ...Edit the files..
88
+ $ #{command "apptype:push"}
89
+
90
+ Custom types can be used like the stock ones. For a new site:
91
+ $ #{command "website:declare"} TYPE=foo
92
+
93
+ Or, for an existing site:
94
+ $ cd path/to/some-site
95
+ $ #{command "website:apptype:switch"} ID=foo
96
+ $ #{command "website:backends:reprepare"}
97
+
98
+ To update a type:
99
+ $ cd foo-type
100
+ # ...Edit the files...
101
+ $ #{command "apptype:push"}
102
+ # ...For each website of that type...
103
+ $ #{command "website:backends:reprepare"}
104
+ DESC
105
+ task :push do
106
+ if type = AppType[name_of_current]
107
+ update type
108
+ else
109
+ create type
110
+ end
111
+ end
112
+
113
+ desc <<-DESC
114
+ Delete an existing type. All website of this type must have been deleted
115
+ before running this tasks.
116
+ DESC
117
+ task :delete do
118
+ id = require_id!
119
+ type = AppType.find(id)
120
+ begin
121
+ type.destroy
122
+ rescue WebService::ResourceConflict
123
+ error! $!.response.data['errors']
124
+ else
125
+ puts_title "Application type deleted"
126
+ puts "Application type #{q type} deleted successfully."
127
+ end
128
+ end
129
+
130
+ def require_id!
131
+ ENV["ID"] or
132
+ error! <<-MSG
133
+ The ID or short name of the application type to deal with must be
134
+ specified in the $ID environment variable. To list all application
135
+ types, run #{qcommand "apptype:list"}.
136
+ MSG
137
+ end
138
+
139
+ def build_type_from_cwd
140
+ type = AppType.new
141
+ dir = Pathname.pwd
142
+
143
+ # (utilities)
144
+ assign_to = lambda do |object, map|
145
+ map.each do |attribute, glob|
146
+ file = Pathname.glob(dir.join(glob)).first
147
+ object.write_attribute(attribute, file.read) if file
148
+ end
149
+ end
150
+
151
+ # Metadata
152
+ meta = dir.join("meta.yml")
153
+ type.attributes = YAML.load(meta.read) if meta.file?
154
+
155
+ # Here is not really the place to assign a default name and short name but
156
+ # since callers of this method expect these attributes to be set, I think
157
+ # it's OK.
158
+ type.name = name_of_current unless type.attribute_set?(:name)
159
+ type.short = type.name unless type.attribute_set?(:short)
160
+
161
+ # Files
162
+ assign_to[type, file_map_for_type]
163
+
164
+ # Build the main ServiceConfig:
165
+ # start.sh => main.start_script
166
+ # finish.sh => main.finish_script
167
+ if file_map_for_service.values.any? { |f| File.file? f }
168
+ main = ServiceConfig.new(:name => "main")
169
+ assign_to[main, file_map_for_service]
170
+ end
171
+
172
+ # Build other ServiceConfig's:
173
+ # foo_start.sh => foo.start_script
174
+ # foo_install.sh => foo.finish_script
175
+ other =
176
+ Pathname.glob(dir.join("*_{#{file_map_for_service.values * ','}}")).
177
+ group_by { |p| p.basename.to_s.split('_', 2).first }.
178
+ map do |name, files|
179
+ service = ServiceConfig.new(:name => name)
180
+ file_map =
181
+ file_map_for_service.inject({}) do |m, (attr, file)|
182
+ if path = files.find { |path| path.basename.to_s.split('_', 2).last == file }
183
+ m[attr] = path
184
+ end
185
+ m
186
+ end
187
+ assign_to[service, file_map]
188
+ service
189
+ end
190
+
191
+ return type, [main, *other].compact
192
+ end
193
+
194
+ def display(type)
195
+ puts "Name: #{type.name}"
196
+ puts "Short name: #{type.short}"
197
+ puts "Stock?: #{type.stock? ? 'Yes' : 'No'}"
198
+ puts "Created: #{type.created_at}"
199
+ puts "Updated: #{type.updated_at}"
200
+ puts "Detection: #{(d = type.detection) ? "test -#{d}" : '(none)'}"
201
+ puts "Install script: (#{type.install_script.to_s.length} bytes)"
202
+ puts "Webserver conf.: (#{type.webserver_conf.to_s.length} bytes)"
203
+ puts "Capfile tmpl.: (#{type.capfile.to_s.length} bytes)"
204
+ if type.service_configs.any?
205
+ puts "Services:"
206
+ type.service_configs.each do |service|
207
+ puts "- #{service}"
208
+ end
209
+ else
210
+ puts "Services: (none)"
211
+ end
212
+ end
213
+
214
+ def name_of_current
215
+ ENV["NAME"] || clean_name(Pathname.pwd.expand_path.basename.to_s.gsub(/^type-|-type$/, ""))
216
+ end
217
+
218
+ def file_map_for_type
219
+ { :root_install_script => "root_install.sh", :install_script => "install.sh",
220
+ :webserver_conf => "nginx.conf", :capfile => "{C,c}apfile" }
221
+ end
222
+
223
+ def file_map_for_service
224
+ { :start_script => "start.sh", :finish_script => "finish.sh" }
225
+ end
226
+
227
+ def create(type)
228
+ type, services = build_type_from_cwd
229
+
230
+ try_save type do
231
+ puts_title "Application type created"
232
+ puts "Creating associated service configurations..."
233
+ services.each do |service|
234
+ service.app_type = type
235
+ try_save service do
236
+ puts "- Created service #{q service}"
237
+ end
238
+ end
239
+ puts
240
+ display(type.reload)
241
+ end
242
+ end
243
+
244
+ def update(type)
245
+ updated, services = build_type_from_cwd
246
+ updated.attributes.except('id').each do |attribute, value|
247
+ type.write_attribute(attribute, value)
248
+ end
249
+
250
+ try_save type do
251
+ puts_title "Application type updated"
252
+ puts "Updating associated service configurations..."
253
+ type.service_configs.each &:destroy
254
+ services.each do |service|
255
+ service.app_type = type
256
+ try_save service do
257
+ puts "- Updated service #{q service}"
258
+ end
259
+ end
260
+ puts
261
+ display(type.reload)
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,161 @@
1
+ desc <<-DESC
2
+ Open a new shell in the specified #{resource_type}'s UNIX account.
3
+ For password-less access, be sure to run #{qcommand "sshkey:register"}
4
+ first.
5
+
6
+ Environment variables:
7
+ (optional) $ID: the ID of the #{resource_type} to connect to.
8
+ (optional) $AS: the login of the UNIX account to connect as.
9
+ DESC
10
+ namespace :connect do
11
+ task :default do
12
+ resource = fetch_current
13
+ ssh_details_for(resource).connect
14
+ end
15
+
16
+ desc <<-DESC
17
+ Display SSH connection details. You can then use these details to connect
18
+ to the #{resource_type} either from the command line (using the standard
19
+ `ssh', `scp' or `sftp' tool suite), programmatically using Net::SSH, or
20
+ graphically using WinSCP or Cyberduck.
21
+
22
+ Environment variables:
23
+ (optional) $ID: the ID of the #{resource_type} to get the
24
+ connection details for.
25
+ DESC
26
+ task :details do
27
+ resource = fetch_current
28
+ details = ssh_details_for(resource)
29
+
30
+ puts_long <<-EOS
31
+ SSH connection details for #{resource_type} #{resource}:
32
+ * Host: #{details.host}
33
+ * Port: #{details.port}
34
+ * Login: #{details.login}
35
+
36
+ Remote command execution:
37
+ $ ssh -p #{details.port} #{details.login}@#{details.host} <command>
38
+
39
+ File upload:
40
+ $ scp -P #{details.port} #{details.login}@#{details.host} <local-path> <remote-path>
41
+ EOS
42
+ end
43
+
44
+ # Allow for defining a method named `command'.
45
+ class << self
46
+ undef_method :command if method_defined? :command
47
+ end
48
+
49
+ desc <<-DESC
50
+ Output an SSH connection command, ready to run.
51
+
52
+ Environment variables:
53
+ (optional) $ID: the ID of the #{resource_type} to get the connection
54
+ command for.
55
+ DESC
56
+ task :command do
57
+ resource = fetch_current
58
+ puts ssh_details_for(resource).connection_command
59
+ end
60
+
61
+ def ssh_details_for(resource)
62
+ details = resource.ssh_details
63
+ details.login = ENV["AS"] || 'root' unless details.attribute_set? :login
64
+
65
+ # Return `details.connection' instead of `details' so that methods
66
+ # of both SshDetails and SshDetails::AccountBinding are made
67
+ # available from the same object.
68
+ details.connection
69
+ rescue WebService::NotAcceptable
70
+ vps = resource.attribute_set?(:vps) ? resource.vps : resource
71
+ reference = vps == resource ? "VPS" : "#{resource_type}'s VPS"
72
+ error! <<-MSG
73
+ The SSH port of the #{reference} (#{q vps}) is not accessible
74
+ from the Internet. You need to open it in order to be able
75
+ to connect to the #{resource_type}'s UNIX account. To do so,
76
+ run #{qcommand "vps:firewall:open PORT=22"}.
77
+ MSG
78
+ end
79
+ end
80
+
81
+ desc "Alias for task `connect'."
82
+ task(:enter) { connect.default }
83
+
84
+ desc <<-DESC
85
+ Run a command in the #{resource_type} environment. Default working
86
+ directory is the home directory.
87
+
88
+ Environment variables:
89
+ (mandatory) $CMD: the command to run.
90
+
91
+ (optional) $ID: the ID of the #{resource_type} to run the command
92
+ in.
93
+ DESC
94
+ task :run do
95
+ cmd = ENV['CMD'] or error! <<-MSG
96
+ The command to execute must be specified in the $CMD environment
97
+ variable.
98
+ MSG
99
+ resource = fetch_current
100
+ connect.ssh_details_for(resource).run(cmd)
101
+ end
102
+
103
+ desc <<-DESC
104
+ Upload a local file to the home directory of the #{resource_type}.
105
+
106
+ Environment variables:
107
+ (mandatory) $FILE: the path to the file to upload.
108
+
109
+ (optional) $ID: the ID of the #{resource_type} to upload the
110
+ file to.
111
+ DESC
112
+ task :upload do
113
+ files = ENV['FILE'] || ENV['FILES'] or error! <<-MSG
114
+ The file to upload must be specified in the $FILE environment
115
+ variable.
116
+ MSG
117
+ resource = fetch_current
118
+ connect.ssh_details_for(resource).upload(*Dir[files])
119
+ end
120
+
121
+ namespace :rubygems do
122
+ desc <<-DESC
123
+ Install or update RubyGems (latest version) on the UNIX account of the
124
+ specified #{resource_type}. Ruby will be installed if necessary (latest
125
+ 1.8).
126
+
127
+ Once you have done that, you can connect to the #{resource_type}'s
128
+ UNIX account and start installing gems. For example:
129
+ $ #{command "db:server:rubygems:install"}
130
+ $ #{command "progress"}
131
+ $ #{command "db:server:enter"}
132
+ mysql@filiberto ~ $ gem install rails
133
+
134
+ Environment variables:
135
+ (optional) $ID: the ID of the #{resource_type} to install RubyGems onto.
136
+ Default: the last #{resource_type}.
137
+ DESC
138
+ task :install do
139
+ resource = fetch_current
140
+
141
+ resource.post(:rubygems)
142
+
143
+ puts_title "Request sent"
144
+ puts "RubyGems (latest version) has been scheduled for installation or update on #{resource_type} #{resource}."
145
+ end
146
+
147
+ desc "Alias for `install'."
148
+ task(:setup) { install }
149
+
150
+ desc <<-DESC
151
+ Update RubyGems to the latest version. Only the package management
152
+ system is updated, not the gems themselves.
153
+
154
+ Environment variables:
155
+ (optional) $ID: the ID of the #{resource_type} to update RubyGems on.
156
+ Default: the last #{resource_type}.
157
+ DESC
158
+ task(:update) do
159
+ install
160
+ end
161
+ end