crazy-yard 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +438 -0
  4. data/bin/ey +9 -0
  5. data/lib/engineyard.rb +9 -0
  6. data/lib/engineyard/cli.rb +816 -0
  7. data/lib/engineyard/cli/api.rb +98 -0
  8. data/lib/engineyard/cli/recipes.rb +129 -0
  9. data/lib/engineyard/cli/ui.rb +275 -0
  10. data/lib/engineyard/cli/web.rb +85 -0
  11. data/lib/engineyard/config.rb +158 -0
  12. data/lib/engineyard/deploy_config.rb +65 -0
  13. data/lib/engineyard/deploy_config/ref.rb +56 -0
  14. data/lib/engineyard/error.rb +82 -0
  15. data/lib/engineyard/eyrc.rb +59 -0
  16. data/lib/engineyard/repo.rb +105 -0
  17. data/lib/engineyard/serverside_runner.rb +159 -0
  18. data/lib/engineyard/templates.rb +6 -0
  19. data/lib/engineyard/templates/ey.yml.erb +196 -0
  20. data/lib/engineyard/templates/ey_yml.rb +119 -0
  21. data/lib/engineyard/thor.rb +215 -0
  22. data/lib/engineyard/version.rb +4 -0
  23. data/lib/vendor/thor/Gemfile +15 -0
  24. data/lib/vendor/thor/LICENSE.md +20 -0
  25. data/lib/vendor/thor/README.md +35 -0
  26. data/lib/vendor/thor/lib/thor.rb +473 -0
  27. data/lib/vendor/thor/lib/thor/actions.rb +318 -0
  28. data/lib/vendor/thor/lib/thor/actions/create_file.rb +105 -0
  29. data/lib/vendor/thor/lib/thor/actions/create_link.rb +60 -0
  30. data/lib/vendor/thor/lib/thor/actions/directory.rb +119 -0
  31. data/lib/vendor/thor/lib/thor/actions/empty_directory.rb +137 -0
  32. data/lib/vendor/thor/lib/thor/actions/file_manipulation.rb +314 -0
  33. data/lib/vendor/thor/lib/thor/actions/inject_into_file.rb +109 -0
  34. data/lib/vendor/thor/lib/thor/base.rb +652 -0
  35. data/lib/vendor/thor/lib/thor/command.rb +136 -0
  36. data/lib/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +80 -0
  37. data/lib/vendor/thor/lib/thor/core_ext/io_binary_read.rb +12 -0
  38. data/lib/vendor/thor/lib/thor/core_ext/ordered_hash.rb +100 -0
  39. data/lib/vendor/thor/lib/thor/error.rb +28 -0
  40. data/lib/vendor/thor/lib/thor/group.rb +282 -0
  41. data/lib/vendor/thor/lib/thor/invocation.rb +172 -0
  42. data/lib/vendor/thor/lib/thor/parser.rb +4 -0
  43. data/lib/vendor/thor/lib/thor/parser/argument.rb +74 -0
  44. data/lib/vendor/thor/lib/thor/parser/arguments.rb +171 -0
  45. data/lib/vendor/thor/lib/thor/parser/option.rb +121 -0
  46. data/lib/vendor/thor/lib/thor/parser/options.rb +218 -0
  47. data/lib/vendor/thor/lib/thor/rake_compat.rb +72 -0
  48. data/lib/vendor/thor/lib/thor/runner.rb +322 -0
  49. data/lib/vendor/thor/lib/thor/shell.rb +88 -0
  50. data/lib/vendor/thor/lib/thor/shell/basic.rb +393 -0
  51. data/lib/vendor/thor/lib/thor/shell/color.rb +148 -0
  52. data/lib/vendor/thor/lib/thor/shell/html.rb +127 -0
  53. data/lib/vendor/thor/lib/thor/util.rb +270 -0
  54. data/lib/vendor/thor/lib/thor/version.rb +3 -0
  55. data/lib/vendor/thor/thor.gemspec +24 -0
  56. data/spec/engineyard/cli/api_spec.rb +50 -0
  57. data/spec/engineyard/cli_spec.rb +28 -0
  58. data/spec/engineyard/config_spec.rb +61 -0
  59. data/spec/engineyard/deploy_config_spec.rb +194 -0
  60. data/spec/engineyard/eyrc_spec.rb +76 -0
  61. data/spec/engineyard/repo_spec.rb +83 -0
  62. data/spec/engineyard_spec.rb +7 -0
  63. data/spec/ey/console_spec.rb +57 -0
  64. data/spec/ey/deploy_spec.rb +435 -0
  65. data/spec/ey/ey_spec.rb +23 -0
  66. data/spec/ey/init_spec.rb +123 -0
  67. data/spec/ey/list_environments_spec.rb +120 -0
  68. data/spec/ey/login_spec.rb +33 -0
  69. data/spec/ey/logout_spec.rb +24 -0
  70. data/spec/ey/logs_spec.rb +36 -0
  71. data/spec/ey/rebuild_spec.rb +18 -0
  72. data/spec/ey/recipes/apply_spec.rb +29 -0
  73. data/spec/ey/recipes/download_spec.rb +43 -0
  74. data/spec/ey/recipes/upload_spec.rb +99 -0
  75. data/spec/ey/rollback_spec.rb +73 -0
  76. data/spec/ey/scp_spec.rb +176 -0
  77. data/spec/ey/servers_spec.rb +209 -0
  78. data/spec/ey/ssh_spec.rb +273 -0
  79. data/spec/ey/status_spec.rb +45 -0
  80. data/spec/ey/timeout_deploy_spec.rb +18 -0
  81. data/spec/ey/web/disable_spec.rb +21 -0
  82. data/spec/ey/web/enable_spec.rb +26 -0
  83. data/spec/ey/web/restart_spec.rb +21 -0
  84. data/spec/ey/whoami_spec.rb +30 -0
  85. data/spec/spec_helper.rb +84 -0
  86. data/spec/support/bundled_ey +7 -0
  87. data/spec/support/fixture_recipes.tgz +0 -0
  88. data/spec/support/git_repos.rb +115 -0
  89. data/spec/support/helpers.rb +330 -0
  90. data/spec/support/matchers.rb +16 -0
  91. data/spec/support/ruby_ext.rb +13 -0
  92. data/spec/support/shared_behavior.rb +278 -0
  93. metadata +411 -0
