jtzemp-poolparty 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. data/CHANGELOG +23 -0
  2. data/LICENSE +22 -0
  3. data/README +139 -0
  4. data/Rakefile +109 -0
  5. data/assets/clouds.png +0 -0
  6. data/bin/instance +68 -0
  7. data/bin/pool +83 -0
  8. data/bin/poolnotify +34 -0
  9. data/config/cloud_master_takeover +17 -0
  10. data/config/create_proxy_ami.sh +582 -0
  11. data/config/haproxy.conf +29 -0
  12. data/config/heartbeat.conf +8 -0
  13. data/config/heartbeat_authkeys.conf +2 -0
  14. data/config/installers/ubuntu_install.sh +77 -0
  15. data/config/monit.conf +9 -0
  16. data/config/monit/haproxy.monit.conf +8 -0
  17. data/config/monit/nginx.monit.conf +0 -0
  18. data/config/nginx.conf +24 -0
  19. data/config/reconfigure_instances_script.sh +37 -0
  20. data/config/sample-config.yml +23 -0
  21. data/config/scp_instances_script.sh +12 -0
  22. data/lib/core/array.rb +16 -0
  23. data/lib/core/exception.rb +9 -0
  24. data/lib/core/float.rb +13 -0
  25. data/lib/core/hash.rb +11 -0
  26. data/lib/core/kernel.rb +12 -0
  27. data/lib/core/module.rb +22 -0
  28. data/lib/core/object.rb +21 -0
  29. data/lib/core/proc.rb +15 -0
  30. data/lib/core/string.rb +56 -0
  31. data/lib/core/time.rb +41 -0
  32. data/lib/helpers/plugin_spec_helper.rb +58 -0
  33. data/lib/modules/callback.rb +133 -0
  34. data/lib/modules/ec2_wrapper.rb +108 -0
  35. data/lib/modules/file_writer.rb +38 -0
  36. data/lib/modules/safe_instance.rb +31 -0
  37. data/lib/modules/sprinkle_overrides.rb +27 -0
  38. data/lib/modules/vlad_override.rb +83 -0
  39. data/lib/poolparty.rb +131 -0
  40. data/lib/poolparty/application.rb +199 -0
  41. data/lib/poolparty/init.rb +6 -0
  42. data/lib/poolparty/master.rb +492 -0
  43. data/lib/poolparty/monitors.rb +11 -0
  44. data/lib/poolparty/monitors/cpu.rb +23 -0
  45. data/lib/poolparty/monitors/memory.rb +33 -0
  46. data/lib/poolparty/monitors/web.rb +29 -0
  47. data/lib/poolparty/optioner.rb +20 -0
  48. data/lib/poolparty/plugin.rb +78 -0
  49. data/lib/poolparty/provider.rb +104 -0
  50. data/lib/poolparty/provider/essential.rb +6 -0
  51. data/lib/poolparty/provider/git.rb +8 -0
  52. data/lib/poolparty/provider/haproxy.rb +9 -0
  53. data/lib/poolparty/provider/heartbeat.rb +6 -0
  54. data/lib/poolparty/provider/rsync.rb +8 -0
  55. data/lib/poolparty/provider/ruby.rb +65 -0
  56. data/lib/poolparty/provider/s3fuse.rb +22 -0
  57. data/lib/poolparty/remote_instance.rb +250 -0
  58. data/lib/poolparty/remoter.rb +171 -0
  59. data/lib/poolparty/remoting.rb +137 -0
  60. data/lib/poolparty/scheduler.rb +93 -0
  61. data/lib/poolparty/tasks.rb +47 -0
  62. data/lib/poolparty/tasks/cloud.rake +57 -0
  63. data/lib/poolparty/tasks/development.rake +78 -0
  64. data/lib/poolparty/tasks/ec2.rake +20 -0
  65. data/lib/poolparty/tasks/instance.rake +63 -0
  66. data/lib/poolparty/tasks/plugins.rake +30 -0
  67. data/lib/poolparty/tasks/server.rake +42 -0
  68. data/lib/poolparty/thread_pool.rb +94 -0
  69. data/lib/s3/s3_object_store_folders.rb +44 -0
  70. data/poolparty.gemspec +71 -0
  71. data/spec/files/describe_response +37 -0
  72. data/spec/files/multi_describe_response +69 -0
  73. data/spec/files/remote_desc_response +37 -0
  74. data/spec/helpers/ec2_mock.rb +57 -0
  75. data/spec/lib/core/core_spec.rb +26 -0
  76. data/spec/lib/core/kernel_spec.rb +24 -0
  77. data/spec/lib/core/string_spec.rb +28 -0
  78. data/spec/lib/modules/callback_spec.rb +213 -0
  79. data/spec/lib/modules/file_writer_spec.rb +74 -0
  80. data/spec/lib/poolparty/application_spec.rb +135 -0
  81. data/spec/lib/poolparty/ec2_wrapper_spec.rb +110 -0
  82. data/spec/lib/poolparty/master_spec.rb +479 -0
  83. data/spec/lib/poolparty/optioner_spec.rb +34 -0
  84. data/spec/lib/poolparty/plugin_spec.rb +115 -0
  85. data/spec/lib/poolparty/poolparty_spec.rb +60 -0
  86. data/spec/lib/poolparty/provider_spec.rb +74 -0
  87. data/spec/lib/poolparty/remote_instance_spec.rb +178 -0
  88. data/spec/lib/poolparty/remoter_spec.rb +72 -0
  89. data/spec/lib/poolparty/remoting_spec.rb +148 -0
  90. data/spec/lib/poolparty/scheduler_spec.rb +70 -0
  91. data/spec/monitors/cpu_monitor_spec.rb +39 -0
  92. data/spec/monitors/memory_spec.rb +51 -0
  93. data/spec/monitors/misc_monitor_spec.rb +51 -0
  94. data/spec/monitors/web_spec.rb +40 -0
  95. data/spec/spec_helper.rb +53 -0
  96. metadata +312 -0
