engineyard-serverside 1.2.2 → 1.3.0

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.
@@ -1,4 +1,5 @@
1
1
  require 'thor'
2
+ require 'pathname'
2
3
 
3
4
  module EY
4
5
  class CLI < Thor
@@ -10,50 +11,61 @@ module EY
10
11
  exit(1)
11
12
  end
12
13
 
13
- method_option :migrate, :type => :string,
14
- :desc => "Run migrations with this deploy",
15
- :aliases => ["-m"]
14
+ method_option :migrate, :type => :string,
15
+ :desc => "Run migrations with this deploy",
16
+ :aliases => ["-m"]
16
17
 
17
- method_option :branch, :type => :string,
18
- :desc => "Git ref to deploy, defaults to master. May be a branch, a tag, or a SHA",
19
- :aliases => %w[-b --ref --tag]
18
+ method_option :branch, :type => :string,
19
+ :desc => "Git ref to deploy, defaults to master. May be a branch, a tag, or a SHA",
20
+ :aliases => %w[-b --ref --tag]
20
21
 
21
- method_option :repo, :type => :string,
22
- :desc => "Remote repo to deploy",
23
- :aliases => ["-r"]
22
+ method_option :repo, :type => :string,
23
+ :desc => "Remote repo to deploy",
24
+ :aliases => ["-r"]
24
25
 
25
- method_option :app, :type => :string,
26
- :required => true,
27
- :desc => "Application to deploy",
28
- :aliases => ["-a"]
26
+ method_option :app, :type => :string,
27
+ :required => true,
28
+ :desc => "Application to deploy",
29
+ :aliases => ["-a"]
29
30
 
30
- method_option :framework_env, :type => :string,
31
- :required => true,
32
- :desc => "Ruby web framework environment",
33
- :aliases => ["-e"]
31
+ method_option :framework_env, :type => :string,
32
+ :required => true,
33
+ :desc => "Ruby web framework environment",
34
+ :aliases => ["-e"]
34
35
 
35
- method_option :config, :type => :string,
36
- :desc => "Additional configuration"
36
+ method_option :config, :type => :string,
37
+ :desc => "Additional configuration"
37
38
 
38
- method_option :stack, :type => :string,
39
- :desc => "Web stack (so we can restart it correctly)"
39
+ method_option :stack, :type => :string,
40
+ :desc => "Web stack (so we can restart it correctly)"
40
41
 
41
- method_option :instances, :type => :array,
42
- :desc => "Instances in cluster"
42
+ method_option :instances, :type => :array,
43
+ :desc => "Hostnames of instances to deploy to, e.g. --instances localhost app1 app2"
43
44
 
44
- method_option :verbose, :type => :boolean,
45
- :default => false,
46
- :desc => "Verbose output",
47
- :aliases => ["-v"]
45
+ method_option :instance_roles, :type => :hash,
46
+ :default => {},
47
+ :desc => "Roles of instances, keyed on hostname, comma-separated. e.g. instance1:app_master,etc instance2:db,memcached ..."
48
+
49
+ method_option :instance_names, :type => :hash,
50
+ :default => {},
51
+ :desc => "Instance names, keyed on hostname. e.g. instance1:name1 instance2:name2"
52
+
53
+ method_option :verbose, :type => :boolean,
54
+ :default => false,
55
+ :desc => "Verbose output",
56
+ :aliases => ["-v"]
48
57
 
49
58
  desc "deploy", "Deploy code from /data/<app>"
50
59
  def deploy(default_task=:deploy)
51
- EY::Server.all = parse_instances(options[:instances])
60
+ config = EY::Deploy::Configuration.new(options)
61
+ EY::Server.load_all_from_array(assemble_instance_hashes(config))
62
+
52
63
  EY::LoggedOutput.verbose = options[:verbose]
53
64
  EY::LoggedOutput.logfile = File.join(ENV['HOME'], "#{options[:app]}-deploy.log")
54
65
 
55
66
  invoke :propagate
