ey-core 3.0.5 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -7
  3. data/bin/ey-core +1 -1
  4. data/ey-core.gemspec +15 -1
  5. data/lib/ey-core/cli.rb +12 -114
  6. data/lib/ey-core/cli/accounts.rb +9 -0
  7. data/lib/ey-core/cli/applications.rb +16 -0
  8. data/lib/ey-core/cli/console.rb +14 -0
  9. data/lib/ey-core/cli/current_user.rb +8 -0
  10. data/lib/ey-core/cli/deploy.rb +65 -0
  11. data/lib/ey-core/cli/environments.rb +17 -0
  12. data/lib/ey-core/cli/errors.rb +7 -0
  13. data/lib/ey-core/cli/init.rb +11 -0
  14. data/lib/ey-core/cli/login.rb +26 -0
  15. data/lib/ey-core/cli/logout.rb +14 -0
  16. data/lib/ey-core/cli/logs.rb +40 -0
  17. data/lib/ey-core/cli/recipes.rb +92 -0
  18. data/lib/ey-core/cli/recipes/apply.rb +34 -0
  19. data/lib/ey-core/cli/recipes/download.rb +26 -0
  20. data/lib/ey-core/cli/recipes/upload.rb +27 -0
  21. data/lib/ey-core/cli/scp.rb +11 -0
  22. data/lib/ey-core/cli/servers.rb +19 -0
  23. data/lib/ey-core/cli/ssh.rb +94 -0
  24. data/lib/ey-core/cli/status.rb +20 -0
  25. data/lib/ey-core/cli/subcommand.rb +114 -0
  26. data/lib/ey-core/cli/timeout_deploy.rb +28 -0
  27. data/lib/ey-core/cli/version.rb +8 -0
  28. data/lib/ey-core/cli/web.rb +10 -0
  29. data/lib/ey-core/cli/web/disable.rb +23 -0
  30. data/lib/ey-core/cli/web/enable.rb +23 -0
  31. data/lib/ey-core/cli/web/restart.rb +23 -0
  32. data/lib/ey-core/cli/whoami.rb +4 -0
  33. data/lib/ey-core/client.rb +9 -0
  34. data/lib/ey-core/client/mock.rb +1 -0
  35. data/lib/ey-core/client/real.rb +4 -4
  36. data/lib/ey-core/collections/deployments.rb +8 -0
  37. data/lib/ey-core/models/account.rb +1 -0
  38. data/lib/ey-core/models/deployment.rb +23 -0
  39. data/lib/ey-core/models/environment.rb +37 -0
  40. data/lib/ey-core/requests/change_environment_maintenance.rb +38 -0
  41. data/lib/ey-core/requests/create_environment.rb +3 -0
  42. data/lib/ey-core/requests/deploy_environment_application.rb +17 -0
  43. data/lib/ey-core/requests/get_deployment.rb +19 -0
  44. data/lib/ey-core/requests/get_deployments.rb +29 -0
  45. data/lib/ey-core/requests/get_token_by_login.rb +30 -0
  46. data/lib/ey-core/requests/restart_environment_app_servers.rb +38 -0
  47. data/lib/ey-core/requests/timeout_deployment.rb +27 -0
  48. data/lib/ey-core/requests/upload_recipes_for_environment.rb +28 -0
  49. data/lib/ey-core/version.rb +1 -1
  50. data/spec/deployments_spec.rb +24 -0
  51. data/spec/tokens_spec.rb +23 -1
  52. metadata +228 -8
