engineyard-serverside 2.1.4 → 2.2.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,7 +26,6 @@ require 'engineyard-serverside/cli'
26
26
  require 'engineyard-serverside/configuration'
27
27
  require 'engineyard-serverside/deprecation'
28
28
  require 'engineyard-serverside/shell'
29
- require 'engineyard-serverside/propagator'
30
29
 
31
30
 
32
31
  module EY
@@ -1,5 +1,6 @@
1
1
  require 'thor'
2
2
  require 'pathname'
3
+ require 'engineyard-serverside/about'
3
4
  require 'engineyard-serverside/deploy'
4
5
  require 'engineyard-serverside/shell'
5
6
  require 'engineyard-serverside/servers'
@@ -8,6 +9,7 @@ require 'engineyard-serverside/cli_helpers'
8
9
  module EY
9
10
  module Serverside
10
11
  class CLI < Thor
12
+
11
13
  extend CLIHelpers
12
14
 
13
15
  method_option :migrate, :type => :string,
@@ -99,15 +101,12 @@ module EY
99
101
 
100
102
  # We have to rsync the entire app dir, so we need all the permissions to be correct!
101
103
  chown_command = "find #{app_dir} -not -user #{config.user} -or -not -group #{config.group} -exec chown #{config.user}:#{config.group} {} +"
102
- shell.logged_system "sudo sh -l -c '#{chown_command}'"
103
-
104
- servers.each do |server|
105
- shell.logged_system server.sync_directory_command(app_dir)
106
- # we're just about to recreate this, so it has to be gone
107
- # first. otherwise, non-idempotent deploy hooks could screw
108
- # things up, and since we don't control deploy hooks, we must
109
- # assume the worst.
110
- shell.logged_system server.command_on_server('sh -l -c', "rm -rf #{current_app_dir}")
104
+ shell.logged_system("sudo sh -l -c '#{chown_command}'", servers.detect {|s| s.local?})
105
+
106
+ servers.run_for_each! do |server|
107
+ sync = server.sync_directory_command(app_dir)
108
+ clean = server.command_on_server('sh -l -c', "rm -rf #{current_app_dir}")
109
+ "(#{sync}) && (#{clean})"
111
110
  end
112
111
 
113
112
  # deploy local-ref to other instances into /data/$app/local-current
@@ -130,8 +129,8 @@ module EY
130
129
 
131
130
  def init_and_propagate(*args)
132
131
  init(*args) do |config, shell|
133
- servers = load_servers(config)
134
- Propagator.call(servers, config, shell)
132
+ servers = load_servers(config, shell)
133
+ propagate(servers, shell)
135
134
  yield servers, config, shell
136
135
  end
137
136
  end
@@ -154,8 +153,28 @@ module EY
154
153
  end
155
154
  end
156
155
 