56
- EY::Deploy.run(options.merge("default_task" => default_task))
67
+
68
+ EY::Deploy.new(config).send(default_task)
57
69
  end
58
70
 
59
71
  method_option :app, :type => :string,
@@ -65,8 +77,8 @@ module EY
65
77
  :desc => "Value for #release_path in hooks (mostly for internal coordination)",
66
78
  :aliases => ["-r"]
67
79
 
68
- method_option :current_role, :type => :string,
69
- :desc => "Value for #current_role in hooks"
80
+ method_option :current_roles, :type => :array,
81
+ :desc => "Value for #current_roles in hooks"
70
82
 
71
83
  method_option :framework_env, :type => :string,
72
84
  :required => true,
@@ -84,6 +96,70 @@ module EY
84
96
  EY::DeployHook.new(options).run(hook_name)
85
97
  end
86
98
 
99
+
100
+ method_option :app, :type => :string,
101
+ :required => true,
102
+ :desc => "Application to deploy",
103
+ :aliases => ["-a"]
104
+
105
+ method_option :framework_env, :type => :string,
106
+ :required => true,
107
+ :desc => "Ruby web framework environment",
108
+ :aliases => ["-e"]
109
+
110
+ method_option :stack, :type => :string,
111
+ :desc => "Web stack (so we can restart it correctly)"
112
+
113
+ method_option :instances, :type => :array,
114
+ :desc => "Hostnames of instances to deploy to, e.g. --instances localhost app1 app2"
115
+
116
+ method_option :instance_roles, :type => :hash,
117
+ :default => {},
118
+ :desc => "Roles of instances, keyed on hostname, comma-separated. e.g. instance1:app_master,etc instance2:db,memcached ..."
119
+
120
+ method_option :instance_names, :type => :hash,
121
+ :default => {},
122
+ :desc => "Instance names, keyed on hostname. e.g. instance1:name1 instance2:name2"
123
+
124
+ method_option :verbose, :type => :boolean,
125
+ :default => false,
126
+ :desc => "Verbose output",
127
+ :aliases => ["-v"]
128
+ desc "integrate", "Integrate other instances into this cluster"
129
+ def integrate
130
+ EY::LoggedOutput.verbose = options[:verbose]
131
+ EY::LoggedOutput.logfile = File.join(ENV['HOME'], "#{options[:app]}-integrate.log")
132
+
133
+ app_dir = Pathname.new "/data/#{options[:app]}"
134
+ current_app_dir = app_dir + "current"
135
+
136
+ # so that we deploy to the same place there that we have here
137
+ integrate_options = options.dup
138
+ integrate_options[:release_path] = current_app_dir.realpath.to_s
139
+
140
+ # we have to deploy the same SHA there as here
141
+ integrate_options[:branch] = (current_app_dir + 'REVISION').read.strip
142
+
143
+ config = EY::Deploy::Configuration.new(integrate_options)
144
+
145
+ EY::Server.load_all_from_array(assemble_instance_hashes(config))
146
+
147
+ invoke :propagate
148
+
149
+ EY::Server.all.each do |server|
150
+ server.sync_directory app_dir
151
+ # we're just about to recreate this, so it has to be gone
152
+ # first. otherwise, non-idempotent deploy hooks could screw
153
+ # things up, and since we don't control deploy hooks, we must
154
+ # assume the worst.
155
+ server.run("rm -rf #{current_app_dir}")
156
+ end
157
+
158
+ # deploy local-ref to other instances into /data/$app/local-current
159
+ EY::Deploy.new(config).cached_deploy
160
+ end
161
+
162
+
87
163
  desc "install_bundler [VERSION]", "Make sure VERSION of bundler is installed (in system ruby)"
88
164
  def install_bundler(version)
89
165
  egrep_escaped_version = version.gsub(/\./, '\.')
@@ -99,7 +175,7 @@ module EY
99
175
  end
100
176
  end
101
177
 