@@ -0,0 +1,92 @@
1
+ class Ey::Core::Cli::Recipes < Ey::Core::Cli::Subcommand
2
+ title "recipes"
3
+ summary "Chef specific commands"
4
+
5
+ Dir[File.dirname(__FILE__) + "/recipes/*.rb"].each { |file| load file }
6
+
7
+ Ey::Core::Cli::Recipes.descendants.each do |d|
8
+ mount d
9
+ end
10
+
11
+ def run_chef(type, environment)
12
+ request = environment.apply(type)
13
+ puts "Started #{type} chef run".green
14
+ request.wait_for { |r| r.ready? }
15
+ if request.successful
16
+ puts "#{type.capitalize} chef run completed".green
17
+ else
18
+ puts "#{type.capitalize} chef run failed".red
19
+ ap request
20
+ end
21
+ end
22
+
23
+ def gzip(tarfile)
24
+ gz = StringIO.new("")
25
+ z = Zlib::GzipWriter.new(gz)
26
+ z.write tarfile.string
27
+ z.close # this is necessary!
28
+
29
+ # z was closed to write the gzip footer, so
30
+ # now we need a new StringIO
31
+ StringIO.new gz.string
32
+ end
33
+
34
+ def archive_directory(path)
35
+ tarfile = StringIO.new("")
36
+ Gem::Package::TarWriter.new(tarfile) do |tar|
37
+ Dir[File.join(path, "**/*")].each do |file|
38
+ mode = File.stat(file).mode
39
+ relative_file = "cookbooks/#{file.sub(/^#{Regexp::escape path}\/?/, '')}"
40
+
41
+ if File.directory?(file)
42
+ tar.mkdir relative_file, mode
43
+ else
44
+ tar.add_file relative_file, mode do |tf|
45
+ File.open(file, "rb") { |f| tf.write f.read }
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ tarfile.rewind
52
+ gzip(tarfile)
53
+ end
54
+
55
+ def upload_recipes(environment, path="cookbooks/")
56
+ recipes_path = Pathname.new(path)
57
+
58
+ if recipes_path.exist? && recipes_path.to_s.match(/\.(tgz|tar\.gz)/)
59
+ environment.upload_recipes(recipes_path)
60
+ elsif recipes_path.exist?
61
+ environment.upload_recipes(archive_directory(path))
62
+ else
63
+ raise Ey::Core::Cli::RecipesNotFound, "Recipes file not found: #{recipes_path}"
64
+ end
65
+ end
66
+
67
+ def ungzip(tarfile)
68
+ z = Zlib::GzipReader.new(tarfile)
69
+ unzipped = StringIO.new(z.read)
70
+ z.close
71
+ unzipped
72
+ end
73
+
74
+
75
+ def untar(io, destination)
76
+ Gem::Package::TarReader.new io do |tar|
77
+ tar.each do |tarfile|
78
+ destination_file = File.join destination, tarfile.full_name
79
+
80
+ if tarfile.directory?
81
+ FileUtils.mkdir_p destination_file
82
+ else
83
+ destination_directory = File.dirname(destination_file)
84
+ FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
85
+ File.open destination_file, "wb" do |f|
86
+ f.print tarfile.read
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,34 @@
1
+ class Ey::Core::Cli::Recipes::Apply < Ey::Core::Cli::Recipes
2
+ title "apply"
3
+ summary "Apply changes to an environment"
4
+ option :account, short: "c", long: "account", description: "Name or id of account", argument: "account"
5
+ option :environment, short: "e", long: "environment", description: "Name or id of environment", argument: "environment"
6
+
7
+ switch :main, short: "m", long: "main", description: "Apply main recipes only"
8
+ switch :custom, long: "custom", description: "Apply custom recipes only"
9
+ switch :quick, short: "q", long: "quick", description: "Quick chef run"
10
+ switch :full, short: "f", long: "full", description: "Run main and custom chef"
11
+
12
+ def handle
13
+ operator, environment = core_operator_and_environment_for(options)
14
+ raise "Unable to find matching environment" unless environment
15
+
16
+ run_type = if switch_active?(:main)
17
+ "main"
18
+ elsif switch_active?(:custom)
19
+ "custom"
20
+ elsif switch_active?(:quick)
21
+ "quick"
22
+ elsif switch_active?(:full)
23
+ "main"
24
+ else
25
+ "main"
26
+ end
27
+
28
+ run_chef(run_type, environment)
29
+
30
+ if switch_active?(:full)
31
+ run_chef("custom", environment)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ class Ey::Core::Cli::Recipes::Download < Ey::Core::Cli::Recipes
2
+ title "download"
3
+ summary "Download a copy of the custom chef recipes from this environment into the current directory"
4
+ description <<-DESC
5
+ The recipes will be unpacked into a directory called "cookbooks" in the
6
+ current directory. This is the opposite of 'recipes upload'.
7
+
8
+ If the cookbooks directory already exists, an error will be raised.
9
+ DESC
10
+
11
+ option :environment, short: "e", long: "environment", description: "Environment that will receive the recipes.", argument: "environment"
12
+ option :account, short: "c", long: "account", description: "Name of the account in which the environment can be found.", argument: "account"
13
+
14
+ def handle
15
+ if File.exist?("cookbooks")
16
+ raise Ey::Core::Clie::RecipesExist.new("Cannot download recipes, cookbooks directory already exists.")
17
+ end
18
+
19
+ operator, environment = core_operator_and_environment_for(options)
20
+ puts "Downloading recipes".green
21
+ recipes = environment.download_recipes
22
+
23
+ puts "Extracting recipes to 'cookbooks/'".green
24
+ untar(ungzip(recipes), './')
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ class Ey::Core::Cli::Recipes::Upload < Ey::Core::Cli::Recipes
2
+ title "upload"
3
+ summary "Upload custom recipes to an environment"
4
+ option :environment, short: "e", long: "environment", description: "Environment that will receive the recipes.", argument: "environment"
5
+ option :account, short: "c", long: "account", description: "Name of the account in which the environment can be found.", argument: "account"
6
+ option :file, short: "f", long: "file", description: "Path to recipes", argument: "path"
7
+
8
+ switch :apply, short: "a", long: "apply", description: "Apply the recipes immediately after they are uploaded"
9
+
10
+ def handle
11
+ operator, environment = core_operator_and_environment_for(options)
12
+ path = option(:file) || "cookbooks/"
13
+
14
+ puts "Uploading custom recipes for #{environment.name}".green
15
+ begin
16
+ upload_recipes(environment, path)
17
+ puts "Uploading custom recipes complete".green
18
+ rescue => e
19
+ abort "There was a problem uploading the recipes".red
20
+ puts e.inspect
21
+ end
22
+
23
+ if switch_active?(:apply)
24
+ run_chef("custom", environment)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ class Ey::Core::Cli::Scp < Ey::Core::Cli::Subcommand
2
+ title "scp"
3
+ summary "This command is deprecated"
4
+ description <<-DESC
5
+ The scp command has been deprecated. We apologize for any inconvenience.
6
+ DESC
7
+
8
+ def handle
9
+ abort "This command is deprecated".red
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ class Ey::Core::Cli::Servers < Ey::Core::Cli::Subcommand
2
+ title "servers"
3
+ summary "List servers you have access to"
4
+ option :account, short: 'c', long: 'account', description: 'Filter by account name or id', argument: 'Account'
5
+ option :environment, short: "-e", long: "environment", description: "Filter by environment.", argument: "environment"
6
+
7
+ def handle
8
+ servers = if option(:account)
9
+ account = core_account_for(options)
10
+ core_client.servers.all(account: account)
11
+ elsif environment = option(:environment)
12
+ (core_client.environments.get(environment) || core_client.environments.first(name: environment)).servers.all
13
+ else
14
+ core_client.servers.all
15
+ end
16
+
17
+ puts TablePrint::Printer.new(servers, [{id: {width: 10}}, :role, :provisioned_id]).table_print
18
+ end
19
+ end
@@ -0,0 +1,94 @@
1
+ class Ey::Core::Cli::Ssh < Ey::Core::Cli::Subcommand
2
+ title "ssh"
3
+ summary "Open an SSH session to the environment's application master"
4
+ option :account, short: "c", long: "account", description: "Name or id of account", argument: "account"
5
+ option :environment, short: "e", long: "environment", description: "Name or id of environment", argument: "environment"
6
+ option :server, short: 's', long: "server", description: "Specific server to ssh into. Id or amazon id (i-12345)", argument: "server"
7
+ option :utilities, long: "utilities", description: "Run command on the utility servers with the given names. Specify all to run the command on all utility servers.", argument: "'all,resque,redis,etc'"
8
+ option :command, long: "command", description: "Command to run", argument: "'command with args'"
9
+ option :shell, short: 's', long: "shell", description: "Run command in a shell other than bash", argument: "shell"
10
+ option :bind_address, long: "bind_address", description: "When no command is specified, pass -L to ssh", argument: "bind address"
11
+
12
+ switch :all, long: "all", description: "Run command on all servers"
13
+ switch :app_servers, long: "app_servers", description: "Run command on all application servers"
14
+ switch :db_servers, long: "db_servers", description: "Run command on all database servers"
15
+ switch :db_master, long: "db_master", description: "Run command on database master"
16
+ switch :db_slaves, long: "db_slaves", description: "Run command on database slaves"
17
+ switch :tty, short: 't', long: "tty", description: "Allocated a tty for the command"
18
+
19
+ def handle
20
+ operator, environment = core_operator_and_environment_for(options)
21
+ abort "Unable to find matching environment".red unless environment
22
+
23
+ cmd = option(:command)
24
+ ssh_opts = []
25
+ ssh_cmd = ["ssh"]
26
+ exits = []
27
+ user = environment.username
28
+ servers = []
29
+
30
+
31
+
32
+ if option(:command)
33
+ if shell = option(:shell)
34
+ cmd = Escape.shell_command([shell,'-lc',cmd])
35
+ end
36
+
37
+ if switch_active?(:tty)
38
+ ssh_opts << "-t"
39
+ elsif cmd.match(/sudo/)
40
+ puts "sudo commands often need a tty to run correctly. Use -t option to spawn a tty.".yellow
41
+ end
42
+
43
+ if switch_active?(:all)
44
+ servers += environment.servers.all.to_a
45
+ end
46
+
47
+ if switch_active?(:app_servers)
48
+ servers += (environment.servers.all(role: "app_master") + environment.servers.all(role: "app") + environment.servers.all(role: "solo")).to_a
49
+ end
50
+
51
+ if switch_active?(:db_servers)
52
+ servers += (environment.servers.all(role: "db_master") + environment.servers.all(role: "db_slave") + environment.servers.all(role: "solo")).to_a
53
+ end
54
+
55
+ if switch_active?(:db_master)
56
+ servers += (environment.servers.all(role: "db_master") + environment.servers.all(role: "solo")).to_a
57
+ end
58
+
59
+ if utils = option(:utilities)
60
+ if utils == 'all'
61
+ servers += environment.servers.all(role: "util").to_a
62
+ else
63
+ servers += environment.servers.all(role: "util", name: utils).to_a
64
+ end
65
+ end
66
+ else
67
+ if option(:bind_address)
68
+ ssh_opts += ["-L", option(:bind_address)]
69
+ end
70
+
71
+ if option(:server)
72
+ servers += [core_server_for(server: option[:server], operator: environment)]
73
+ else
74
+ servers += (environment.servers.all(role: "app_master") + environment.servers.all(role: "solo")).to_a
75
+ end
76
+ end
77
+
78
+ if servers.empty?
79
+ abort "Unable to find any matching servers. Aborting.".red
80
+ end
81
+
82
+ servers.each do |server|
83
+ host = server.public_hostname
84
+ name = server.name ? "#{server.role} (#{server.name})" : server.role
85
+ puts "\nConnecting to #{name} #{host}".green
86
+ sshcmd = Escape.shell_command((ssh_cmd + ["#{user}@#{host}"] + [cmd]).compact)
87
+ puts "Running command: #{sshcmd}".green
88
+ system sshcmd
89
+ exits << $?.exitstatus
90
+ end
91
+
92
+ exit exits.detect {|status| status != 0 } || 0
93
+ end
94
+ end
@@ -0,0 +1,20 @@
1
+ class Ey::Core::Cli::Status < Ey::Core::Cli::Subcommand
2
+ title "status"
3
+ summary "Show the deployment status of the app"
4
+
5
+ option :environment, short: "e", long: "environment", description: "Name or id of the environment to deploy to.", argument: "Environment"
6
+ option :account, short: 'c', long: 'account', description: 'Name or ID of the account that the environment resides in. If no account is specified, the app will deploy to the first environment that meets the criteria, in the accounts you have access to.', argument: 'Account name or id'
7
+ option :app, short: "a", long: "app", description: "Application name or ID to deploy. If :account is not specified, this will be the first app that matches the criteria in the accounts you have access to.", argument: "app"
8
+
9
+ description <<-DESC
10
+ Show the current status of the most recent deployment of the specifed application and environment
11
+ DESC
12
+
13
+ def handle
14
+ operator, environment = core_operator_and_environment_for(self.options)
15
+ app = core_application_for(self.options)
16
+ deployments = core_client.deployments.all(environment_id: environment.id, application_id: app.id)
17
+
18
+ ap deployments.first
19
+ end
20
+ end
@@ -0,0 +1,114 @@
1
+ class Ey::Core::Cli::Subcommand < Belafonte::App
2
+ def self.descendants
3
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
4
+ end
5
+
6
+ class << self
7
+ attr_accessor :core_file
8
+
9
+ def core_file
10
+ @core_file ||= File.expand_path("~/.ey-core")
11
+ end
12
+
13
+ def eyrc
14
+ @eyrc ||= File.expand_path("~/.eyrc")
15
+ end
16
+ end
17
+
18
+ def unauthenticated_core_client
19
+ @unauthenticated_core_client ||= Ey::Core::Client.new(token: nil, url: core_url)
20
+ end
21
+
22
+ def core_client
23
+ @core_client ||= Ey::Core::Client.new(url: core_url, config_file: self.class.core_file)
24
+ rescue RuntimeError => e
25
+ if legacy_token = e.message.match(/missing token/i) && eyrc_yaml["api_token"]
26
+ puts "Found legacy .eyrc token. Migrating to core file".green
27
+ write_core_yaml(legacy_token)
28
+ retry
29
+ elsif e.message.match(/missing token/i)
30
+ abort "Missing credentials: Run 'ey login' to retrieve your Engine Yard Cloud API token.".yellow
31
+ else
32
+ raise e
33
+ end
34
+ end
35
+
36
+ def core_url
37
+ env_url = ENV["CORE_URL"] || ENV["CLOUD_URL"]
38
+ (env_url && File.join(env_url, '/')) || "https://api.engineyard.com/"
39
+ end
40
+
41
+ def current_accounts
42
+ core_client.users.current.accounts
43
+ end
44
+
45
+ def longest_length_by_name(collection)
46
+ collection.map(&:name).group_by(&:size).max.last.length
47
+ end
48
+
49
+ def write_core_yaml(token=nil)
50
+ core_yaml[core_url] = token if token
51
+ File.open(self.class.core_file, "w") { |f| f.puts core_yaml.to_yaml }
52
+ end
53
+
54
+ def eyrc_yaml
55
+ @eyrc_yaml ||= YAML.load_file(self.class.eyrc) || {}
56
+ rescue Errno::ENOENT => e # we don't really care if this doesn't exist
57
+ {}
58
+ end
59
+
60
+ def core_yaml
61
+ @core_yaml ||= YAML.load_file(self.class.core_file) || {}
62
+ rescue Errno::ENOENT => e
63
+ puts "Creating #{self.class.core_file}".yellow
64
+ FileUtils.touch(self.class.core_file)
65
+ retry
66
+ end
67
+
68
+ def core_account_for(options={})
69
+ @core_account ||= core_client.accounts.get(options[:account])
70
+ @core_account ||= core_client.users.current.accounts.first(name: options[:account])
71
+ end
72
+
73
+ def operator(options)
74
+ options[:account] ? core_account_for(options) : core_client
75
+ end
76
+
77
+ def core_operator_and_environment_for(options={})
78
+ operator = operator(options)
79
+ environment = operator.environments.get(options[:environment]) || operator.environments.first(name: options[:environment])
80
+ [operator, environment]
81
+ end
82
+
83
+ def core_environment_for(options={})
84
+ core_client.environments.get(options[:environment]) || core_client.environments.first(name: options[:environment])
85
+ end
86
+
87
+ def core_server_for(options={})
88
+ operator = options.fetch(:operator, core_client)
89
+ operator.servers.get(options[:server]) || operator.servers.first(provisioned_id: options[:server])
90
+ end
91
+
92
+ def core_application_for(options={})
93
+ return nil unless options[:app]
94
+
95
+ app = begin
96
+ Integer(options[:app])
97
+ rescue
98
+ options[:app]
99
+ end
100
+
101
+ actor = options[:environment].is_a?(Ey::Core::Client::Environment) ? options[:environment].account : operator(options)
102
+
103
+ if app.is_a?(Integer)
104
+ actor.applications.get(app)
105
+ else
106
+ applications = actor.applications.all(name: app)
107
+ if applications.count == 1
108
+ applications.first
109
+ else
110
+ raise Ey::Core::Cli::AmbiguousSearch.new("Found multiple applications that matched that search. Please be more specific by specifying the account, environment, and application name.")
111
+ end
112
+ end
113
+ end
114
+ end