@@ -0,0 +1,131 @@
1
+ =begin rdoc
2
+ The main file, contains the client and the server application methods
3
+ =end
4
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
5
+
6
+ $TRACE = true
7
+
8
+ # rubygems
9
+ require 'rubygems'
10
+ require "aws/s3"
11
+ require "EC2"
12
+ require "aska"
13
+ begin
14
+ require 'crafterm-sprinkle'
15
+ rescue LoadError
16
+ require 'sprinkle'
17
+ end
18
+ require "pp"
19
+ require "tempfile"
20
+
21
+ begin
22
+ require 'fastthread'
23
+ require 'system_timer'
24
+ @@timer = SystemTimer
25
+ rescue LoadError
26
+ require 'thread'
27
+ require 'timeout'
28
+ @@timer = Timeout
29
+ end
30
+
31
+ ## Load PoolParty
32
+ pwd = File.dirname(__FILE__)
33
+
34
+ # Load the required files
35
+ # If there is an init file, load that, otherwise
36
+ # require all the files in each directory
37
+ %w(core modules s3 helpers poolparty).each do |dir|
38
+ Dir["#{pwd}/#{dir}"].each do |dir|
39
+ begin
40
+ require File.join(dir, "init")
41
+ rescue LoadError => e
42
+ Dir["#{pwd}/#{File.basename(dir)}/**"].each {|file| require File.join(dir, File.basename(file))}
43
+ end
44
+ end
45
+ end
46
+
47
+ module PoolParty
48
+ class Version #:nodoc:
49
+ @major = 0
50
+ @minor = 1
51
+ @tiny = 2
52
+
53
+ def self.string
54
+ [@major, @minor, @tiny].join('.')
55
+ end
56
+ end
57
+ def timer
58
+ @@timer
59
+ end
60
+ # PoolParty options
61
+ def options(opts={})
62
+ Application.options(opts)
63
+ end
64
+ # Are we working in verbose-mode
65
+ def verbose?
66
+ options.verbose == true
67
+ end
68
+ # Send a message if we are in verbose-mode
69
+ def message(msg="")
70
+ puts "-- #{msg}" if verbose?
71
+ end
72
+ # Root directory of the application
73
+ def root_dir
74
+ File.expand_path(File.dirname(__FILE__) + "/..")
75
+ end
76
+ # User directory
77
+ def user_dir
78
+ Application.working_directory
79
+ end
80
+ # Write string to a tempfile
81
+ def write_to_temp_file(str="")
82
+ tempfile = Tempfile.new("rand#{rand(1000)}-#{rand(1000)}")
83
+ tempfile.print(str)
84
+ tempfile.flush
85
+ tempfile
86
+ end
87
+ def register_monitor(*names)
88
+ names.each do |name|
89
+ unless registered_monitor?(name)
90
+ PoolParty::Monitors.extend name
91
+
92
+ PoolParty::Master.send :include, name::Master
93
+ PoolParty::RemoteInstance.send :include, name::Remote
94
+
95
+ registered_monitors << name
96
+ end
97
+ end
98
+ end
99
+ def registered_monitor?(name); registered_monitors.include?(name); end
100
+ def registered_monitors; @@registered_monitors ||= [];end
101
+
102
+ def load_app
103
+ load_monitors
104
+ load_plugins
105
+ end
106
+ def load_monitors
107
+ loc = File.directory?("#{user_dir}/monitors") ? "#{user_dir}/monitors" : "#{root_dir}/lib/poolparty/monitors"
108
+ Dir["#{loc}/*"].each {|f| require f}
109
+ end
110
+
111
+ def load_plugins
112
+ Dir["#{plugin_dir}/**/init.rb"].each {|a| require a} if File.directory?(plugin_dir)
113
+ end
114
+ def reset!
115
+ @@registered_monitors = nil
116
+ @@installed_plugins = nil
117
+ end
118
+ def plugin_dir
119
+ "#{user_dir}/plugins"
120
+ end
121
+ def read_config_file(filename)
122
+ return {} unless filename
123
+ YAML.load(open(filename).read)
124
+ end
125
+ def include_cloud_tasks
126
+ Tasks.new.define_tasks
127
+ end
128
+
129
+ alias_method :tasks, :include_cloud_tasks
130
+ alias_method :include_tasks, :include_cloud_tasks
131
+ end
@@ -0,0 +1,199 @@
1
+ =begin rdoc
2
+ Application
3
+ This handles user interaction
4
+ =end
5
+ module PoolParty
6
+ class Application
7
+ class << self
8
+ attr_accessor :verbose, :options
9
+
10
+ # The application options
11
+ def options(opts={})
12
+ @options ||= make_options(opts)
13
+ end
14
+ # Make the options with the config_file overrides included
15
+ # Default config file assumed to be at config/config.yml
16
+ def make_options(opts={})
17
+ loading_options = opts.delete(:optsparse) || {}
18
+ loading_options.merge!( opts || {})
19
+
20
+ load_options!(loading_options) # Load command-line options
21
+ config_file_location = (default_options[:config_file] || opts[:config_file])
22
+
23
+ # If the config_file options are specified and not empty
24
+ unless config_file_location.nil? || config_file_location.empty?
25
+ require "yaml"
26
+ # Try loading the file if it exists
27
+ filedata = File.open("#{config_file_location}").read if File.file?("#{config_file_location}")
28
+ default_options.merge!( YAML.load(filedata) ) if filedata rescue ""
29
+ end
30
+ # We want the command-line to overwrite the config file
31
+ default_options.merge!(local_user_data) unless local_user_data.nil?
32
+ OpenStruct.new(default_options)
33
+ end
34
+
35
+ # Load options via commandline
36
+ def load_options!(opts={})
37
+ require 'optparse'
38
+ OptionParser.new do |op|
39
+ op.banner = opts[:banner] if opts[:banner]
40
+ op.on('-A key', '--access-key key', "Ec2 access key (ENV['AWS_ACCESS_KEY'])") { |key| default_options[:access_key] = key }
41
+ op.on('-S key', '--secret-access-key key', "Ec2 secret access key (ENV['AWS_SECRET_ACCESS'])") { |key| default_options[:secret_access_key] = key }
42
+ op.on('-I ami', '--image-id id', "AMI instance (default: 'ami-40bc5829')") {|id| default_options[:ami] = id }
43
+ op.on('-k keypair', '--keypair name', "Keypair name (ENV['KEYPAIR_NAME'])") { |key| default_options[:keypair] = key }
44
+ op.on('-b bucket', '--bucket bucket', "Application bucket") { |bucket| default_options[:shared_bucket] = bucket }
45
+ # //THIS IS WHERE YOU LEFT OFF
46
+ op.on('-D working directory', '--dir dir', "Working directory") { |d| default_options[:working_directory] = d }
47
+
48
+ op.on('--ec2-dir dir', "Directory with ec2 data (default: '~/.ec2')") {|id| default_options[:ec2_dir] = id }
49
+ op.on('-r names', '--services names', "Monitored services (default: '')") {|id| default_options[:services] = id }
50
+ op.on('-c file', '--config-file file', "Config file (default: '')") {|file| default_options[:config_file] = file }
51
+ op.on('-l plugin_dir', '--plugin-dir dir', "Plugin directory (default: '')") {|file| default_options[:plugin_dir] = file }
52
+ op.on('-p port', '--host_port port', "Run on specific host_port (default: 7788)") { |host_port| default_options[:host_port] = host_port }
53
+ op.on('-m monitors', '--monitors names', "Monitor instances using (default: 'web,memory,cpu')") {|s| default_options[:monitor_load_on] = s }
54
+ op.on('-o port', '--client_port port', "Run on specific client_port (default: 7788)") { |client_port| default_options[:client_port] = client_port }
55
+ op.on('-O os', '--os os', "Configure for os (default: ubuntu)") { |os| default_options[:os] = os }
56
+ op.on('-e env', '--environment env', "Run on the specific environment (default: development)") { |env| default_options[:environment] = env }
57
+ op.on('-a address', '--public-ip address', "Associate this public address with the master node") {|s| default_options[:public_ip] = s}
58
+ op.on('-s size', '--size size', "Run specific sized instance") {|s| default_options[:size] = s}
59
+ op.on('-a name', '--name name', "Application name") {|n| default_options[:app_name] = n}
60
+ op.on('-u username', '--username name', "Login with the user (default: root)") {|s| default_options[:user] = s}
61
+ op.on('-d user-data','--user-data data', "Extra data to send each of the instances (default: "")") { |data| default_options[:user_data] = data.to_str }
62
+ op.on('-i', '--install-on-boot', 'Install the PoolParty and custom software on boot (default: false)') {|b| default_options[:install_on_load] = true}
63
+ op.on('-t seconds', '--polling-time', "Time between polling in seconds (default 50)") {|t| default_options[:polling_time] = t }
64
+ op.on('-v', '--[no-]verbose', 'Run verbosely (default: false)') {|v| default_options[:verbose] = true}
65
+ op.on('-n number', '--minimum-instances', "The minimum number of instances to run at all times (default 1)") {|i| default_options[:minimum_instances] = i.to_i}
66
+ op.on('-x number', '--maximum-instances', "The maximum number of instances to run (default 3)") {|x| default_options[:maximum_instances] = x.to_i}
67
+
68
+ op.on_tail("-V", "Show version") do
69
+ puts Application.version
70
+ exit
71
+ end
72
+ op.on_tail("-h", "-?", "Show this message") do
73
+ puts op
74
+ exit
75
+ end
76
+ end.parse!(opts[:argv] ? opts.delete(:argv) : ARGV.dup)
77
+ end
78
+
79
+ # Basic default options
80
+ # All can be overridden by the command line
81
+ # or in a config.yml file
82
+ def default_options
83
+ @default_options ||= {
84
+ :app_name => "application_name",
85
+ :host_port => 80,
86
+ :client_port => 8001,
87
+ :environment => 'development',
88
+ :verbose => false,
89
+ :logging => true,
90
+ :size => "m1.small",
91
+ :polling_time => "30.seconds",
92
+ :user_data => "",
93
+ :heavy_load => 0.80,
94
+ :light_load => 0.15,
95
+ :minimum_instances => 2,
96
+ :maximum_instances => 4,
97
+ :public_ip => "",
98
+ :access_key => ENV["AWS_ACCESS_KEY"],
99
+ :secret_access_key => ENV["AWS_SECRET_ACCESS"],
100
+ :config_file => if ENV["CONFIG_FILE"] && !ENV["CONFIG_FILE"].empty?
101
+ ENV["CONFIG_FILE"]
102
+ elsif File.file?("config/config.yml")
103
+ "config/config.yml"
104
+ else
105
+ nil
106
+ end,
107
+ :username => "root",
108
+ :ec2_dir => (ENV["EC2_HOME"].nil? || ENV["EC2_HOME"].empty?) ? "~/.ec2" : ENV["EC2_HOME"],
109
+ :keypair => (ENV["KEYPAIR_NAME"].nil? || ENV["KEYPAIR_NAME"].empty?) ? File.basename(`pwd`).strip : ENV["KEYPAIR_NAME"],
110
+ :ami => 'ami-44bd592d',
111
+ :shared_bucket => "",
112
+ :expand_when => "web < 1.5\n memory > 0.85",
113
+ :contract_when => "cpu < 0.20\n memory < 0.10",
114
+ :os => "ubuntu",
115
+ :plugin_dir => "plugins",
116
+ :install_on_load => false,
117
+ :working_directory => Dir.pwd
118
+ }
119
+ end
120
+ # Services monitored by Heartbeat
121
+ def master_managed_services
122
+ "cloud_master_takeover"
123
+ end
124
+ alias_method :managed_services, :master_managed_services
125
+ def launching_user_data
126
+ hash_to_launch_with.to_yaml
127
+ end
128
+ def hash_to_launch_with
129
+ @hash ||= { :polling_time => polling_time,
130
+ :access_key => access_key,
131
+ :secret_access_key => secret_access_key,
132
+ :user_data => user_data,
133
+ :keypair => keypair,
134
+ :keypair_path => "/mnt"
135
+ }
136
+ end
137
+ def local_user_data
138
+ @local_user_data ||= begin
139
+ @@timer.timeout(2.seconds) do
140
+ YAML.load(open("http://169.254.169.254/latest/user-data").read)
141
+ end
142
+ rescue Exception => e
143
+ {}
144
+ end
145
+ end
146
+ # For testing purposes
147
+ def reset!
148
+ @options = nil
149
+ @local_user_data = nil
150
+ end
151
+ # Keypair path
152
+ # Idiom:
153
+ # /Users/username/.ec2/[name]
154
+ def keypair_path
155
+ options.keypair_path ? options.keypair_path : "#{ec2_dir}/#{keypair_name}"
156
+ end
157
+ def keypair_name
158
+ "id_rsa-#{keypair}"
159
+ end
160
+ # Are we in development or test mode
161
+ %w(development production test).each do |env|
162
+ eval <<-EOE
163
+ def #{env}?
164
+ environment == '#{env}'
165
+ end
166
+ EOE
167
+ end
168
+ def environment=(env)
169
+ environment = env
170
+ end
171
+ def maintain_pid_path
172
+ "/var/run/pool_maintain.pid"
173
+ end
174
+ %w(scp_instances_script reconfigure_instances_script).each do |file|
175
+ define_method "sh_#{file}" do
176
+ File.join(File.dirname(__FILE__), "../..", "config", "#{file}.sh")
177
+ end
178
+ end
179
+ # Standard configuration files
180
+ %w(haproxy monit heartbeat heartbeat_authkeys).each do |file|
181
+ define_method "#{file}_config_file" do
182
+ File.join(File.dirname(__FILE__), "../..", "config", "#{file}.conf")
183
+ end
184
+ end
185
+ def version
186
+ PoolParty::Version.string
187
+ end
188
+ def install_on_load?(bool=false)
189
+ options.install_on_load == true || bool
190
+ end
191
+ # Call the options from the Application
192
+ def method_missing(m,*args)
193
+ options.methods.include?("#{m}") ? options.send(m,args) : super
194
+ end
195
+ end
196
+
197
+ end
198
+
199
+ end
@@ -0,0 +1,6 @@
1
+ =begin rdoc
2
+ Load the files in order
3
+ =end
4
+ %w(optioner application thread_pool scheduler provider remoter remoting remote_instance master monitors tasks plugin).each do |f|
5
+ require File.join(File.dirname(__FILE__), f)
6
+ end
@@ -0,0 +1,492 @@
1
+ =begin rdoc
2
+ The basic master for PoolParty
3
+ =end
4
+ module PoolParty
5
+ class Master < Remoting
6
+ include Aska
7
+ include Callbacks
8
+ # ############################
9
+ include Remoter
10
+ # ############################
11
+ include FileWriter
12
+
13
+ def initialize
14
+ super
15
+
16
+ self.class.send :rules, :contract_when, Application.options.contract_when unless are_rules?(:contract_when)
17
+ self.class.send :rules, :expand_when, Application.options.expand_when unless are_rules?(:expand_when)
18
+ end
19
+ # Start the cloud
20
+ def start_cloud!
21
+ start!
22
+ end
23
+ alias_method :start_cloud, :start_cloud!
24
+ # Start the cloud, which launches the minimum_instances
25
+ def start!
26
+ message "Launching minimum_instances"
27
+ launch_minimum_instances
28
+ message "Waiting for master to boot up"
29
+
30
+ wait_for_all_instances_to_boot
31
+
32
+ setup_cloud
33
+ end
34
+ def setup_cloud
35
+ install_cloud
36
+ configure_cloud
37
+ end
38
+ alias_method :start, :start!
39
+ def wait_for_all_instances_to_boot
40
+ reset!
41
+ while !number_of_pending_instances.zero?
42
+ wait "2.seconds" unless Application.test?
43
+ waited = true
44
+ reset!
45
+ end
46
+ unless Application.test? || waited.nil?
47
+ message "Give some time for the instance ssh to start up"
48
+ wait "15.seconds"
49
+ end
50
+ end
51
+ def wait_for_all_instances_to_terminate
52
+ reset!
53
+ while !list_of_terminating_instances.size.zero?
54
+ wait "2.seconds" unless Application.test?
55
+ waited = true
56
+ reset!
57
+ end
58
+ unless Application.test? || waited.nil?
59
+ message "Give some time for the instance ssh to start up"
60
+ wait "15.seconds"
61
+ end
62
+ reset!
63
+ end
64
+ # Configure the master because the master will take care of the rest after that
65
+ def configure_cloud
66
+ message "Configuring master"
67
+ build_and_send_config_files_in_temp_directory
68
+ remote_configure_instances
69
+
70
+ nodes.each do |node|
71
+ node.configure
72
+ end
73
+ end
74
+ before :install_cloud, :add_ssh_key
75
+ after :configure_cloud, :remove_ssh_key
76
+ def add_ssh_key(i)
77
+ Kernel.system("ssh-add #{Application.keypair_path} >/dev/null 2>/dev/null")
78
+ end
79
+ def remove_ssh_key(i)
80
+ Kernel.system("ssh-add -d #{Application.keypair_name} >/dev/null 2>/dev/null")
81
+ end
82
+ def install_cloud(bool=false)
83
+ if Application.install_on_load? || bool
84
+ # Just in case, add the new ubuntu apt-sources as well as updating and fixing the
85
+ # update packages.
86
+ update_apt_string =<<-EOE
87
+ touch /etc/apt/sources.list
88
+ echo 'deb http://mirrors.kernel.org/ubuntu hardy main universe' >> /etc/apt/sources.list
89
+ apt-get update --fix-missing
90
+ EOE
91
+
92
+ ssh(update_apt_string)
93
+
94
+ Provider.install_poolparty
95
+
96
+ # For plugins
97
+ nodes.each do |node|
98
+ node.install
99
+ end
100
+
101
+ end
102
+ end
103
+ def cloud_ips
104
+ @ips ||= nodes.collect {|a| a.ip }
105
+ end
106
+ # Launch the minimum number of instances.
107
+ def launch_minimum_instances
108
+ request_launch_new_instances(Application.minimum_instances - number_of_pending_and_running_instances)
109
+ nodes
110
+ end
111
+ # Start monitoring the cloud with the threaded loop
112
+ def start_monitor!
113
+ begin
114
+ trap("INT") do
115
+ on_exit
116
+ exit
117
+ end
118
+ # Daemonize only if we are not in the test environment
119
+ run_thread_loop(:daemonize => !Application.test?) do
120
+ add_task {PoolParty.message "Checking cloud"}
121
+ add_task {launch_minimum_instances}
122
+ add_task {reconfigure_cloud_when_necessary}
123
+ add_task {scale_cloud!}
124
+ add_task {check_stats}
125
+ end
126
+ rescue Exception => e
127
+ Process.kill("HUP", Process.pid)
128
+ end
129
+ end
130
+ alias_method :start_monitor, :start_monitor!
131
+ def user_tasks
132
+ end
133
+ # Sole purpose to check the stats, mainly in a plugin
134
+ def check_stats
135
+ str = registered_monitors.collect {|m| "#{m}"}
136
+ PoolParty.message "Monitors: #{str.join(", ")}"
137
+ end
138
+ # Add an instance if the cloud needs one ore terminate one if necessary
139
+ def scale_cloud!
140
+ add_instance_if_load_is_high
141
+ terminate_instance_if_load_is_low
142
+ end
143
+ alias_method :scale_cloud, :scale_cloud!
144
+ # Tough method:
145
+ # We need to make sure that all the instances have the required software installed
146
+ # This is a basic check against the local store of the instances that have the
147
+ # stack installed.
148
+ def reconfigure_cloud_when_necessary
149
+ PoolParty.message "#{number_of_unconfigured_nodes} unconfigured nodes"
150
+ configure_cloud if number_of_unconfigured_nodes > 0
151
+ end
152
+ def number_of_unconfigured_nodes
153
+ # TODO: Find a better way to tell if the nodes are configured.
154
+ nodes.reject {|a| a.stack_installed? }.size
155
+ end
156
+ def grow_by(num=1)
157
+ request_launch_new_instances(num)
158
+
159
+ wait_for_all_instances_to_boot
160
+
161
+ reset!
162
+ configure_cloud
163
+ end
164
+ def shrink_by(num=1)
165
+ num.times do |i|
166
+ # Get the last node that is not the master
167
+ node = nodes.reject {|a| a.master? }[-1]
168
+ res = request_termination_of_instance(node.instance_id) if node
169
+ PoolParty.message "#{res ? "Could" : "Could not"} shutdown instance"
170
+ end
171
+ wait_for_all_instances_to_terminate
172
+ configure_cloud
173
+ end
174
+ def make_base_tmp_dir(c)
175
+ `mkdir #{base_tmp_dir}` unless File.directory?(base_tmp_dir)
176
+ end
177
+ before :build_and_send_config_files_in_temp_directory, :make_base_tmp_dir
178
+ def build_and_send_config_files_in_temp_directory
179
+ require 'ftools'
180
+ if File.directory?(Application.plugin_dir)
181
+ Kernel.system("tar -czf #{base_tmp_dir}/plugins.tar.gz #{File.basename(Application.plugin_dir)}")
182
+ end
183
+
184
+ if Master.requires_heartbeat?
185
+ build_and_copy_heartbeat_authkeys_file
186
+ File.copy(get_config_file_for("cloud_master_takeover"), "#{base_tmp_dir}/cloud_master_takeover")
187
+ File.copy(get_config_file_for("heartbeat.conf"), "#{base_tmp_dir}/ha.cf")
188
+ end
189
+
190
+ File.copy(Application.config_file, "#{base_tmp_dir}/config.yml") if Application.config_file && File.exists?(Application.config_file)
191
+ File.copy(Application.keypair_path, "#{base_tmp_dir}/keypair") if File.exists?(Application.keypair_path)
192
+
193
+ copy_pem_files_to_tmp_dir
194
+
195
+ copy_config_files_in_directory_to_tmp_dir("config/resource.d")
196
+ # copy_config_files_in_directory_to_tmp_dir("config/monit.d")
197
+
198
+ build_haproxy_file
199
+ Master.build_user_global_files
200
+
201
+ build_nodes_list
202
+
203
+ Master.with_nodes do |node|
204
+ build_hosts_file_for(node)
205
+ build_reconfigure_instances_script_for(node)
206
+ Master.build_user_node_files_for(node)
207
+
208
+ if Master.requires_heartbeat?
209
+ build_heartbeat_config_file_for(node)
210
+ build_heartbeat_resources_file_for(node)
211
+ end
212
+ end
213
+ end
214
+ def copy_pem_files_to_tmp_dir
215
+ %w(EC2_CERT EC2_PRIVATE_KEY).each do |key|
216
+ begin
217
+ file = `echo $#{key}`.strip
218
+ File.copy(file, "#{base_tmp_dir}/#{File.basename(file)}")
219
+ rescue Exception => e
220
+ end
221
+ end
222
+ end
223
+ def cleanup_tmp_directory(c)
224
+ Dir["#{base_tmp_dir}/*"].each {|f| FileUtils.rm_rf f} if File.directory?("tmp/")
225
+ end
226
+ before :build_and_send_config_files_in_temp_directory, :cleanup_tmp_directory
227
+ # Send the files to the nodes
228
+ def send_config_files_to_nodes(c)
229
+ run_array_of_tasks(rsync_tasks("#{base_tmp_dir}/*", "#{remote_base_tmp_dir}"))
230
+ end
231
+ after :build_and_send_config_files_in_temp_directory, :send_config_files_to_nodes
232
+ def remote_configure_instances
233
+ arr = []
234
+ Master.with_nodes do |node|
235
+ script_file = "#{remote_base_tmp_dir}/#{node.name}-configuration"
236
+ str=<<-EOC
237
+ chmod +x #{script_file}
238
+ /bin/sh #{script_file}
239
+ EOC
240
+ arr << "#{self.class.ssh_string} #{node.ip} '#{str.strip.runnable}'"
241
+ end
242
+ run_array_of_tasks(arr)
243
+ end
244
+ # Add an instance if the load is high
245
+ def add_instance_if_load_is_high
246
+ if expand?
247
+ PoolParty.message "Cloud needs expansion"
248
+ grow_by(1)
249
+ end
250
+ end
251
+ alias_method :add_instance, :add_instance_if_load_is_high
252
+ # Teardown an instance if the load is pretty low
253
+ def terminate_instance_if_load_is_low
254
+ if contract?
255
+ PoolParty.message "Cloud to shrink"
256
+ shrink_by(1)
257
+ end
258
+ end
259
+ alias_method :terminate_instance, :terminate_instance_if_load_is_low
260
+ # FOR MONITORING
261
+ def contract?
262
+ valid_rules?(:contract_when)
263
+ end
264
+ def expand?
265
+ valid_rules?(:expand_when)
266
+ end
267
+ # Restart the running instances services with monit on all the nodes
268
+ def restart_running_instances_services
269
+ nodes.each do |node|
270
+ node.restart_with_monit
271
+ end
272
+ end
273
+ # Build the basic haproxy config file from the config file in the config directory and return a tempfile
274
+ def build_haproxy_file
275
+ write_to_file_for("haproxy") do
276
+ servers=<<-EOS
277
+ #{nodes.collect {|node| node.haproxy_entry}.join("\n")}
278
+ EOS
279
+ open(Application.haproxy_config_file).read.strip ^ {:servers => servers, :host_port => Application.host_port}
280
+ end
281
+ end
282
+ # Build host file for a specific node
283
+ def build_hosts_file_for(n)
284
+ write_to_file_for("hosts", n) do
285
+ "#{nodes.collect {|node| node.ip == n.ip ? node.local_hosts_entry : node.hosts_entry}.join("\n")}"
286
+ end
287
+ end
288
+ def build_nodes_list
289
+ write_to_file_for(RemoteInstance.node_list_name) do
290
+ "#{cloud_ips.join("\n")}"
291
+ end
292
+ end
293
+ # Build the basic auth file for the heartbeat
294
+ def build_and_copy_heartbeat_authkeys_file
295
+ write_to_file_for("authkeys") do
296
+ open(Application.heartbeat_authkeys_config_file).read
297
+ end
298
+ end
299
+ # Build heartbeat config file
300
+ def build_heartbeat_config_file_for(node)
301
+ write_to_file_for("heartbeat", node) do
302
+ servers = "#{node.node_entry}\n#{get_next_node(node).node_entry}" rescue ""
303
+ open(Application.heartbeat_config_file).read.strip ^ {:nodes => servers}
304
+ end
305
+ end
306
+ def build_heartbeat_resources_file_for(node)
307
+ write_to_file_for("haresources", node) do
308
+ "#{node.haproxy_resources_entry}\n#{get_next_node(node).haproxy_resources_entry}" rescue ""
309
+ end
310
+ end
311
+ # Build basic configuration script for the node
312
+ def build_reconfigure_instances_script_for(node)
313
+ write_to_file_for("configuration", node) do
314
+ open(Application.sh_reconfigure_instances_script).read.strip ^ node.configure_tasks( !PoolParty.verbose? )
315
+ end
316
+ end
317
+
318
+ # Try the user's directory before the master directory
319
+ def get_config_file_for(name)
320
+ if File.exists?("#{user_dir}/config/#{name}")
321
+ "#{user_dir}/config/#{name}"
322
+ else
323
+ "#{root_dir}/config/#{name}"
324
+ end
325
+ end
326
+ # Copy all the files in the directory to the dest
327
+ def copy_config_files_in_directory_to_tmp_dir(dir)
328
+ dest_dir = "#{base_tmp_dir}/#{File.basename(dir)}"
329
+ FileUtils.mkdir_p dest_dir
330
+
331
+ if File.directory?("#{user_dir}/#{dir}")
332
+ Dir["#{user_dir}/#{dir}/*"].each do |file|
333
+ File.copy(file, dest_dir)
334
+ end
335
+ else
336
+ Dir["#{root_dir}/#{dir}/*"].each do |file|
337
+ File.copy(file, dest_dir)
338
+ end
339
+ end
340
+ end
341
+ # Return a list of the nodes and cache them
342
+ def nodes
343
+ @nodes ||= list_of_nonterminated_instances.collect_with_index do |inst, i|
344
+ RemoteInstance.new(inst.merge({:number => i}))
345
+ end
346
+ end
347
+ # Return a list of the nodes for each keypair and cache them
348
+ def cloud_nodes
349
+ @cloud_nodes ||= begin
350
+ nodes_list = []
351
+ cloud_keypairs.each {|keypair|
352
+ list_of_nonterminated_instances(list_of_instances(keypair)).collect_with_index { |inst, i|
353
+ nodes_list << RemoteInstance.new(inst.merge({:number => i}))
354
+ }
355
+ }
356
+ nodes_list
357
+ end
358
+ end
359
+ # Get the node at the specific index from the cached nodes
360
+ def get_node(i=0)
361
+ nodes.select {|a| a.number == i.to_i}.first
362
+ end
363
+ # Get the next node in sequence, so we can configure heartbeat to monitor the next node
364
+ def get_next_node(node)
365
+ i = node.number + 1
366
+ i = 0 if i >= nodes.size
367
+ get_node(i)
368
+ end
369
+ # On exit command
370
+ def on_exit
371
+ end
372
+ # List the clouds
373
+ def list
374
+ if number_of_pending_and_running_instances > 0
375
+ out = "-- CLOUD (#{number_of_pending_and_running_instances})--\n"
376
+ out << nodes.collect {|node| node.description }.join("\n")
377
+ else
378
+ out = "Cloud is not running"
379
+ end
380
+ out
381
+ end
382
+ def clouds_list
383
+ if number_of_all_pending_and_running_instances > 0
384
+ out = "-- ALL CLOUDS (#{number_of_all_pending_and_running_instances})--\n"
385
+ keypair = nil
386
+ out << cloud_nodes.collect {|node|
387
+ str = ""
388
+ if keypair != node.keypair
389
+ keypair = node.keypair;
390
+ str = "key pair: #{keypair} (#{number_of_pending_and_running_instances(keypair)})\n"
391
+ end
392
+ str += "\t"+node.description if !node.description.nil?
393
+ }.join("\n")
394
+ else
395
+ out = "Clouds are not running"
396
+ end
397
+ out
398
+ end
399
+ # Reset and clear the caches
400
+ def reset!
401
+ @cached_descriptions = nil
402
+ @nodes = nil
403
+ @cloud_nodes = nil
404
+ end
405
+
406
+ class << self
407
+ include PoolParty
408
+ include FileWriter
409
+
410
+ def with_nodes(&block)
411
+ new.nodes.each &block
412
+ end
413
+
414
+ def collect_nodes(&block)
415
+ new.nodes.collect &block
416
+ end
417
+
418
+ def requires_heartbeat?
419
+ new.nodes.size > 1
420
+ end
421
+ def is_master_responding?
422
+ `ping -c1 -t5 #{get_master.ip}`
423
+ end
424
+ def get_master
425
+ new.nodes[0]
426
+ end
427
+ def cloud_ips
428
+ new.cloud_ips
429
+ end
430
+ def get_next_node(node)
431
+ new.get_next_node(node)
432
+ end
433
+ def set_hosts(c, remotetask=nil)
434
+ unless remotetask.nil?
435
+ rt = remotetask
436
+ end
437
+
438
+ ssh_location = `which ssh`.gsub(/\n/, '')
439
+ rsync_location = `which rsync`.gsub(/\n/, '')
440
+ rt.set :user, Application.username
441
+ # rt.set :domain, "#{Application.user}@#{ip}"
442
+ rt.set :application, Application.app_name
443
+ rt.set :ssh_flags, "-i #{Application.keypair_path} -o StrictHostKeyChecking=no"
444
+ rt.set :rsync_flags , ['-azP', '--delete', "-e '#{ssh_location} -l #{Application.username} -i #{Application.keypair_path} -o StrictHostKeyChecking=no'"]
445
+
446
+ master = get_master
447
+ rt.set :domain, "#{master.ip}" if master
448
+ Master.with_nodes { |node|
449
+ rt.host "#{Application.username}@#{node.ip}",:app if node.status =~ /running/
450
+ }
451
+ end
452
+
453
+ def ssh_configure_string_for(node)
454
+ cmd=<<-EOC
455
+ #{node.update_plugin_string(node)}
456
+ pool maintain -c ~/.config -l #{PoolParty.plugin_dir}
457
+ hostname -v #{node.name}
458
+ /usr/bin/s3fs #{Application.shared_bucket} -o accessKeyId=#{Application.access_key} -o secretAccessKey=#{Application.secret_access_key} -o nonempty /data
459
+ EOC
460
+ end
461
+ def build_haproxy_file
462
+ servers=<<-EOS
463
+ #{collect_nodes {|node| node.haproxy_entry}.join("\n")}
464
+ EOS
465
+ open(Application.haproxy_config_file).read.strip ^ {:servers => servers, :host_port => Application.host_port}
466
+ end
467
+
468
+ # Placeholders
469
+ def build_user_global_files
470
+ global_user_files.each do |arr|
471
+ write_to_file_for(arr[0]) &arr[1]
472
+ end
473
+ end
474
+ def build_user_node_files_for(node)
475
+ user_node_files.each do |arr|
476
+ write_to_file_for(arr[0], node) do
477
+ arr[1].call(node)
478
+ end
479
+ end
480
+ end
481
+ def define_global_user_file(name, &block)
482
+ global_user_files << [name, block]
483
+ end
484
+ def global_user_files;@global_user_files ||= [];end
485
+ def define_node_user_file(name, &block)
486
+ user_node_files << [name, block]
487
+ end
488
+ def user_node_files;@user_node_files ||= [];end
489
+ end
490
+
491
+ end
492
+ end