102
- desc "propagate", "Propagate the engineyard-serverside gem to the other instances in the cluster. This will install exactly version #{VERSION} and remove other versions if found."
178
+ desc "propagate", "Propagate the engineyard-serverside gem to the other instances in the cluster. This will install exactly version #{VERSION}."
103
179
  def propagate
104
180
  config = EY::Deploy::Configuration.new
105
181
  gem_filename = "engineyard-serverside-#{VERSION}.gem"
@@ -107,8 +183,6 @@ module EY
107
183
  remote_gem_file = File.join(Dir.tmpdir, gem_filename)
108
184
  gem_binary = File.join(Gem.default_bindir, 'gem')
109
185
 
110
- EY::Server.config = config
111
-
112
186
  barrier(*(EY::Server.all.find_all do |server|
113
187
  !server.local? # of course this machine has it
114
188
  end.map do |server|
@@ -135,12 +209,14 @@ module EY
135
209
 
136
210
  private
137
211
 
138
- def parse_instances(instance_strings)
139
- instance_strings.map do |s|
140
- tuple = s.split(/,/)
141
- {:hostname => tuple[0], :role => tuple[1], :name => tuple[2]}
142
- end
212
+ def assemble_instance_hashes(config)
213
+ options[:instances].collect { |hostname|
214
+ { :hostname => hostname,
215
+ :roles => options[:instance_roles][hostname].to_s.split(','),
216
+ :name => options[:instance_names][hostname],
217
+ :user => config.user,
218
+ }
219
+ }
143
220
  end
144
-
145
221
  end
146
222
  end
@@ -84,6 +84,10 @@ module EY
84
84
  node['instance_role']
85
85
  end
86
86
 
87
+ def current_role
88
+ current_roles.first
89
+ end
90
+
87
91
  def copy_exclude
88
92
  @copy_exclude ||= Array(configuration.fetch("copy_exclude", []))
89
93
  end
@@ -11,6 +11,11 @@ module EY
11
11
  def deploy
12
12
  debug "Starting deploy at #{Time.now.asctime}"
13
13
  update_repository_cache
14
+ cached_deploy
15
+ end
16
+
17
+ def cached_deploy
18
+ debug "Deploying app from cached copy at #{Time.now.asctime}"
14
19
  require_custom_tasks
15
20
  push_code
16
21
 
@@ -92,7 +97,7 @@ module EY
92
97
  def push_code
93
98
  info "~> Pushing code to all servers"
94
99
  barrier *(EY::Server.all.map do |server|
95
- need_later { server.push_code }
100
+ need_later { server.sync_directory(config.repository_cache) }
96
101
  end)
97
102
  end
98
103
 
@@ -230,7 +235,7 @@ module EY
230
235
  run Escape.shell_command(base_command) do |server, cmd|
231
236
  per_instance_args = [
232
237
  '--framework-env', c.environment.to_s,
233
- '--current-role', server.role.to_s,
238
+ '--current-roles', server.roles.join(' '),
234
239
  '--config', c.to_json,
235
240
  ]
236
241
  per_instance_args << '--current-name' << server.name.to_s if server.name
@@ -318,16 +323,11 @@ module EY
318
323
  end # DeployBase
319
324
 
320
325
  class Deploy < DeployBase
321
- def self.new(opts={})
326
+ def self.new(config)
322
327
  # include the correct fetch strategy
323
- include EY::Strategies.const_get(opts.strategy)::Helpers
328
+ include EY::Strategies.const_get(config.strategy)::Helpers
324
329
  super
325
330
  end
326
331
 
327
- def self.run(opts={})
328
- conf = EY::Deploy::Configuration.new(opts)
329
- EY::Server.config = conf
330
- new(conf).send(opts["default_task"])
331
- end
332
332
  end
333
333
  end
@@ -68,7 +68,7 @@ module EY
68
68
 
69
69
  private
70
70
  def on_roles(desired_roles)
71
- yield if desired_roles.include?(current_role.to_s)
71
+ yield if desired_roles.any? { |role| current_roles.include?(role) }
72
72
  end
73
73
 
74
74
  end
@@ -2,64 +2,87 @@ require 'open-uri'
2
2
  require 'engineyard-serverside/logged_output'
3
3
 
4
4
  module EY
5
- class Server < Struct.new(:hostname, :role, :name)
5
+ class Server < Struct.new(:hostname, :roles, :name, :user)
6
6
  include LoggedOutput
7
7
 
8
- def initialize(*fields)
9
- super
10
- self.role = self.role.to_sym
11
- end
12
-
13
- def self.config=(config)
14
- @@config = config
8
+ class DuplicateHostname < StandardError
9
+ def initialize(hostname)
10
+ super "There is already an EY::Server with hostname '#{hostname}'"
11
+ end
15
12
  end
16
13
 
17
- def config
18
- @@config
14
+ def initialize(*fields)
15
+ super
16
+ self.roles = self.roles.map { |r| r.to_sym } if self.roles
19
17
  end
20
18
 
21
19
  attr_writer :default_task
22
20
 
23
- def self.from_roles(*roles)
24
- roles = roles.flatten.compact
25
- return all if !roles || roles.include?(:all) || roles.empty?
21
+ def self.from_roles(*want_roles)
22
+ want_roles = want_roles.flatten.compact.map{|r| r.to_sym}
23
+ return all if !want_roles || want_roles.include?(:all) || want_roles.empty?
26
24
 
27
25
  all.select do |s|
28
- roles.include?(s.role) || roles.include?(s.name)
26
+ !(s.roles & want_roles).empty?
29
27
  end
30
28
  end
31
29
 
32
- def self.from_hash(h)
33
- new(h[:hostname], h[:role], h[:name])
30
+ def role
31
+ roles.first
32
+ end
33
+
34
+ def self.load_all_from_array(server_hashes)
35
+ server_hashes.each do |instance_hash|
36
+ add(instance_hash)
37
+ end
34
38
  end
35
39
 
36
40
  def self.all
37
41
  @all
38
42
  end
39
43
 
40
- def self.all=(server_hashes)
41
- @all = server_hashes.map { |s| from_hash(s) }
44
+ def self.by_hostname(hostname)
45
+ all.find{|s| s.hostname == hostname}
46
+ end
47
+
48
+ def self.add(server_hash)
49
+ hostname = server_hash[:hostname]
50
+ if by_hostname(hostname)
51
+ raise DuplicateHostname.new(hostname)
52
+ end
53
+ server = new(hostname, server_hash[:roles], server_hash[:name], server_hash[:user])
54
+ @all << server
55
+ server
42
56
  end
43
57
 
44
58
  def self.current
45
59
  all.find {|s| s.local? }
46
60
  end
47
61
 
62
+ def self.reset
63
+ @all = []
64
+ end
65
+ reset
66
+
67
+ def roles=(roles)
68
+ super(roles.map{|r| r.to_sym})
69
+ end
70
+
48
71
  def local?
49
- [:app_master, :solo].include?(role)
72
+ hostname == 'localhost'
50
73
  end
51
74
 
52
- def push_code
75
+ def sync_directory(directory)
53
76
  return if local?
54
- run "mkdir -p #{config.repository_cache}"
55
- logged_system(%|rsync --delete -aq -e "#{ssh_command}" #{config.repository_cache}/ #{config.user}@#{hostname}:#{config.repository_cache}|)
77
+ run "mkdir -p #{directory}"
78
+ logged_system(%|rsync --delete -aq -e "#{ssh_command}" #{directory}/ #{user}@#{hostname}:#{directory}|)
56
79
  end
57
80
 
58
81
  def run(command)
59
82
  if local?
60
83
  logged_system(command)
61
84
  else
62
- logged_system(ssh_command + " " + Escape.shell_command(["#{config.user}@#{hostname}", command]))
85
+ logged_system(ssh_command + " " + Escape.shell_command(["#{user}@#{hostname}", command]))
63
86
  end
64
87
  end
65
88
 
@@ -1,3 +1,3 @@
1
1
  module EY
2
- VERSION = '1.2.2'
2
+ VERSION = '1.3.0'
3
3
  end
@@ -53,7 +53,7 @@ describe "the EY::Deploy API" do
53
53
  end
54
54
 
55
55
  before(:each) do
56
- @tempdir = `mktemp -d -t custom_deploy_spec`.strip
56
+ @tempdir = `mktemp -d -t custom_deploy_spec.XXXXX`.strip
57
57
  @config = EY::Deploy::Configuration.new('repository_cache' => @tempdir)
58
58
  @deploy = TestQuietDeploy.new(@config)
59
59
  end
@@ -130,6 +130,8 @@ describe "the deploy-hook API" do
130
130
  {:instance_role => 'app'},
131
131
  {:instance_role => 'db_master'},
132
132
  {:instance_role => 'db_slave'},
133
+ {:instance_role => 'multi_role,app'},
134
+ {:instance_role => 'multi,util'},
133
135
  {:instance_role => 'util', :name => "alpha"},
134
136
  {:instance_role => 'util', :name => "beta"},
135
137
  {:instance_role => 'util', :name => "gamma"},
@@ -138,7 +140,7 @@ describe "the deploy-hook API" do
138
140
 
139
141
  def where_code_runs_with(method, *args)
140
142
  scenarios.map do |s|
141
- @callback_context.configuration[:current_role] = s[:instance_role]
143
+ @callback_context.configuration[:current_roles] = s[:instance_role].split(',')
142
144
  @callback_context.configuration[:current_name] = s[:name]
143
145
 
144
146
  if run_hook { send(method, *args) { 'ran!'} } == 'ran!'
@@ -154,17 +156,17 @@ describe "the deploy-hook API" do
154
156
  end
155
157
 
156
158
  it "#on_app_servers runs on app masters, app slaves, and solos" do
157
- where_code_runs_with(:on_app_servers).should == %w(solo app_master app)
159
+ where_code_runs_with(:on_app_servers).should == %w(solo app_master app multi_role,app)
158
160
  end
159
161
 
160
162
  it "#on_app_servers_and_utilities does what it says on the tin" do
161
163
  where_code_runs_with(:on_app_servers_and_utilities).should ==
162
- %w(solo app_master app util_alpha util_beta util_gamma)
164
+ %w(solo app_master app multi_role,app multi,util util_alpha util_beta util_gamma)
163
165
  end
164
166
 
165
167
  it "#on_utilities() runs on all utility instances" do
166
168
  where_code_runs_with(:on_utilities).should ==
167
- %w(util_alpha util_beta util_gamma)
169
+ %w(multi,util util_alpha util_beta util_gamma)
168
170
  end
169
171
 
170
172
  it "#on_utilities('sometype') runs on only utilities of type 'sometype'" do
@@ -196,4 +198,10 @@ describe "the deploy-hook API" do
196
198
  @hook.syntax_error(hook_path).should =~ match
197
199
  end
198
200
  end
201
+
202
+ context "is compatible with older deploy hook scripts" do
203
+ it "#current_role returns the first role" do
204
+ run_hook(:current_roles => %w(a b)) { current_role.should == 'a' }
205
+ end
206
+ end
199
207
  end
@@ -105,7 +105,8 @@ describe "deploying an application" do
105
105
  @deploy_dir = File.join(Dir.tmpdir, "serverside-deploy-#{Time.now.to_i}-#{$$}")
106
106
 
107
107
  # set up EY::Server like we're on a solo
108
- EY::Server.all = [{:hostname => 'dontcare', :role => 'solo'}]
108
+ EY::Server.reset
109
+ EY::Server.add(:hostname => 'localhost', :roles => %w[solo])
109
110
 
110
111
  # run a deploy
111
112
  config = EY::Deploy::Configuration.new({
@@ -0,0 +1,76 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe EY::Server do
4
+ before(:each) do
5
+ EY::Server.reset
6
+ end
7
+
8
+ context ".all" do
9
+ it "starts off empty" do
10
+ EY::Server.all.should be_empty
11
+ end
12
+
13
+ it "is added to with .add" do
14
+ EY::Server.add(:hostname => 'otherhost', :roles => %w[fire water])
15
+ EY::Server.all.size.should == 1
16
+
17
+ EY::Server.by_hostname('otherhost').should_not be_nil
18
+ end
19
+
20
+ it "rejects duplicates" do
21
+ EY::Server.add(:hostname => 'otherhost')
22
+ lambda do
23
+ EY::Server.add(:hostname => 'otherhost')
24
+ end.should raise_error(EY::Server::DuplicateHostname)
25
+ end
26
+ end
27
+
28
+ it "makes sure your roles are symbols at creation time" do
29
+ EY::Server.add(:hostname => 'otherhost', :roles => ['beerguy'])
30
+
31
+ EY::Server.by_hostname('otherhost').roles.should == [:beerguy]
32
+ end
33
+
34
+ it "makes sure your roles are symbols when updated" do
35
+ EY::Server.add(:hostname => 'otherhost')
36
+
37
+ server = EY::Server.by_hostname('otherhost')
38
+ server.roles = %w[bourbon scotch beer]
39
+ server.roles.should == [:bourbon, :scotch, :beer]
40
+ end
41
+
42
+ context ".from_roles" do
43
+ before(:each) do
44
+ @localhost = EY::Server.add(:hostname => 'localhost', :roles => [:ice, :cold])
45
+ @host1 = EY::Server.add(:hostname => 'host1', :roles => [:fire, :water])
46
+ @host2 = EY::Server.add(:hostname => 'host2', :roles => [:ice, :water])
47
+ end
48
+
49
+ it "works with strings or symbols" do
50
+ EY::Server.from_roles(:fire).should == [@host1]
51
+ EY::Server.from_roles('fire').should == [@host1]
52
+ end
53
+
54
+ it "finds all servers with the specified role" do
55
+ EY::Server.from_roles('ice').size.should == 2
56
+ EY::Server.from_roles('ice').sort do |a, b|
57
+ a.hostname <=> b.hostname
58
+ end.should == [@host2, @localhost]
59
+ end
60
+
61
+ it "finds all servers with any of the specified roles" do
62
+ EY::Server.from_roles(:ice, :water).should == EY::Server.all
63
+ end
64
+
65
+ it "returns everything when asked for :all" do
66
+ EY::Server.from_roles(:all).should == EY::Server.all
67
+ end
68
+ end
69
+
70
+ context "#local?" do
71
+ it "is true only for localhost" do
72
+ EY::Server.new('localhost').should be_local
73
+ EY::Server.new('neighborhost').should_not be_local
74
+ end
75
+ end
76
+ end
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
- - 2
9
- - 2
10
- version: 1.2.2
8
+ - 3
9
+ - 0
10
+ version: 1.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - EY Cloud Team
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-31 00:00:00 -07:00
18
+ date: 2010-09-13 00:00:00 -07:00
19
19
  default_executable: engineyard-serverside
20
20
  dependencies: []
21
21
 
@@ -242,6 +242,7 @@ files:
242
242
  - spec/git_strategy_spec.rb
243
243
  - spec/lockfile_parser_spec.rb
244
244
  - spec/real_deploy_spec.rb
245
+ - spec/server_spec.rb
245
246
  - spec/spec_helper.rb
246
247
  - spec/support/lockfiles/0.9-no-bundler
247
248
  - spec/support/lockfiles/0.9-with-bundler
@@ -292,6 +293,7 @@ test_files:
292
293
  - spec/git_strategy_spec.rb
293
294
  - spec/lockfile_parser_spec.rb
294
295
  - spec/real_deploy_spec.rb
296
+ - spec/server_spec.rb
295
297
  - spec/spec_helper.rb
296
298
  - spec/support/lockfiles/0.9-no-bundler
297
299
  - spec/support/lockfiles/0.9-with-bundler