157
- def load_servers(config)
158
- EY::Serverside::Servers.from_hashes(assemble_instance_hashes(config))
156
+ def propagate(servers, shell)
157
+ shell.status "Verifying and propagating #{About.name_with_version} to all servers."
158
+
159
+ gem_binary = File.join(Gem.default_bindir, 'gem')
160
+ remote_gem_file = File.join(Dir.tmpdir, About.gem_filename)
161
+
162
+ # the [,)] is to stop us from looking for e.g. 0.5.1, seeing
163
+ # 0.5.11, and mistakenly thinking 0.5.1 is there
164
+ check_command = %{#{gem_binary} list #{About.gem_name} | grep "#{About.gem_name}" | egrep -q "#{About.version.gsub(/\./, '\.')}[,)]"}
165
+ install_command = "#{gem_binary} install --no-rdoc --no-ri '#{remote_gem_file}'"
166
+
167
+ servers.remote.run_for_each! do |server|
168
+ check = server.command_on_server('sh -l -c', check_command)
169
+ scp = server.scp_command(About.gem_file, remote_gem_file)
170
+ install = server.command_on_server('sudo sh -l -c', install_command)
171
+
172
+ "(#{check}) || ((#{scp}) && (#{install}))"
173
+ end
174
+ end
175
+
176
+ def load_servers(config, shell)
177
+ EY::Serverside::Servers.from_hashes(assemble_instance_hashes(config), shell)
159
178
  end
160
179
 
161
180
  def assemble_instance_hashes(config)
@@ -60,7 +60,7 @@ module EY
60
60
  def_option :precompile_assets, 'detect'
61
61
  def_option :precompile_assets_task, 'assets:precompile'
62
62
  def_option :asset_strategy, 'shifting'
63
- def_option :asset_dependencies, %w[app/assets lib/assets vendor/assets Gemfile.lock config/routes.rb]
63
+ def_option :asset_dependencies, %w[app/assets lib/assets vendor/assets Gemfile.lock config/routes.rb config/application.rb]
64
64
  def_option :stack, nil
65
65
  def_option :strategy, 'Git'
66
66
  def_option :branch, 'master'
@@ -42,11 +42,11 @@ To fix this problem, commit your composer.lock to the repository and redeploy.
42
42
  end
43
43
 
44
44
  def composer_install
45
- run "composer install --no-interaction --working-dir #{paths.active_release}"
45
+ run "composer install --no-interaction --working-dir #{paths.active_release}"
46
46
  end
47
47
 
48
48
  def composer_selfupdate
49
- run "composer self-update"
49
+ run "composer self-update"
50
50
  end
51
51
 
52
52
  def composer_available?
@@ -106,9 +106,8 @@ module EY
106
106
  # task
107
107
  def push_code
108
108
  shell.status "Pushing code to all servers"
109
- servers.remote.run_on_each(shell) do |server|
110
- cmd = server.sync_directory_command(paths.repository_cache)
111
- shell.logged_system(cmd)
109
+ servers.remote.run_for_each do |server|
110
+ server.sync_directory_command(paths.repository_cache)
112
111
  end
113
112
  end
114
113
 
@@ -149,7 +148,7 @@ module EY
149
148
  path = paths.ssh_wrapper
150
149
  <<-SCRIPT
151
150
  mkdir -p #{path.dirname}
152
- [[ -x #{path} ]] || cat > #{path} <<'SSH'
151
+ [ -x #{path} ] || cat > #{path} <<'SSH'
153
152
  #!/bin/sh
154
153
  unset SSH_AUTH_SOCK
155
154
  ssh -o CheckHostIP=no -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o LogLevel=INFO -o IdentityFile=#{paths.deploy_key} -o IdentitiesOnly=yes $*
@@ -68,7 +68,7 @@ module EY
68
68
  end
69
69
 
70
70
  def run(cmd)
71
- @servers.roles(:app_master, :app, :solo).run(shell, cmd)
71
+ @servers.roles(:app_master, :app, :solo).run(cmd)
72
72
  end
73
73
 
74
74
  def paths
@@ -86,10 +86,15 @@ module EY
86
86
  run_precompile_assets_task
87
87
 
88
88
  shell.warning <<-WARN
89
- Inferred asset compilation succeeded, but failures may be silently ignored!
89
+ Assets were detected and precompiled for this application, but asset precompile
90
+ failures may be silently ignored in the future without updating config/ey.yml.
90
91
 
91
- ACTION REQUIRED: Add precompile_assets option to ey.yml.
92
- precompile_assets: true # precompile assets when asset changes detected
92
+ ACTION REQUIRED: Add this line to config/ey.yml to ensure assets are compiled
93
+ every deploy and deploys are halted if there is an asset compilation failure.
94
+
95
+ precompile_assets: true # precompile assets when assets are changed.
96
+
97
+ This warning will continue to show until you update and commit config/ey.yml.
93
98
  WARN
94
99
  rescue EY::Serverside::RemoteFailure => e
95
100
  # If we are implicitly precompiling, we want to fail non-destructively
@@ -42,6 +42,18 @@ module EY
42
42
  ].join(' && ')
43
43
  end
44
44
 
45
+ def scp_command(local_file, remote_file)
46
+ Escape.shell_command([
47
+ 'scp',
48
+ '-i', "#{ENV['HOME']}/.ssh/internal",
49
+ "-o", "StrictHostKeyChecking=no",
50
+ "-o", "UserKnownHostsFile=#{self.class.known_hosts_file.path}",
51
+ "-o", "PasswordAuthentication=no",
52
+ local_file,
53
+ "#{authority}:#{remote_file}",
54
+ ])
55
+ end
56
+
45
57
  def command_on_server(prefix, cmd, &block)
46
58
  command = block ? block.call(self, cmd.dup) : cmd
47
59
  command = "#{prefix} #{Escape.shell_command([command])}"
@@ -1,6 +1,7 @@
1
1
  require 'engineyard-serverside/server'
2
2
  require 'forwardable'
3
3
  require 'set'
4
+ require 'engineyard-serverside/spawner'
4
5
 
5
6
  module EY
6
7
  module Serverside
@@ -16,24 +17,25 @@ module EY
16
17
  extend Forwardable
17
18
  include Enumerable
18
19
  def_delegators :@servers, :each, :size, :empty?
19
- def select(*a, &b) self.class.new @servers.select(*a,&b) end
20
- def reject(*a, &b) self.class.new @servers.reject(*a,&b) end
20
+ def select(*a, &b) self.class.new @servers.select(*a,&b), @shell end
21
+ def reject(*a, &b) self.class.new @servers.reject(*a,&b), @shell end
21
22
  def to_a() @servers end
22
23
  def ==(other) other.respond_to?(:to_a) && other.to_a == to_a end
23
24
 
24
25
 
25
- def self.from_hashes(server_hashes)
26
+ def self.from_hashes(server_hashes, shell)
26
27
  servers = server_hashes.inject({}) do |memo, server_hash|
27
28
  server = Server.from_hash(server_hash)
28
29
  raise DuplicateHostname.new(server.hostname) if memo.key?(server.hostname)
29
30
  memo[server.hostname] = server
30
31
  memo
31
32
  end
32
- new(servers.values)
33
+ new(servers.values, shell)
33
34
  end
34
35
 
35
- def initialize(servers)
36
+ def initialize(servers, shell)
36
37
  @servers = servers
38
+ @shell = shell
37
39
  @cache = {}
38
40
  end
39
41
 
@@ -62,40 +64,43 @@ module EY
62
64
  end
63
65
  end
64
66
 
67
+ def run_on_each(cmd, &block)
68
+ run_for_each do |server|
69
+ server.command_on_server('sh -l -c', cmd, &block)
70
+ end
71
+ end
72
+
65
73
  # Run a command on this set of servers.
66
- def run(shell, cmd, &block)
67
- run_on_each(shell) do |server|
68
- exec_cmd = server.command_on_server('sh -l -c', cmd, &block)
69
- shell.logged_system(exec_cmd, server)
74
+ def run_on_each!(cmd, &block)
75
+ run_for_each! do |server|
76
+ server.command_on_server('sh -l -c', cmd, &block)
70
77
  end
71
78
  end
79
+ alias run run_on_each!
72
80
 
73
81
  # Run a sudo command on this set of servers.
74
- def sudo(shell, cmd, &block)
75
- run_on_each(shell) do |server|
76
- exec_cmd = server.command_on_server('sudo sh -l -c', cmd, &block)
77
- shell.logged_system(exec_cmd, server)
82
+ def sudo_on_each(cmd, &block)
83
+ run_for_each do |server|
84
+ server.command_on_server('sudo sh -l -c', cmd, &block)
78
85
  end
79
86
  end
80
87
 
81
- # Makes a thread for each server and executes the block,
82
- # returning an array of return values
83
- def map_in_parallel(&block)
84
- threads = map { |server| Thread.new { block.call(server) } }
85
- threads.map { |t| t.value }
88
+ # Run a sudo command on this set of servers.
89
+ def sudo_on_each!(cmd, &block)
90
+ run_for_each! do |server|
91
+ server.command_on_server('sudo sh -l -c', cmd, &block)
92
+ end
86
93
  end
94
+ alias sudo sudo_on_each!
87
95
 
88
- def select_in_parallel(&block)
89
- results = map_in_parallel { |server| block.call(server) ? server : nil }.compact
90
- self.class.new results
96
+ def run_for_each(&block)
97
+ spawner = Spawner.new
98
+ each { |server| spawner.add(block.call(server), @shell, server) }
99
+ spawner.run
91
100
  end
92
101
 
93
- # Makes a theard for each server and executes the block,
94
- # Assumes that the return value of the block is a CommandResult
95
- # and ensures that all the command results were successful.
96
- def run_on_each(shell, &block)
97
- results = map_in_parallel(&block)
98
- failures = results.reject {|result| result.success? }
102
+ def run_for_each!(&block)
103
+ failures = run_for_each(&block).reject {|result| result.success? }
99
104
 
100
105
  if failures.any?
101
106
  commands = failures.map { |f| f.command }.uniq
@@ -1,6 +1,6 @@
1
1
  require 'logger'
2
2
  require 'pathname'
3
- require 'systemu'
3
+ require 'open3'
4
4
  require 'engineyard-serverside/shell/formatter'
5
5
  require 'engineyard-serverside/shell/command_result'
6
6
  require 'engineyard-serverside/shell/yieldio'
@@ -64,21 +64,9 @@ module EY
64
64
  def command_err(msg) unknown msg.gsub(/^/,' ') end
65
65
 
66
66
  def logged_system(cmd, server = nil)
67
- command_show(cmd)
68
- output = ""
69
- outio = YieldIO.new { |msg| output << msg; command_out(msg) }
70
- errio = YieldIO.new { |msg| output << msg; command_err(msg) }
71
- result = spawn_process(cmd, outio, errio)
72
- CommandResult.new(cmd, result.exitstatus, output, server)
67
+ EY::Serverside::Spawner.run(cmd, self, server)
73
68
  end
74
69
 
75
- protected
76
-
77
- # This is the meat of process spawning. It's nice to keep it separate even
78
- # though it's simple because we've had to modify it frequently.
79
- def spawn_process(cmd, outio, errio)
80
- systemu cmd, 'stdout' => outio, 'stderr' => errio
81
- end
82
70
  end
83
71
  end
84
72
  end
@@ -1,17 +1,13 @@
1
1
  module EY
2
2
  module Serverside
3
3
  class Shell
4
- class CommandResult < Struct.new(:command, :exitstatus, :output, :server)
5
- def success?
6
- exitstatus.to_i == 0
7
- end
4
+ class CommandResult < Struct.new(:command, :success, :output, :server)
5
+ alias success? success
8
6
 
9
7
  def inspect
10
8
  <<-EOM
11
- $ #{command}
9
+ $ #{success? ? "(success)" : "(failed)"} #{command}
12
10
  #{output}
13
-
14
- ($?: #{exitstatus})
15
11
  EOM
16
12
  end
17
13
  end
@@ -0,0 +1,187 @@
1
+ require 'open3'
2
+ require 'engineyard-serverside/spawner'
3
+
4
+ module EY
5
+ module Serverside
6
+ class Spawner
7
+ def self.run(cmd, shell, server = nil)
8
+ s = new
9
+ s.add(cmd, shell, server)
10
+ s.run.first
11
+ end
12
+
13
+ def initialize
14
+ @poll_period = 0.5
15
+ @children = []
16
+ end
17
+
18
+ def add(cmd, shell, server = nil)
19
+ @children << Child.new(cmd, shell, server)
20
+ end
21
+
22
+ def run
23
+ @child_by_fd = {}
24
+ @child_by_pid = {}
25
+
26
+ @children.each do |child|
27
+ pid, stdout_fd, stderr_fd = child.spawn
28
+ @child_by_pid[pid] = child
29
+ @child_by_fd[stdout_fd] = child
30
+ @child_by_fd[stderr_fd] = child
31
+ end
32
+
33
+ while @child_by_pid.any?
34
+ process
35
+ wait
36
+ end
37
+
38
+ @children.map { |child| child.result }
39
+ end
40
+
41
+ protected
42
+
43
+ def process
44
+ read_fds = @child_by_pid.values.map {|child| child.ios }.flatten.compact
45
+ ra, _, _ = IO.select(read_fds, [], [], @poll_period)
46
+ process_readable(ra) if ra
47
+ end
48
+
49
+ def process_readable(ra)
50
+ ra.each do |fd|
51
+ child = @child_by_fd[fd]
52
+ if !child
53
+ raise "Select returned unknown fd: #{fd.inspect}"
54
+ end
55
+
56
+ begin
57
+ if buf = fd.sysread(4096)
58
+ child.append_to_buffer(fd, buf)
59
+ else
60
+ raise "sysread() returned nil"
61
+ end
62
+ rescue SystemCallError, EOFError => e
63
+ @child_by_fd.delete(fd)
64
+ child.close(fd)
65
+ end
66
+ end
67
+ end
68
+
69
+ def wait
70
+ possible_children = true
71
+ just_reaped = []
72
+ while possible_children
73
+ begin
74
+ pid, status = Process::waitpid2(-1, Process::WNOHANG)
75
+ if pid.nil?
76
+ possible_children = false
77
+ else
78
+ child = @child_by_pid.delete(pid)
79
+ child.finished status
80
+ just_reaped << child
81
+ end
82
+ rescue Errno::ECHILD
83
+ possible_children = false
84
+ end
85
+ end
86
+ # We may have waited on a child before reading all its output. Collect those missing bits. No blocking.
87
+ if just_reaped.any?
88
+ read_fds = just_reaped.map {|child| child.ios }.flatten.compact
89
+ ra, _, _ = IO.select(read_fds, nil, nil, 0)
90
+ process_readable(ra) if ra
91
+ end
92
+ end
93
+
94
+ class Child
95
+ attr_reader :stdout_fd, :stderr_fd
96
+
97
+ def initialize(command, shell, server = nil)
98
+ @command = command
99
+ @shell = shell
100
+ @server = server
101
+ @output = ""
102
+ end
103
+
104
+ def spawn
105
+ @shell.command_show @command
106
+ #stdin, @stdout_fd, @stderr_fd, @waitthr = Open3.popen3(@cmd)
107
+ #stdin.close
108
+
109
+ stdin_rd, stdin_wr = IO.pipe
110
+ @stdout_fd, stdout_wr = IO.pipe
111
+ @stderr_fd, stderr_wr = IO.pipe
112
+
113
+ @pid = fork do
114
+ stdin_wr.close
115
+ @stdout_fd.close
116
+ @stderr_fd.close
117
+ STDIN.reopen(stdin_rd)
118
+ STDOUT.reopen(stdout_wr)
119
+ STDERR.reopen(stderr_wr)
120
+ Kernel.exec(@command)
121
+ raise "Exec failed!"
122
+ end
123
+
124
+ stdin_rd.close
125
+ stdin_wr.close
126
+ stdout_wr.close
127
+ stderr_wr.close
128
+
129
+ [@pid, @stdout_fd, @stderr_fd]
130
+ end
131
+
132
+ def ios
133
+ [@stdout_fd, @stderr_fd].compact
134
+ end
135
+
136
+ def finished(status)
137
+ @status = status
138
+ end
139
+
140
+ def result
141
+ if @status
142
+ Result.new(@command, @status.success?, @output, @server)
143
+ else
144
+ raise "No result from unfinished child process"
145
+ end
146
+ end
147
+
148
+ def close(fd)
149
+ case fd
150
+ when @stdout_fd then @stdout_fd = nil
151
+ when @stderr_fd then @stderr_fd = nil
152
+ end
153
+ fd.close rescue true
154
+ end
155
+
156
+ def append_to_buffer(fd,data)
157
+ case fd
158
+ when @stdout_fd
159
+ @shell.command_out data
160
+ @output << data
161
+ when @stderr_fd
162
+ @shell.command_err data
163
+ @output << data
164
+ end
165
+ end
166
+ end
167
+
168
+ class Result
169
+ attr_reader :command, :success, :output, :server
170
+ def initialize(command, success, output, server = nil)
171
+ @command = command
172
+ @success = success
173
+ @output = output
174
+ @server = server
175
+ end
176
+
177
+ alias success? success
178
+ def inspect
179
+ <<-EOM
180
+ $ #{success? ? "(success)" : "(failed)"} #{command}
181
+ #{output}
182
+ EOM
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end