azuki 0.0.1

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +71 -0
  3. data/bin/azuki +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/azuki.rb +17 -0
  6. data/lib/azuki/auth.rb +339 -0
  7. data/lib/azuki/cli.rb +38 -0
  8. data/lib/azuki/client.rb +764 -0
  9. data/lib/azuki/client/azuki_postgresql.rb +141 -0
  10. data/lib/azuki/client/cisaurus.rb +26 -0
  11. data/lib/azuki/client/pgbackups.rb +113 -0
  12. data/lib/azuki/client/rendezvous.rb +108 -0
  13. data/lib/azuki/client/ssl_endpoint.rb +25 -0
  14. data/lib/azuki/command.rb +294 -0
  15. data/lib/azuki/command/account.rb +23 -0
  16. data/lib/azuki/command/accounts.rb +34 -0
  17. data/lib/azuki/command/addons.rb +305 -0
  18. data/lib/azuki/command/apps.rb +393 -0
  19. data/lib/azuki/command/auth.rb +86 -0
  20. data/lib/azuki/command/base.rb +230 -0
  21. data/lib/azuki/command/certs.rb +209 -0
  22. data/lib/azuki/command/config.rb +137 -0
  23. data/lib/azuki/command/db.rb +218 -0
  24. data/lib/azuki/command/domains.rb +85 -0
  25. data/lib/azuki/command/drains.rb +46 -0
  26. data/lib/azuki/command/fork.rb +164 -0
  27. data/lib/azuki/command/git.rb +64 -0
  28. data/lib/azuki/command/help.rb +179 -0
  29. data/lib/azuki/command/keys.rb +115 -0
  30. data/lib/azuki/command/labs.rb +147 -0
  31. data/lib/azuki/command/logs.rb +45 -0
  32. data/lib/azuki/command/maintenance.rb +61 -0
  33. data/lib/azuki/command/pg.rb +269 -0
  34. data/lib/azuki/command/pgbackups.rb +329 -0
  35. data/lib/azuki/command/plugins.rb +110 -0
  36. data/lib/azuki/command/ps.rb +232 -0
  37. data/lib/azuki/command/regions.rb +22 -0
  38. data/lib/azuki/command/releases.rb +124 -0
  39. data/lib/azuki/command/run.rb +180 -0
  40. data/lib/azuki/command/sharing.rb +89 -0
  41. data/lib/azuki/command/ssl.rb +43 -0
  42. data/lib/azuki/command/stack.rb +62 -0
  43. data/lib/azuki/command/status.rb +51 -0
  44. data/lib/azuki/command/update.rb +47 -0
  45. data/lib/azuki/command/version.rb +23 -0
  46. data/lib/azuki/deprecated.rb +5 -0
  47. data/lib/azuki/deprecated/help.rb +38 -0
  48. data/lib/azuki/distribution.rb +9 -0
  49. data/lib/azuki/excon.rb +9 -0
  50. data/lib/azuki/helpers.rb +517 -0
  51. data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
  52. data/lib/azuki/helpers/log_displayer.rb +70 -0
  53. data/lib/azuki/plugin.rb +163 -0
  54. data/lib/azuki/updater.rb +171 -0
  55. data/lib/azuki/version.rb +3 -0
  56. data/lib/vendor/azuki/okjson.rb +598 -0
  57. data/spec/azuki/auth_spec.rb +256 -0
  58. data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
  59. data/spec/azuki/client/pgbackups_spec.rb +43 -0
  60. data/spec/azuki/client/rendezvous_spec.rb +62 -0
  61. data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
  62. data/spec/azuki/client_spec.rb +564 -0
  63. data/spec/azuki/command/addons_spec.rb +601 -0
  64. data/spec/azuki/command/apps_spec.rb +351 -0
  65. data/spec/azuki/command/auth_spec.rb +38 -0
  66. data/spec/azuki/command/base_spec.rb +109 -0
  67. data/spec/azuki/command/certs_spec.rb +178 -0
  68. data/spec/azuki/command/config_spec.rb +144 -0
  69. data/spec/azuki/command/db_spec.rb +110 -0
  70. data/spec/azuki/command/domains_spec.rb +87 -0
  71. data/spec/azuki/command/drains_spec.rb +34 -0
  72. data/spec/azuki/command/fork_spec.rb +56 -0
  73. data/spec/azuki/command/git_spec.rb +144 -0
  74. data/spec/azuki/command/help_spec.rb +93 -0
  75. data/spec/azuki/command/keys_spec.rb +120 -0
  76. data/spec/azuki/command/labs_spec.rb +100 -0
  77. data/spec/azuki/command/logs_spec.rb +60 -0
  78. data/spec/azuki/command/maintenance_spec.rb +51 -0
  79. data/spec/azuki/command/pg_spec.rb +236 -0
  80. data/spec/azuki/command/pgbackups_spec.rb +307 -0
  81. data/spec/azuki/command/plugins_spec.rb +104 -0
  82. data/spec/azuki/command/ps_spec.rb +195 -0
  83. data/spec/azuki/command/releases_spec.rb +130 -0
  84. data/spec/azuki/command/run_spec.rb +83 -0
  85. data/spec/azuki/command/sharing_spec.rb +59 -0
  86. data/spec/azuki/command/stack_spec.rb +46 -0
  87. data/spec/azuki/command/status_spec.rb +48 -0
  88. data/spec/azuki/command/version_spec.rb +16 -0
  89. data/spec/azuki/command_spec.rb +211 -0
  90. data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
  91. data/spec/azuki/helpers_spec.rb +48 -0
  92. data/spec/azuki/plugin_spec.rb +172 -0
  93. data/spec/azuki/updater_spec.rb +44 -0
  94. data/spec/helper/legacy_help.rb +16 -0
  95. data/spec/spec.opts +1 -0
  96. data/spec/spec_helper.rb +224 -0
  97. data/spec/support/display_message_matcher.rb +49 -0
  98. data/spec/support/openssl_mock_helper.rb +8 -0
  99. metadata +211 -0