data/bin/ey ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'engineyard/cli'
4
+
5
+ begin
6
+ EY::CLI.start
7
+ rescue
8
+ exit(1)
9
+ end
@@ -0,0 +1,9 @@
1
+ module EY
2
+ autoload :Repo, 'engineyard/repo'
3
+ autoload :Templates, 'engineyard/templates'
4
+ end
5
+
6
+ require 'engineyard-cloud-client'
7
+ require 'engineyard/version'
8
+ require 'engineyard/error'
9
+ require 'engineyard/config'
@@ -0,0 +1,816 @@
1
+ require 'engineyard'
2
+ require 'engineyard/error'
3
+ require 'engineyard/thor'
4
+ require 'engineyard/deploy_config'
5
+ require 'engineyard/serverside_runner'
6
+ require 'launchy'
7
+ require 'fileutils'
8
+
9
+ module EY
10
+ class CLI < EY::Thor
11
+ require 'engineyard/cli/recipes'
12
+ require 'engineyard/cli/web'
13
+ require 'engineyard/cli/api'
14
+ require 'engineyard/cli/ui'
15
+ require 'engineyard/error'
16
+ require 'engineyard-cloud-client/errors'
17
+
18
+ include Thor::Actions
19
+
20
+ def self.start(given_args=ARGV, config={})
21
+ Thor::Base.shell = EY::CLI::UI
22
+ ui = EY::CLI::UI.new
23
+ super(given_args, {shell: ui}.merge(config))
24
+ rescue Thor::Error, EY::Error, EY::CloudClient::Error => e
25
+ ui.print_exception(e)
26
+ raise
27
+ rescue Interrupt => e
28
+ puts
29
+ ui.print_exception(e)
30
+ ui.say("Quitting...")
31
+ raise
32
+ rescue SystemExit, Errno::EPIPE
33
+ # don't print a message for safe exits
34
+ raise
35
+ rescue Exception => e
36
+ ui.print_exception(e)
37
+ raise
38
+ end
39
+
40
+ class_option :api_token, type: :string, desc: "Use API_TOKEN to authenticate this command"
41
+ class_option :serverside_version, type: :string, desc: "Please use with care! Override deploy system version (same as ENV variable ENGINEYARD_SERVERSIDE_VERSION)"
42
+ class_option :quiet, aliases: %w[-q], type: :boolean, desc: "Quieter CLI output."
43
+
44
+ desc "init",
45
+ "Initialize the current directory with an ey.yml configuration file."
46
+ long_desc <<-DESC
47
+ Initialize the current directory with an ey.yml configuration file.
48
+
49
+ Please read the generated file and make adjustments.
50
+ Many applications will need only the default behavior.
51
+ For reference, many available options are explained in the generated file.
52
+
53
+ IMPORTANT: THE GENERATED FILE '#{EY::Config.pathname_for_write}'
54
+ MUST BE COMMITTED TO YOUR REPOSITORY OR OPTIONS WILL NOT BE LOADED.
55
+ DESC
56
+ method_option :path, type: :string, aliases: %w(-p),
57
+ desc: "Path for ey.yml (supported paths: #{EY::Config::CONFIG_FILES.join(', ')})"
58
+ def init
59
+ unless EY::Repo.exist?
60
+ raise EY::Error, "Working directory is not a repository. Aborting."
61
+ end
62
+
63
+ path = Pathname.new(options['path'] || EY::Config.pathname_for_write)
64
+
65
+ existing = {}
66
+ if path.exist?
67
+ ui.warn "Reinitializing existing file: #{path}"
68
+ existing = EY::Config.load_config
69
+ end
70
+
71
+ template = EY::Templates::EyYml.new(existing)
72
+ template.write(path)
73
+
74
+ ui.info <<-GIT
75
+
76
+ Configuration generated: #{path}
77
+ Go look at it, then add it to your repository!
78
+
79
+ \tgit add #{path}
80
+ \tgit commit -m "Add generated #{path} from ey init"
81
+
82
+ GIT
83
+ end
84
+
85
+
86
+ desc "deploy [--environment ENVIRONMENT] [--ref GIT-REF]",
87
+ "Deploy specified branch, tag, or sha to specified environment."
88
+ long_desc <<-DESC
89
+ This command must be run with the current directory containing the app to be
90
+ deployed. If ey.yml specifies a default branch then the ref parameter can be
91
+ omitted. Furthermore, if a default branch is specified but a different command
92
+ is supplied the deploy will fail unless -R or --force-ref is used.
93
+
94
+ Migrations are run based on the settings in your ey.yml file.
95
+ With each deploy the default migration setting can be overriden by
96
+ specifying --migrate or --migrate 'rake db:migrate'.
97
+ Migrations can also be skipped by using --no-migrate.
98
+ DESC
99
+ method_option :ignore_bad_master, type: :boolean, aliases: %w(--ignore-bad-bridge),
100
+ desc: "Force a deploy even if the master is in a bad state"
101
+ method_option :migrate, type: :string, aliases: %w(-m),
102
+ lazy_default: true,
103
+ desc: "Run migrations via [MIGRATE]; use --no-migrate to avoid running migrations"
104
+ method_option :ref, type: :string, aliases: %w(-r --branch --tag),
105
+ required: true, default: '',
106
+ desc: "Git ref to deploy. May be a branch, a tag, or a SHA. Use -R to deploy a different ref if a default is set."
107
+ method_option :force_ref, type: :string, aliases: %w(--ignore-default-branch -R),
108
+ lazy_default: true,
109
+ desc: "Force a deploy of the specified git ref even if a default is set in ey.yml."
110
+ method_option :environment, type: :string, aliases: %w(-e),
111
+ required: true, default: false,
112
+ desc: "Environment in which to deploy this application"
113
+ method_option :app, type: :string, aliases: %w(-a),
114
+ required: true, default: '',
115
+ desc: "Name of the application to deploy"
116
+ method_option :account, type: :string, aliases: %w(-c),
117
+ required: true, default: '',
118
+ desc: "Name of the account in which the environment can be found"
119
+ method_option :verbose, type: :boolean, aliases: %w(-v),
120
+ desc: "Be verbose"
121
+ method_option :config, type: :hash, default: {}, aliases: %w(--extra-deploy-hook-options),
122
+ desc: "Hash made available in deploy hooks (in the 'config' hash), can also override some ey.yml settings."
123
+ def deploy
124
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
125
+
126
+ env_config = config.environment_config(app_env.environment_name)
127
+ deploy_config = EY::DeployConfig.new(options, env_config, repo, ui)
128
+
129
+ deployment = app_env.new_deployment({
130
+ ref: deploy_config.ref,
131
+ migrate: deploy_config.migrate,
132
+ migrate_command: deploy_config.migrate_command,
133
+ extra_config: deploy_config.extra_config,
134
+ serverside_version: serverside_version,
135
+ })
136
+
137
+ runner = serverside_runner(app_env, deploy_config.verbose, deployment.serverside_version, options[:ignore_bad_master])
138
+
139
+ out = EY::CLI::UI::Tee.new(ui.out, deployment.output)
140
+ err = EY::CLI::UI::Tee.new(ui.err, deployment.output)
141
+
142
+ ui.info "Beginning deploy...", :green
143
+ begin
144
+ deployment.start
145
+ rescue
146
+ ui.error "Error encountered before deploy. Deploy not started."
147
+ raise
148
+ end
149
+
150
+ begin
151
+ ui.show_deployment(deployment)
152
+ out << "Deploy initiated.\n"
153
+
154
+ runner.deploy do |args|
155
+ args.config = deployment.config if deployment.config
156
+ if deployment.migrate
157
+ args.migrate = deployment.migrate_command
158
+ else
159
+ args.migrate = false
160
+ end
161
+ args.ref = deployment.resolved_ref
162
+ end
163
+ deployment.successful = runner.call(out, err)
164
+ rescue Interrupt
165
+ Signal.trap(:INT) { # The fingers you have used to dial are too fat...
166
+ ui.info "\nRun `ey timeout-deploy` to mark an unfinished deployment as failed."
167
+ exit 1
168
+ }
169
+ err << "Interrupted. Deployment halted.\n"
170
+ ui.warn <<-WARN
171
+ Recording interruption of this unfinished deployment in Engine Yard Cloud...
172
+
173
+ WARNING: Interrupting again may prevent Engine Yard Cloud from recording this
174
+ failed deployment. Unfinished deployments can block future deploys.
175
+ WARN
176
+ raise
177
+ rescue StandardError => e
178
+ deployment.err << "Error encountered during deploy.\n#{e.class} #{e}\n"
179
+ ui.print_exception(e)
180
+ raise
181
+ ensure
182
+ ui.info "Saving log... ", :green
183
+ deployment.finished
184
+
185
+ if deployment.successful?
186
+ ui.info "Successful deployment recorded on Engine Yard Cloud.", :green
187
+ ui.info "Run `ey launch` to open the application in a browser."
188
+ else
189
+ ui.info "Failed deployment recorded on Engine Yard Cloud", :green
190
+ raise EY::Error, "Deploy failed"
191
+ end
192
+ end
193
+ end
194
+
195
+ desc "timeout-deploy [--environment ENVIRONMENT]",
196
+ "Fail a stuck unfinished deployment."
197
+ long_desc <<-DESC
198
+ NOTICE: Timing out a deploy does not stop currently running deploy
199
+ processes.
200
+
201
+ This command must be run in the current directory containing the app.
202
+ The latest running deployment will be marked as failed, allowing a
203
+ new deployment to be run. It is possible to mark a potentially successful
204
+ deployment as failed. Only run this when a deployment is known to be
205
+ wrongly unfinished/stuck and when further deployments are blocked.
206
+ DESC
207
+ method_option :environment, type: :string, aliases: %w(-e),
208
+ required: true, default: false,
209
+ desc: "Environment in which to deploy this application"
210
+ method_option :app, type: :string, aliases: %w(-a),
211
+ required: true, default: '',
212
+ desc: "Name of the application to deploy"
213
+ method_option :account, type: :string, aliases: %w(-c),
214
+ required: true, default: '',
215
+ desc: "Name of the account in which the environment can be found"
216
+ def timeout_deploy
217
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
218
+ deployment = app_env.last_deployment
219
+ if deployment && !deployment.finished?
220
+ begin
221
+ ui.info "Marking last deployment failed...", :green
222
+ deployment.timeout
223
+ ui.deployment_status(deployment)
224
+ rescue EY::CloudClient::RequestFailed => e
225
+ ui.error "Error encountered attempting to timeout previous deployment."
226
+ raise
227
+ end
228
+ else
229
+ raise EY::Error, "No unfinished deployment was found for #{app_env.hierarchy_name}."
230
+ end
231
+ end
232
+
233
+ desc "status", "Show the deployment status of the app"
234
+ long_desc <<-DESC
235
+ Show the current status of most recent deployment of the specified
236
+ application and environment.
237
+ DESC
238
+ method_option :environment, type: :string, aliases: %w(-e),
239
+ required: true, default: '',
240
+ desc: "Environment where the application is deployed"
241
+ method_option :app, type: :string, aliases: %w(-a),
242
+ required: true, default: '',
243
+ desc: "Name of the application"
244
+ method_option :account, type: :string, aliases: %w(-c),
245
+ required: true, default: '',
246
+ desc: "Name of the account in which the application can be found"
247
+ def status
248
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
249
+ deployment = app_env.last_deployment
250
+ if deployment
251
+ ui.deployment_status(deployment)
252
+ else
253
+ raise EY::Error, "Application #{app_env.app.name} has not been deployed on #{app_env.environment.name}."
254
+ end
255
+ end
256
+
257
+ desc "environments [--all]", "List environments for this app; use --all to list all environments."
258
+ long_desc <<-DESC
259
+ By default, environments for this app are displayed. The --all option will
260
+ display all environments, including those for this app.
261
+ DESC
262
+
263
+ method_option :all, type: :boolean, aliases: %(-A),
264
+ desc: "Show all environments (ignores --app, --account, and --environment arguments)"
265
+ method_option :simple, type: :boolean, aliases: %(-s),
266
+ desc: "Display one environment per line with no extra output"
267
+ method_option :app, type: :string, aliases: %w(-a),
268
+ required: true, default: '',
269
+ desc: "Show environments for this application"
270
+ method_option :account, type: :string, aliases: %w(-c),
271
+ required: true, default: '',
272
+ desc: "Show environments in this account"
273
+ method_option :environment, type: :string, aliases: %w(-e),
274
+ required: true, default: '',
275
+ desc: "Show environments matching environment name"
276
+ def environments
277
+ if options[:all] && options[:simple]
278
+ ui.print_simple_envs api.environments
279
+ elsif options[:all]
280
+ ui.print_envs api.apps
281
+ else
282
+ remotes = nil
283
+ if options[:app] == ''
284
+ repo.fail_on_no_remotes!
285
+ remotes = repo.remotes
286
+ end
287
+
288
+ resolver = api.resolve_app_environments({
289
+ account_name: options[:account],
290
+ app_name: options[:app],
291
+ environment_name: options[:environment],
292
+ remotes: remotes,
293
+ })
294
+
295
+ resolver.no_matches do |errors|
296
+ messages = errors
297
+ messages << "Use #{self.class.send(:banner_base)} environments --all to see all environments."
298
+ raise EY::NoMatchesError.new(messages.join("\n"))
299
+ end
300
+
301
+ apps = resolver.matches.map { |app_env| app_env.app }.uniq
302
+
303
+ if options[:simple]
304
+ if apps.size > 1
305
+ message = "# This app matches multiple Applications in Engine Yard Cloud:\n"
306
+ apps.each { |app| message << "#\t#{app.name}\n" }
307
+ message << "# The following environments contain those applications:\n\n"
308
+ ui.warn(message)
309
+ end
310
+ ui.print_simple_envs(apps.map{ |app| app.environments }.flatten)
311
+ else
312
+ ui.print_envs(apps, config.default_environment)
313
+ end
314
+ end
315
+ end
316
+ map "envs" => :environments
317
+
318
+ desc "servers", "List servers for an environment."
319
+ long_desc <<-DESC
320
+ Display a list of all servers on an environment.
321
+ Specify -s (--simple) to make parsing the output easier
322
+ or -uS (--user --host) to output bash loop friendly "user@hostname"
323
+ DESC
324
+
325
+ method_option :simple, type: :boolean, aliases: %(-s),
326
+ desc: "Display all information in a simplified format without extra text or column alignment"
327
+ method_option :host, type: :boolean, aliases: %(-S),
328
+ desc: "Display only hostnames, one per newline (use options -uS (--user --host) for user@hostname)"
329
+ method_option :user, type: :boolean, aliases: %w(-u),
330
+ desc: "Include the ssh username in front of the hostname for easy SSH scripting"
331
+ method_option :account, type: :string, aliases: %w(-c),
332
+ required: true, default: '',
333
+ desc: "Find environment in this account"
334
+ method_option :environment, type: :string, aliases: %w(-e),
335
+ required: true, default: '',
336
+ desc: "Show servers in environment matching environment name"
337
+ method_option :all, type: :boolean, aliases: %(-A),
338
+ desc: "Show all servers (for compatibility only, this is the default for this command)"
339
+ method_option :app_master, type: :boolean,
340
+ desc: "Show only app master server"
341
+ method_option :app_servers, type: :boolean, aliases: %w(--app),
342
+ desc: "Show only application servers"
343
+ method_option :db_servers, type: :boolean, aliases: %w(--db),
344
+ desc: "Show only database servers"
345
+ method_option :db_master, type: :boolean,
346
+ desc: "Show only the master database server"
347
+ method_option :db_slaves, type: :boolean,
348
+ desc: "Show only the slave database servers"
349
+ method_option :utilities, type: :array, lazy_default: true, aliases: %w(--util),
350
+ desc: "Show only utility servers or only utility servers with the given names"
351
+ def servers
352
+ if options[:environment] == '' && options[:account] == ''
353
+ repo.fail_on_no_remotes!
354
+ end
355
+
356
+ environment = nil
357
+ ui.mute_if(options[:simple] || options[:host]) do
358
+ environment = fetch_environment(options[:environment], options[:account])
359
+ end
360
+
361
+ username = options[:user] && environment.username
362
+
363
+ servers = filter_servers(environment, options, default: {all: true})
364
+
365
+ if options[:host]
366
+ ui.print_hostnames(servers, username)
367
+ elsif options[:simple]
368
+ ui.print_simple_servers(servers, username)
369
+ else
370
+ ui.print_servers(servers, environment.hierarchy_name, username)
371
+ end
372
+ end
373
+
374
+ desc "rebuild [--environment ENVIRONMENT]", "Rebuild specified environment."
375
+ long_desc <<-DESC
376
+ Engine Yard's main configuration run occurs on all servers. Mainly used to fix
377
+ failed configuration of new or existing servers, or to update servers to latest
378
+ Engine Yard stack (e.g. to apply an Engine Yard supplied security
379
+ patch).
380
+
381
+ Note that uploaded recipes are also run after the main configuration run has
382
+ successfully completed.
383
+ DESC
384
+
385
+ method_option :environment, type: :string, aliases: %w(-e),
386
+ required: true, default: '',
387
+ desc: "Environment to rebuild"
388
+ method_option :account, type: :string, aliases: %w(-c),
389
+ required: true, default: '',
390
+ desc: "Name of the account in which the environment can be found"
391
+ def rebuild
392
+ environment = fetch_environment(options[:environment], options[:account])
393
+ ui.info "Updating instances on #{environment.hierarchy_name}"
394
+ environment.rebuild
395
+ end
396
+ map "update" => :rebuild
397
+
398
+ desc "rollback [--environment ENVIRONMENT]", "Rollback to the previous deploy."
399
+ long_desc <<-DESC
400
+ Uses code from previous deploy in the "/data/APP_NAME/releases" directory on
401
+ remote server(s) to restart application servers.
402
+ DESC
403
+
404
+ method_option :environment, type: :string, aliases: %w(-e),
405
+ required: true, default: '',
406
+ desc: "Environment in which to roll back the application"
407
+ method_option :app, type: :string, aliases: %w(-a),
408
+ required: true, default: '',
409
+ desc: "Name of the application to roll back"
410
+ method_option :account, type: :string, aliases: %w(-c),
411
+ required: true, default: '',
412
+ desc: "Name of the account in which the environment can be found"
413
+ method_option :verbose, type: :boolean, aliases: %w(-v),
414
+ desc: "Be verbose"
415
+ method_option :config, type: :hash, default: {}, aliases: %w(--extra-deploy-hook-options),
416
+ desc: "Hash made available in deploy hooks (in the 'config' hash), can also override some ey.yml settings."
417
+ def rollback
418
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
419
+ env_config = config.environment_config(app_env.environment_name)
420
+ deploy_config = EY::DeployConfig.new(options, env_config, repo, ui)
421
+
422
+ ui.info "Rolling back #{app_env.hierarchy_name}"
423
+
424
+ runner = serverside_runner(app_env, deploy_config.verbose)
425
+ runner.rollback do |args|
426
+ args.config = {'deployed_by' => api.current_user.name, 'input_ref' => 'N/A'}.merge(deploy_config.extra_config || {})
427
+ end
428
+
429
+ if runner.call(ui.out, ui.err)
430
+ ui.info "Rollback complete"
431
+ else
432
+ raise EY::Error, "Rollback failed"
433
+ end
434
+ end
435
+
436
+ desc "ssh [COMMAND] [--all] [--environment ENVIRONMENT]", "Open an ssh session to the master app server, or run a command."
437
+ long_desc <<-DESC
438
+ If a command is supplied, it will be run, otherwise a session will be
439
+ opened. The bridge server (app master) is used for environments with multiple instances.
440
+
441
+ Option --all requires a command to be supplied and runs it on all servers or
442
+ pass --each to connect to each server one after another.
443
+
444
+ Note: this command is a bit picky about its ordering. To run a command with arguments on
445
+ all servers, like "rm -f /some/file", you need to order it like so:
446
+
447
+ $ #{banner_base} ssh "rm -f /some/file" -e my-environment --all
448
+ DESC
449
+ method_option :environment, type: :string, aliases: %w(-e),
450
+ required: true, default: '',
451
+ desc: "Environment to ssh into"
452
+ method_option :account, type: :string, aliases: %w(-c),
453
+ required: true, default: '',
454
+ desc: "Name of the account in which the environment can be found"
455
+ method_option :all, type: :boolean, aliases: %(-A),
456
+ desc: "Run command on all servers"
457
+ method_option :app_servers, type: :boolean,
458
+ desc: "Run command on all application servers"
459
+ method_option :db_servers, type: :boolean,
460
+ desc: "Run command on the database servers"
461
+ method_option :db_master, type: :boolean,
462
+ desc: "Run command on the master database server"
463
+ method_option :db_slaves, type: :boolean,
464
+ desc: "Run command on the slave database servers"
465
+ method_option :utilities, type: :array, lazy_default: true,
466
+ desc: "Run command on the utility servers with the given names. If no names are given, run on all utility servers."
467
+ method_option :shell, type: :string, default: 'bash', aliases: %w(-s),
468
+ desc: "Run command in a shell other than bash. Use --no-shell to run the command without a shell."
469
+ method_option :pty, type: :boolean, default: false, aliases: %w(-t),
470
+ desc: "If a command is given, run in a pty. Required for interactive commands like sudo."
471
+ method_option :bind_address, type: :string, aliases: %w(-L),
472
+ desc: "When a command is not given, pass -L to the ssh command."
473
+ method_option :each, type: :boolean, default: false,
474
+ desc: "If no command is given, connect to multiple servers each one after another, instead of exiting with an error."
475
+
476
+ def ssh(cmd=nil)
477
+ environment = fetch_environment(options[:environment], options[:account])
478
+ instances = filter_servers(environment, options, default: {app_master: true})
479
+ user = environment.username
480
+ ssh_opts = []
481
+
482
+ if cmd
483
+ if options[:shell]
484
+ cmd = Escape.shell_command([options[:shell],'-lc',cmd])
485
+ end
486
+
487
+ if options[:pty]
488
+ ssh_opts = ["-t"]
489
+ elsif cmd =~ /sudo/
490
+ ui.warn "sudo commands often need a tty to run correctly. Use -t option to spawn a tty."
491
+ end
492
+ else
493
+ if instances.size != 1 && options[:each] == false
494
+ raise NoCommandError.new
495
+ end
496
+
497
+ if options[:bind_address]
498
+ ssh_opts = ["-L", options[:bind_address]]
499
+ end
500
+ end
501
+
502
+ ssh_cmd = ["ssh"]
503
+ ssh_cmd += ssh_opts
504
+
505
+ trap(:INT) { abort "Aborting..." }
506
+
507
+ exits = []
508
+ instances.each do |instance|
509
+ host = instance.public_hostname
510
+ name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
511
+ ui.info "\nConnecting to #{name} #{host}..."
512
+ unless cmd
513
+ ui.info "Ctrl + C to abort"
514
+ sleep 1.3
515
+ end
516
+ sshcmd = Escape.shell_command((ssh_cmd + ["#{user}@#{host}"] + [cmd]).compact)
517
+ ui.debug "$ #{sshcmd}"
518
+ system sshcmd
519
+ exits << $?.exitstatus
520
+ end
521
+
522
+ exit exits.detect {|status| status != 0 } || 0
523
+ end
524
+
525
+ desc "console [--app APP] [--environment ENVIRONMENT] [--account ACCOUNT]", "Open a Rails console session to the master app server."
526
+ long_desc <<-DESC
527
+ Opens a Rails console session on app master.
528
+ DESC
529
+ method_option :environment, type: :string, aliases: %w(-e),
530
+ required: true, default: '',
531
+ desc: "Environment to console into"
532
+ method_option :app, type: :string, aliases: %w(-a),
533
+ required: true, default: '',
534
+ desc: "Name of the application"
535
+ method_option :account, type: :string, aliases: %w(-c),
536
+ required: true, default: '',
537
+ desc: "Name of the account in which the environment can be found"
538
+
539
+ def console
540
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
541
+ instances = filter_servers(app_env.environment, options, default: {app_master: true})
542
+ user = app_env.environment.username
543
+ cmd = "cd /data/#{app_env.app.name}/current && current_user=#{api.current_user.name} bundle exec rails console"
544
+ cmd = Escape.shell_command(['bash','-lc',cmd])
545
+
546
+ ssh_cmd = ["ssh"]
547
+ ssh_cmd += ["-t"]
548
+
549
+ trap(:INT) { abort "Aborting..." }
550
+
551
+ exits = []
552
+ instances.each do |instance|
553
+ host = instance.public_hostname
554
+ name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
555
+ ui.info "\nConnecting to #{name} #{host}..."
556
+ unless cmd
557
+ ui.info "Ctrl + C to abort"
558
+ sleep 1.3
559
+ end
560
+ sshcmd = Escape.shell_command((ssh_cmd + ["#{user}@#{host}"] + [cmd]).compact)
561
+ ui.debug "$ #{sshcmd}"
562
+ system sshcmd
563
+ exits << $?.exitstatus
564
+ end
565
+
566
+ exit exits.detect {|status| status != 0 } || 0
567
+ end
568
+
569
+ desc "scp [FROM_PATH] [TO_PATH] [--all] [--environment ENVIRONMENT]", "scp a file to/from multiple servers in an environment"
570
+ long_desc <<-DESC
571
+ Use the system `scp` command to copy files to some or all of the servers.
572
+
573
+ If `HOST:` is found in the FROM_PATH or TO_PATH, the server name will be
574
+ substituted in place of `HOST:` when scp is run. This allows you to scp in
575
+ either direction by putting `HOST:` in the FROM_PATH or TO_PATH, as follows:
576
+
577
+ $ #{banner_base} scp example.json HOST:/data/app_name/current/config/ -e env --app-servers
578
+
579
+ $ #{banner_base} scp HOST:/data/app_name/current/config/example.json ./ -e env --app-servers
580
+
581
+ If `HOST:` is not specified, TO_PATH will be used as the remote path.
582
+ Be sure to escape shell words so they don't expand locally (e.g. '~').
583
+
584
+ Note: this command is a bit picky about its ordering. FROM_PATH TO_PATH
585
+ must follow immediately after `ey scp` with no flags in between.
586
+ DESC
587
+ method_option :environment, :type => :string, :aliases => %w(-e),
588
+ :required => true, :default => '',
589
+ :desc => "Name of the destination environment"
590
+ method_option :account, :type => :string, :aliases => %w(-c),
591
+ :required => true, :default => '',
592
+ :desc => "Name of the account in which the environment can be found"
593
+ method_option :all, :type => :boolean, :aliases => %(-A),
594
+ :desc => "scp to all servers"
595
+ method_option :app_servers, :type => :boolean,
596
+ :desc => "scp to all application servers"
597
+ method_option :db_servers, :type => :boolean,
598
+ :desc => "scp to database servers"
599
+ method_option :db_master, :type => :boolean,
600
+ :desc => "scp to the master database server"
601
+ method_option :db_slaves, :type => :boolean,
602
+ :desc => "scp to the slave database servers"
603
+ method_option :utilities, :type => :array, :lazy_default => true,
604
+ :desc => "scp to all utility servers or only those with the given names"
605
+
606
+ def scp(from_path, to_path)
607
+ environment = fetch_environment(options[:environment], options[:account])
608
+ instances = filter_servers(environment, options, default: {app_master: true})
609
+ user = environment.username
610
+
611
+ ui.info "Copying '#{from_path}' to '#{to_path}' on #{instances.count} server#{instances.count == 1 ? '' : 's'} serially..."
612
+
613
+ # default to `scp FROM_PATH HOST:TO_PATH`
614
+ unless [from_path, to_path].detect { |path| path =~ /HOST:/ }
615
+ to_path = "HOST:#{to_path}"
616
+ end
617
+
618
+ exits = []
619
+ instances.each do |instance|
620
+ host = instance.public_hostname
621
+ authority = "#{user}@#{host}:"
622
+
623
+ name = instance.name ? "#{instance.role} (#{instance.name})" : instance.role
624
+ ui.info "# #{name} #{host}"
625
+
626
+ from = from_path.sub(/^HOST:/, authority)
627
+ to = to_path.sub(/^HOST:/, authority)
628
+
629
+ cmd = Escape.shell_command(["scp", from, to])
630
+ ui.debug "$ #{cmd}"
631
+ system cmd
632
+ exits << $?.exitstatus
633
+ end
634
+
635
+ exit exits.detect {|status| status != 0 } || 0
636
+ end
637
+
638
+ no_tasks do
639
+ OPT_TO_ROLES = {
640
+ all: %w[all],
641
+ app_master: %w[solo app_master],
642
+ app_servers: %w[solo app app_master],
643
+ db_servers: %w[solo db_master db_slave],
644
+ db_master: %w[solo db_master],
645
+ db_slaves: %w[db_slave],
646
+ utilities: %w[util],
647
+ }
648
+
649
+ def filter_servers(environment, cli_opts, filter_opts)
650
+ if (cli_opts.keys.map(&:to_sym) & OPT_TO_ROLES.keys).any?
651
+ options = cli_opts.dup
652
+ else
653
+ options = filter_opts[:default].dup
654
+ end
655
+
656
+ options.keep_if {|k,v| OPT_TO_ROLES.has_key?(k.to_sym) }
657
+
658
+ if options[:all]
659
+ instances = environment.instances
660
+ else
661
+ roles = {}
662
+ options.each do |cli_opt,cli_val|
663
+ if cli_val && OPT_TO_ROLES.has_key?(cli_opt.to_sym)
664
+ OPT_TO_ROLES[cli_opt.to_sym].each do |role|
665
+ roles[role] = cli_val # val is true or an array of strings
666
+ end
667
+ end
668
+ end
669
+ instances = environment.select_instances(roles)
670
+ end
671
+
672
+ if instances.empty?
673
+ raise NoInstancesError.new(environment.name)
674
+ end
675
+
676
+ return instances
677
+ end
678
+ end
679
+
680
+ desc "logs [--environment ENVIRONMENT]", "Retrieve the latest logs for an environment."
681
+ long_desc <<-DESC
682
+ Displays Engine Yard configuration logs for all servers in the environment. If
683
+ recipes were uploaded to the environment & run, their logs will also be
684
+ displayed beneath the main configuration logs.
685
+ DESC
686
+ method_option :environment, type: :string, aliases: %w(-e),
687
+ required: true, default: '',
688
+ desc: "Environment with the interesting logs"
689
+ method_option :account, type: :string, aliases: %w(-c),
690
+ required: true, default: '',
691
+ desc: "Name of the account in which the environment can be found"
692
+ def logs
693
+ environment = fetch_environment(options[:environment], options[:account])
694
+ environment.logs.each do |log|
695
+ ui.say "Instance: #{log.instance_name}"
696
+
697
+ if log.main
698
+ ui.say "Main logs for #{environment.name}:", :green
699
+ ui.say log.main
700
+ end
701
+
702
+ if log.custom
703
+ ui.say "Custom logs for #{environment.name}:", :green
704
+ ui.say log.custom
705
+ end
706
+ end
707
+ end
708
+
709
+ desc "recipes", "Commands related to chef recipes."
710
+ subcommand "recipes", EY::CLI::Recipes
711
+
712
+ desc "web", "Commands related to maintenance pages."
713
+ subcommand "web", EY::CLI::Web
714
+
715
+ desc "version", "Print version number."
716
+ def version
717
+ ui.say %{engineyard version #{EY::VERSION}}
718
+ end
719
+ map ["-v", "--version"] => :version
720
+
721
+ desc "help [COMMAND]", "Describe all commands or one specific command."
722
+ def help(*cmds)
723
+ if cmds.empty?
724
+ base = self.class.send(:banner_base)
725
+ list = self.class.printable_tasks
726
+
727
+ ui.say "Usage:"
728
+ ui.say " #{base} [--help] [--version] COMMAND [ARGS]"
729
+ ui.say
730
+
731
+ ui.say "Deploy commands:"
732
+ deploy_cmds = %w(deploy environments logs rebuild rollback status)
733
+ deploy_cmds.map! do |name|
734
+ list.find{|task| task[0] =~ /^#{base} #{name}/ }
735
+ end
736
+ list -= deploy_cmds
737
+
738
+ ui.print_help(deploy_cmds)
739
+ ui.say
740
+
741
+ self.class.subcommands.each do |name|
742
+ klass = self.class.subcommand_class_for(name)
743
+ list.reject!{|cmd| cmd[0] =~ /^#{base} #{name}/}
744
+ ui.say "#{name.capitalize} commands:"
745
+ tasks = klass.printable_tasks.reject{|t| t[0] =~ /help$/ }
746
+ ui.print_help(tasks)
747
+ ui.say
748
+ end
749
+
750
+ %w(help version).each{|n| list.reject!{|c| c[0] =~ /^#{base} #{n}/ } }
751
+ if list.any?
752
+ ui.say "Other commands:"
753
+ ui.print_help(list)
754
+ ui.say
755
+ end
756
+
757
+ self.class.send(:class_options_help, shell)
758
+ ui.say "See '#{base} help COMMAND' for more information on a specific command."
759
+ elsif klass = self.class.subcommand_class_for(cmds.first)
760
+ klass.new.help(*cmds[1..-1])
761
+ else
762
+ super
763
+ end
764
+ end
765
+
766
+ desc "launch [--app APP] [--environment ENVIRONMENT] [--account ACCOUNT]", "Open application in browser."
767
+ method_option :environment, type: :string, aliases: %w(-e),
768
+ required: true, default: '',
769
+ desc: "Environment where the application is deployed"
770
+ method_option :app, type: :string, aliases: %w(-a),
771
+ required: true, default: '',
772
+ desc: "Name of the application"
773
+ method_option :account, type: :string, aliases: %w(-c),
774
+ required: true, default: '',
775
+ desc: "Name of the account in which the application can be found"
776
+ def launch
777
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
778
+ Launchy.open(app_env.uri)
779
+ end
780
+
781
+ desc "whoami", "Who am I logged in as?"
782
+ def whoami
783
+ current_user = api.current_user
784
+ ui.say "#{current_user.name} (#{current_user.email})"
785
+ end
786
+
787
+ desc "login", "Log in and verify access to Engine Yard Cloud."
788
+ long_desc <<-DESC
789
+ You may run this command to log in to EY Cloud without performing
790
+ any other action.
791
+
792
+ Once you are logged in, a file will be stored at ~/.eyrc with your
793
+ API token. You may override the location of this file using the
794
+ $EYRC environment variable.
795
+
796
+ Instead of logging in, you may specify a token on the command line
797
+ with --api-token or using the $ENGINEYARD_API_TOKEN environment
798
+ variable.
799
+ DESC
800
+ def login
801
+ whoami
802
+ end
803
+
804
+ desc "logout", "Remove the current API key from ~/.eyrc or env variable $EYRC"
805
+ def logout
806
+ eyrc = EYRC.load
807
+ if eyrc.delete_api_token
808
+ ui.info "API token removed: #{eyrc.path}"
809
+ ui.info "Run any other command to login again."
810
+ else
811
+ ui.info "Already logged out. Run any other command to login again."
812
+ end
813
+ end
814
+
815
+ end # CLI
816
+ end # EY