engineyard-serverside 2.1.4 → 2.2.0.pre

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.
@@ -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