@@ -0,0 +1,329 @@
1
+ require "azuki/client/pgbackups"
2
+ require "azuki/command/base"
3
+ require "azuki/helpers/azuki_postgresql"
4
+
5
+ module Azuki::Command
6
+
7
+ # manage backups of azuki postgresql databases
8
+ class Pgbackups < Base
9
+
10
+ include Azuki::Helpers::AzukiPostgresql
11
+
12
+ # pgbackups
13
+ #
14
+ # list captured backups
15
+ #
16
+ def index
17
+ validate_arguments!
18
+
19
+ backups = []
20
+ pgbackup_client.get_transfers.each { |t|
21
+ next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at']
22
+ backups << {
23
+ 'id' => backup_name(t['to_url']),
24
+ 'created_at' => t['created_at'],
25
+ 'status' => transfer_status(t),
26
+ 'size' => t['size'],
27
+ 'database' => t['from_name']
28
+ }
29
+ }
30
+
31
+ if backups.empty?
32
+ no_backups_error!
33
+ else
34
+ display_table(
35
+ backups,
36
+ %w{ id created_at status size database },
37
+ ["ID", "Backup Time", "Status", "Size", "Database"]
38
+ )
39
+ end
40
+ end
41
+
42
+ # pgbackups:url [BACKUP_ID]
43
+ #
44
+ # get a temporary URL for a backup
45
+ #
46
+ def url
47
+ name = shift_argument
48
+ validate_arguments!
49
+
50
+ if name
51
+ b = pgbackup_client.get_backup(name)
52
+ else
53
+ b = pgbackup_client.get_latest_backup
54
+ end
55
+ unless b['public_url']
56
+ error("No backup found.")
57
+ end
58
+ if $stdout.isatty
59
+ display '"'+b['public_url']+'"'
60
+ else
61
+ display b['public_url']
62
+ end
63
+ end
64
+
65
+ # pgbackups:capture [DATABASE]
66
+ #
67
+ # capture a backup from a database id
68
+ #
69
+ # if no DATABASE is specified, defaults to DATABASE_URL
70
+ #
71
+ # -e, --expire # if no slots are available, destroy the oldest manual backup to make room
72
+ #
73
+ def capture
74
+ attachment = hpg_resolve(shift_argument, "DATABASE_URL")
75
+ validate_arguments!
76
+
77
+ from_name = attachment.display_name
78
+ from_url = attachment.url
79
+ to_url = nil # server will assign
80
+ to_name = "BACKUP"
81
+
82
+ opts = {:expire => options[:expire]}
83
+
84
+ backup = transfer!(from_url, from_name, to_url, to_name, opts)
85
+
86
+ to_uri = URI.parse backup["to_url"]
87
+ backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*')
88
+ display "\n#{from_name} ----backup---> #{backup_id}"
89
+
90
+ backup = poll_transfer!(backup)
91
+
92
+ if backup["error_at"]
93
+ message = "An error occurred and your backup did not finish."
94
+ message += "\nPlease run `azuki logs --ps pgbackups` for details."
95
+ message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/
96
+ message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/
97
+ error(message)
98
+ end
99
+ end
100
+
101
+ # pgbackups:restore [<DATABASE> [BACKUP_ID|BACKUP_URL]]
102
+ #
103
+ # restore a backup to a database
104
+ #
105
+ # if no DATABASE is specified, defaults to DATABASE_URL and latest backup
106
+ # if DATABASE is specified, but no BACKUP_ID, defaults to latest backup
107
+ #
108
+ def restore
109
+ if 0 == args.size
110
+ attachment = hpg_resolve(nil, "DATABASE_URL")
111
+ to_name = attachment.display_name
112
+ to_url = attachment.url
113
+ backup_id = :latest
114
+ elsif 1 == args.size
115
+ attachment = hpg_resolve(shift_argument)
116
+ to_name = attachment.display_name
117
+ to_url = attachment.url
118
+ backup_id = :latest
119
+ else
120
+ attachment = hpg_resolve(shift_argument)
121
+ to_name = attachment.display_name
122
+ to_url = attachment.url
123
+ backup_id = shift_argument
124
+ end
125
+
126
+ if :latest == backup_id
127
+ backup = pgbackup_client.get_latest_backup
128
+ no_backups_error! if {} == backup
129
+ to_uri = URI.parse backup["to_url"]
130
+ backup_id = File.basename(to_uri.path, '.*')
131
+ backup_id = "#{backup_id} (most recent)"
132
+ from_url = backup["to_url"]
133
+ from_name = "BACKUP"
134
+ elsif backup_id =~ /^http(s?):\/\//
135
+ from_url = backup_id
136
+ from_name = "EXTERNAL_BACKUP"
137
+ from_uri = URI.parse backup_id
138
+ backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path)
139
+ else
140
+ backup = pgbackup_client.get_backup(backup_id)
141
+ abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"]
142
+
143
+ from_url = backup["to_url"]
144
+ from_name = "BACKUP"
145
+ end
146
+
147
+ message = "#{to_name} <---restore--- "
148
+ padding = " " * message.length
149
+ display "\n#{message}#{backup_id}"
150
+ if backup
151
+ display padding + "#{backup['from_name']}"
152
+ display padding + "#{backup['created_at']}"
153
+ display padding + "#{backup['size']}"
154
+ end
155
+
156
+ if confirm_command
157
+ restore = transfer!(from_url, from_name, to_url, to_name)
158
+ restore = poll_transfer!(restore)
159
+
160
+ if restore["error_at"]
161
+ message = "An error occurred and your restore did not finish."
162
+ if restore['log'] =~ /Invalid dump format: .*: XML document text/
163
+ message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL."
164
+ else
165
+ message += "\nPlease run `azuki logs --ps pgbackups` for details."
166
+ end
167
+ error(message)
168
+ end
169
+ end
170
+ end
171
+
172
+ # pgbackups:destroy BACKUP_ID
173
+ #
174
+ # destroys a backup
175
+ #
176
+ def destroy
177
+ unless name = shift_argument
178
+ error("Usage: azuki pgbackups:destroy BACKUP_ID\nMust specify BACKUP_ID to destroy.")
179
+ end
180
+ backup = pgbackup_client.get_backup(name)
181
+ if backup["destroyed_at"]
182
+ error("Backup #{name} already destroyed.")
183
+ end
184
+
185
+ action("Destroying #{name}") do
186
+ pgbackup_client.delete_backup(name)
187
+ end
188
+ end
189
+
190
+ protected
191
+
192
+ def transfer_status(t)
193
+ if t['finished_at']
194
+ "Finished @ #{t["finished_at"]}"
195
+ elsif t['started_at']
196
+ step = t['progress'] && t['progress'].split[0]
197
+ step.nil? ? 'Unknown' : step_map[step]
198
+ else
199
+ "Unknown"
200
+ end
201
+ end
202
+
203
+ def config_vars
204
+ @config_vars ||= api.get_config_vars(app).body
205
+ end
206
+
207
+ def pgbackup_client
208
+ pgbackups_url = ENV["PGBACKUPS_URL"] || config_vars["PGBACKUPS_URL"]
209
+ error("Please add the pgbackups addon first via:\nazuki addons:add pgbackups") unless pgbackups_url
210
+ @pgbackup_client ||= Azuki::Client::Pgbackups.new(pgbackups_url)
211
+ end
212
+
213
+ def backup_name(to_url)
214
+ # translate s3://bucket/email/foo/bar.dump => foo/bar
215
+ parts = to_url.split('/')
216
+ parts.slice(4..-1).join('/').gsub(/\.dump$/, '')
217
+ end
218
+
219
+ def transfer!(from_url, from_name, to_url, to_name, opts={})
220
+ pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts)
221
+ end
222
+
223
+ def poll_error(app)
224
+ error <<-EOM
225
+ Failed to query the PGBackups status API. Your backup may still be running.
226
+ Verify the status of your backup with `azuki pgbackups -a #{app}`
227
+ You can also watch progress with `azuki logs --tail --ps pgbackups -a #{app}`
228
+ EOM
229
+ end
230
+
231
+ def poll_transfer!(transfer)
232
+ display "\n"
233
+
234
+ if transfer["errors"]
235
+ transfer["errors"].values.flatten.each { |e|
236
+ output_with_bang "#{e}"
237
+ }
238
+ abort
239
+ end
240
+
241
+ while true
242
+ update_display(transfer)
243
+ break if transfer["finished_at"]
244
+
245
+ sleep_time = 1
246
+ begin
247
+ sleep(sleep_time)
248
+ transfer = pgbackup_client.get_transfer(transfer["id"])
249
+ rescue
250
+ if sleep_time > 300
251
+ poll_error(app)
252
+ else
253
+ sleep_time *= 2
254
+ retry
255
+ end
256
+ end
257
+ end
258
+
259
+ display "\n"
260
+
261
+ return transfer
262
+ end
263
+
264
+ def step_map
265
+ @step_map ||= {
266
+ "dump" => "Capturing",
267
+ "upload" => "Storing",
268
+ "download" => "Retrieving",
269
+ "restore" => "Restoring",
270
+ "gunzip" => "Uncompressing",
271
+ "load" => "Restoring",
272
+ }
273
+ end
274
+
275
+ def update_display(transfer)
276
+ @ticks ||= 0
277
+ @last_updated_at ||= 0
278
+ @last_logs ||= []
279
+ @last_progress ||= ["", 0]
280
+
281
+ @ticks += 1
282
+
283
+ if !transfer["log"]
284
+ @last_progress = ['pending', nil]
285
+ redisplay "Pending... #{spinner(@ticks)}"
286
+ else
287
+ logs = transfer["log"].split("\n")
288
+ new_logs = logs - @last_logs
289
+ @last_logs = logs
290
+
291
+ new_logs.each do |line|
292
+ matches = line.scan /^([a-z_]+)_progress:\s+([^ ]+)/
293
+ next if matches.empty?
294
+
295
+ step, amount = matches[0]
296
+
297
+ if ['done', 'error'].include? amount
298
+ # step is done, explicitly print result and newline
299
+ redisplay "#{@last_progress[0].capitalize}... #{amount}\n"
300
+ end
301
+
302
+ # store progress, last one in the logs will get displayed
303
+ step = step_map[step] || step
304
+ @last_progress = [step, amount]
305
+ end
306
+
307
+ step, amount = @last_progress
308
+ unless ['done', 'error'].include? amount
309
+ redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}"
310
+ end
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def no_backups_error!
317
+ error("No backups. Capture one with `azuki pgbackups:capture`.")
318
+ end
319
+
320
+ # lists all types of backups ('to_name' attribute)
321
+ #
322
+ # Useful when one doesn't care if a backup is of a particular
323
+ # kind, but wants to know what backups of any kind exist.
324
+ #
325
+ def backup_types
326
+ %w[BACKUP DAILY_SCHEDULED_BACKUP HOURLY_SCHEDULED_BACKUP AUTO_SCHEDULED_BACKUP]
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,110 @@
1
+ require "azuki/command/base"
2
+
3
+ module Azuki::Command
4
+
5
+ # manage plugins to the azuki gem
6
+ class Plugins < Base
7
+
8
+ # plugins
9
+ #
10
+ # list installed plugins
11
+ #
12
+ #Example:
13
+ #
14
+ # $ azuki plugins
15
+ # === Installed Plugins
16
+ # azuki-accounts
17
+ #
18
+ def index
19
+ validate_arguments!
20
+
21
+ plugins = ::Azuki::Plugin.list
22
+
23
+ if plugins.length > 0
24
+ styled_header("Installed Plugins")
25
+ styled_array(plugins)
26
+ else
27
+ display("You have no installed plugins.")
28
+ end
29
+ end
30
+
31
+ # plugins:install URL
32
+ #
33
+ # install a plugin
34
+ #
35
+ #Example:
36
+ #
37
+ # $ azuki plugins:install https://github.com/ddollar/azuki-accounts.git
38
+ # Installing azuki-accounts... done
39
+ #
40
+ def install
41
+ plugin = Azuki::Plugin.new(shift_argument)
42
+ validate_arguments!
43
+
44
+ action("Installing #{plugin.name}") do
45
+ if plugin.install
46
+ unless Azuki::Plugin.load_plugin(plugin.name)
47
+ plugin.uninstall
48
+ exit(1)
49
+ end
50
+ else
51
+ error("Could not install #{plugin.name}. Please check the URL and try again.")
52
+ end
53
+ end
54
+ end
55
+
56
+ # plugins:uninstall PLUGIN
57
+ #
58
+ # uninstall a plugin
59
+ #
60
+ #Example:
61
+ #
62
+ # $ azuki plugins:uninstall azuki-accounts
63
+ # Uninstalling azuki-accounts... done
64
+ #
65
+ def uninstall
66
+ plugin = Azuki::Plugin.new(shift_argument)
67
+ validate_arguments!
68
+
69
+ action("Uninstalling #{plugin.name}") do
70
+ plugin.uninstall
71
+ end
72
+ end
73
+
74
+ # plugins:update [PLUGIN]
75
+ #
76
+ # updates all plugins or a single plugin by name
77
+ #
78
+ #Example:
79
+ #
80
+ # $ azuki plugins:update
81
+ # Updating azuki-accounts... done
82
+ #
83
+ # $ azuki plugins:update azuki-accounts
84
+ # Updating azuki-accounts... done
85
+ #
86
+ def update
87
+ plugins = if plugin = shift_argument
88
+ [plugin]
89
+ else
90
+ ::Azuki::Plugin.list
91
+ end
92
+ validate_arguments!
93
+
94
+ plugins.each do |plugin|
95
+ begin
96
+ action("Updating #{plugin}") do
97
+ begin
98
+ Azuki::Plugin.new(plugin).update
99
+ rescue Azuki::Plugin::ErrorUpdatingSymlinkPlugin
100
+ status "skipped symlink"
101
+ end
102
+ end
103
+ rescue SystemExit
104
+ # ignore so that other plugins still update
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,232 @@
1
+ require "azuki/command/base"
2
+
3
+ # manage processes (dynos, workers)
4
+ #
5
+ class Azuki::Command::Ps < Azuki::Command::Base
6
+
7
+ # ps:dynos [QTY]
8
+ #
9
+ # DEPRECATED: use `azuki ps:scale dynos=N`
10
+ #
11
+ # scale to QTY web processes
12
+ #
13
+ # if QTY is not specified, display the number of web processes currently running
14
+ #
15
+ #Example:
16
+ #
17
+ # $ azuki ps:dynos 3
18
+ # Scaling dynos... done, now running 3
19
+ #
20
+ def dynos
21
+ # deprecation notice added to v2.21.3 on 03/16/12
22
+ display("~ `azuki ps:dynos QTY` has been deprecated and replaced with `azuki ps:scale dynos=QTY`")
23
+
24
+ dynos = shift_argument
25
+ validate_arguments!
26
+
27
+ if dynos
28
+ action("Scaling dynos") do
29
+ new_dynos = api.put_dynos(app, dynos).body["dynos"]
30
+ status("now running #{new_dynos}")
31
+ end
32
+ else
33
+ app_data = api.get_app(app).body
34
+ if app_data["stack"] == "cedar"
35
+ raise(Azuki::Command::CommandFailed, "For Cedar apps, use `azuki ps`")
36
+ else
37
+ display("#{app} is running #{quantify("dyno", app_data["dynos"])}")
38
+ end
39
+ end
40
+ end
41
+
42
+ alias_command "dynos", "ps:dynos"
43
+
44
+ # ps:workers [QTY]
45
+ #
46
+ # DEPRECATED: use `azuki ps:scale workers=N`
47
+ #
48
+ # scale to QTY background processes
49
+ #
50
+ # if QTY is not specified, display the number of background processes currently running
51
+ #
52
+ #Example:
53
+ #
54
+ # $ azuki ps:dynos 3
55
+ # Scaling workers... done, now running 3
56
+ #
57
+ def workers
58
+ # deprecation notice added to v2.21.3 on 03/16/12
59
+ display("~ `azuki ps:workers QTY` has been deprecated and replaced with `azuki ps:scale workers=QTY`")
60
+
61
+ workers = shift_argument
62
+ validate_arguments!
63
+
64
+ if workers
65
+ action("Scaling workers") do
66
+ new_workers = api.put_workers(app, workers).body["workers"]
67
+ status("now running #{new_workers}")
68
+ end
69
+ else
70
+ app_data = api.get_app(app).body
71
+ if app_data["stack"] == "cedar"
72
+ raise(Azuki::Command::CommandFailed, "For Cedar apps, use `azuki ps`")
73
+ else
74
+ display("#{app} is running #{quantify("worker", app_data["workers"])}")
75
+ end
76
+ end
77
+ end
78
+
79
+ alias_command "workers", "ps:workers"
80
+
81
+ # ps
82
+ #
83
+ # list processes for an app
84
+ #
85
+ #Example:
86
+ #
87
+ # $ azuki ps
88
+ # === run: one-off processes
89
+ # run.1: up for 5m: `bash`
90
+ #
91
+ # === web: `bundle exec thin start -p $PORT`
92
+ # web.1: created for 30s
93
+ #
94
+ def index
95
+ validate_arguments!
96
+ processes = api.get_ps(app).body
97
+
98
+ processes_by_command = Hash.new {|hash,key| hash[key] = []}
99
+ processes.each do |process|
100
+ name = process["process"].split(".").first
101
+ elapsed = time_ago(Time.now - process['elapsed'])
102
+
103
+ if name == "run"
104
+ key = "run: one-off processes"
105
+ item = "%s: %s %s: `%s`" % [ process["process"], process["state"], elapsed, process["command"] ]
106
+ else
107
+ key = "#{name}: `#{process["command"]}`"
108
+ item = "%s: %s %s" % [ process["process"], process["state"], elapsed ]
109
+ end
110
+
111
+ processes_by_command[key] << item
112
+ end
113
+
114
+ processes_by_command.keys.each do |key|
115
+ processes_by_command[key] = processes_by_command[key].sort do |x,y|
116
+ x.match(/\.(\d+):/).captures.first.to_i <=> y.match(/\.(\d+):/).captures.first.to_i
117
+ end
118
+ end
119
+
120
+ processes_by_command.keys.sort.each do |key|
121
+ styled_header(key)
122
+ styled_array(processes_by_command[key], :sort => false)
123
+ end
124
+ end
125
+
126
+ # ps:restart [PROCESS]
127
+ #
128
+ # restart an app process
129
+ #
130
+ # if PROCESS is not specified, restarts all processes on the app
131
+ #
132
+ #Examples:
133
+ #
134
+ # $ azuki ps:restart web.1
135
+ # Restarting web.1 process... done
136
+ #
137
+ # $ azuki ps:restart web
138
+ # Restarting web processes... done
139
+ #
140
+ # $ azuki ps:restart
141
+ # Restarting processes... done
142
+ #
143
+ def restart
144
+ process = shift_argument
145
+ validate_arguments!
146
+
147
+ message, options = case process
148
+ when NilClass
149
+ ["Restarting processes", {}]
150
+ when /.+\..+/
151
+ ps = args.first
152
+ ["Restarting #{ps} process", { :ps => ps }]
153
+ else
154
+ type = args.first
155
+ ["Restarting #{type} processes", { :type => type }]
156
+ end
157
+
158
+ action(message) do
159
+ api.post_ps_restart(app, options)
160
+ end
161
+ end
162
+
163
+ alias_command "restart", "ps:restart"
164
+
165
+ # ps:scale PROCESS1=AMOUNT1 [PROCESS2=AMOUNT2 ...]
166
+ #
167
+ # scale processes by the given amount
168
+ #
169
+ #Examples:
170
+ #
171
+ # $ azuki ps:scale web=3 worker+1
172
+ # Scaling web processes... done, now running 3
173
+ # Scaling worker processes... done, now running 1
174
+ #
175
+ def scale
176
+ changes = {}
177
+ args.each do |arg|
178
+ if arg =~ /^([a-zA-Z0-9_]+)([=+-]\d+)$/
179
+ changes[$1] = $2
180
+ end
181
+ end
182
+
183
+ if changes.empty?
184
+ error("Usage: azuki ps:scale PROCESS1=AMOUNT1 [PROCESS2=AMOUNT2 ...]\nMust specify PROCESS and AMOUNT to scale.")
185
+ end
186
+
187
+ changes.keys.sort.each do |process|
188
+ amount = changes[process]
189
+ action("Scaling #{process} processes") do
190
+ amount.gsub!("=", "")
191
+ new_qty = api.post_ps_scale(app, process, amount).body
192
+ status("now running #{new_qty}")
193
+ end
194
+ end
195
+ end
196
+
197
+ alias_command "scale", "ps:scale"
198
+
199
+ # ps:stop PROCESS
200
+ #
201
+ # stop an app process
202
+ #
203
+ # Examples:
204
+ #
205
+ # $ azuki stop run.3
206
+ # Stopping run.3 process... done
207
+ #
208
+ # $ azuki stop run
209
+ # Stopping run processes... done
210
+ #
211
+ def stop
212
+ process = shift_argument
213
+ validate_arguments!
214
+
215
+ message, options = case process
216
+ when NilClass
217
+ error("Usage: azuki ps:stop PROCESS\nMust specify PROCESS to stop.")
218
+ when /.+\..+/
219
+ ps = args.first
220
+ ["Stopping #{ps} process", { :ps => ps }]
221
+ else
222
+ type = args.first
223
+ ["Stopping #{type} processes", { :type => type }]
224
+ end
225
+
226
+ action(message) do
227
+ api.post_ps_stop(app, options)
228
+ end
229
+ end
230
+
231
+ alias_command "stop", "ps:stop"
232
+ end