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.
- checksums.yaml +7 -0
- data/README.md +71 -0
- data/bin/azuki +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/azuki.rb +17 -0
- data/lib/azuki/auth.rb +339 -0
- data/lib/azuki/cli.rb +38 -0
- data/lib/azuki/client.rb +764 -0
- data/lib/azuki/client/azuki_postgresql.rb +141 -0
- data/lib/azuki/client/cisaurus.rb +26 -0
- data/lib/azuki/client/pgbackups.rb +113 -0
- data/lib/azuki/client/rendezvous.rb +108 -0
- data/lib/azuki/client/ssl_endpoint.rb +25 -0
- data/lib/azuki/command.rb +294 -0
- data/lib/azuki/command/account.rb +23 -0
- data/lib/azuki/command/accounts.rb +34 -0
- data/lib/azuki/command/addons.rb +305 -0
- data/lib/azuki/command/apps.rb +393 -0
- data/lib/azuki/command/auth.rb +86 -0
- data/lib/azuki/command/base.rb +230 -0
- data/lib/azuki/command/certs.rb +209 -0
- data/lib/azuki/command/config.rb +137 -0
- data/lib/azuki/command/db.rb +218 -0
- data/lib/azuki/command/domains.rb +85 -0
- data/lib/azuki/command/drains.rb +46 -0
- data/lib/azuki/command/fork.rb +164 -0
- data/lib/azuki/command/git.rb +64 -0
- data/lib/azuki/command/help.rb +179 -0
- data/lib/azuki/command/keys.rb +115 -0
- data/lib/azuki/command/labs.rb +147 -0
- data/lib/azuki/command/logs.rb +45 -0
- data/lib/azuki/command/maintenance.rb +61 -0
- data/lib/azuki/command/pg.rb +269 -0
- data/lib/azuki/command/pgbackups.rb +329 -0
- data/lib/azuki/command/plugins.rb +110 -0
- data/lib/azuki/command/ps.rb +232 -0
- data/lib/azuki/command/regions.rb +22 -0
- data/lib/azuki/command/releases.rb +124 -0
- data/lib/azuki/command/run.rb +180 -0
- data/lib/azuki/command/sharing.rb +89 -0
- data/lib/azuki/command/ssl.rb +43 -0
- data/lib/azuki/command/stack.rb +62 -0
- data/lib/azuki/command/status.rb +51 -0
- data/lib/azuki/command/update.rb +47 -0
- data/lib/azuki/command/version.rb +23 -0
- data/lib/azuki/deprecated.rb +5 -0
- data/lib/azuki/deprecated/help.rb +38 -0
- data/lib/azuki/distribution.rb +9 -0
- data/lib/azuki/excon.rb +9 -0
- data/lib/azuki/helpers.rb +517 -0
- data/lib/azuki/helpers/azuki_postgresql.rb +165 -0
- data/lib/azuki/helpers/log_displayer.rb +70 -0
- data/lib/azuki/plugin.rb +163 -0
- data/lib/azuki/updater.rb +171 -0
- data/lib/azuki/version.rb +3 -0
- data/lib/vendor/azuki/okjson.rb +598 -0
- data/spec/azuki/auth_spec.rb +256 -0
- data/spec/azuki/client/azuki_postgresql_spec.rb +71 -0
- data/spec/azuki/client/pgbackups_spec.rb +43 -0
- data/spec/azuki/client/rendezvous_spec.rb +62 -0
- data/spec/azuki/client/ssl_endpoint_spec.rb +48 -0
- data/spec/azuki/client_spec.rb +564 -0
- data/spec/azuki/command/addons_spec.rb +601 -0
- data/spec/azuki/command/apps_spec.rb +351 -0
- data/spec/azuki/command/auth_spec.rb +38 -0
- data/spec/azuki/command/base_spec.rb +109 -0
- data/spec/azuki/command/certs_spec.rb +178 -0
- data/spec/azuki/command/config_spec.rb +144 -0
- data/spec/azuki/command/db_spec.rb +110 -0
- data/spec/azuki/command/domains_spec.rb +87 -0
- data/spec/azuki/command/drains_spec.rb +34 -0
- data/spec/azuki/command/fork_spec.rb +56 -0
- data/spec/azuki/command/git_spec.rb +144 -0
- data/spec/azuki/command/help_spec.rb +93 -0
- data/spec/azuki/command/keys_spec.rb +120 -0
- data/spec/azuki/command/labs_spec.rb +100 -0
- data/spec/azuki/command/logs_spec.rb +60 -0
- data/spec/azuki/command/maintenance_spec.rb +51 -0
- data/spec/azuki/command/pg_spec.rb +236 -0
- data/spec/azuki/command/pgbackups_spec.rb +307 -0
- data/spec/azuki/command/plugins_spec.rb +104 -0
- data/spec/azuki/command/ps_spec.rb +195 -0
- data/spec/azuki/command/releases_spec.rb +130 -0
- data/spec/azuki/command/run_spec.rb +83 -0
- data/spec/azuki/command/sharing_spec.rb +59 -0
- data/spec/azuki/command/stack_spec.rb +46 -0
- data/spec/azuki/command/status_spec.rb +48 -0
- data/spec/azuki/command/version_spec.rb +16 -0
- data/spec/azuki/command_spec.rb +211 -0
- data/spec/azuki/helpers/azuki_postgresql_spec.rb +155 -0
- data/spec/azuki/helpers_spec.rb +48 -0
- data/spec/azuki/plugin_spec.rb +172 -0
- data/spec/azuki/updater_spec.rb +44 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +224 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- 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
|