crazy-yard 3.2.2

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 (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