appscale-tools 1.6.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.
Files changed (114) hide show
  1. data/LICENSE +37 -0
  2. data/README +17 -0
  3. data/bin/appscale-add-keypair +15 -0
  4. data/bin/appscale-describe-instances +16 -0
  5. data/bin/appscale-remove-app +13 -0
  6. data/bin/appscale-reset-pwd +13 -0
  7. data/bin/appscale-run-instances +15 -0
  8. data/bin/appscale-terminate-instances +14 -0
  9. data/bin/appscale-upload-app +13 -0
  10. data/doc/AdvancedNode.html +163 -0
  11. data/doc/AppControllerClient.html +831 -0
  12. data/doc/AppEngineConfigException.html +165 -0
  13. data/doc/AppScaleException.html +165 -0
  14. data/doc/AppScaleTools.html +768 -0
  15. data/doc/BadCommandLineArgException.html +166 -0
  16. data/doc/BadConfigurationException.html +166 -0
  17. data/doc/CommonFunctions.html +2559 -0
  18. data/doc/EncryptionHelper.html +332 -0
  19. data/doc/GodInterface.html +443 -0
  20. data/doc/InfrastructureException.html +166 -0
  21. data/doc/Node.html +470 -0
  22. data/doc/NodeLayout.html +1297 -0
  23. data/doc/Object.html +539 -0
  24. data/doc/ParseArgs.html +268 -0
  25. data/doc/RemoteLogging.html +268 -0
  26. data/doc/SimpleNode.html +163 -0
  27. data/doc/UsageText.html +1204 -0
  28. data/doc/UserAppClient.html +993 -0
  29. data/doc/VMTools.html +1365 -0
  30. data/doc/bin/appscale-add-keypair.html +56 -0
  31. data/doc/bin/appscale-describe-instances.html +56 -0
  32. data/doc/bin/appscale-remove-app.html +56 -0
  33. data/doc/bin/appscale-reset-pwd.html +56 -0
  34. data/doc/bin/appscale-run-instances.html +56 -0
  35. data/doc/bin/appscale-terminate-instances.html +56 -0
  36. data/doc/bin/appscale-upload-app.html +56 -0
  37. data/doc/created.rid +21 -0
  38. data/doc/images/add.png +0 -0
  39. data/doc/images/brick.png +0 -0
  40. data/doc/images/brick_link.png +0 -0
  41. data/doc/images/bug.png +0 -0
  42. data/doc/images/bullet_black.png +0 -0
  43. data/doc/images/bullet_toggle_minus.png +0 -0
  44. data/doc/images/bullet_toggle_plus.png +0 -0
  45. data/doc/images/date.png +0 -0
  46. data/doc/images/delete.png +0 -0
  47. data/doc/images/find.png +0 -0
  48. data/doc/images/loadingAnimation.gif +0 -0
  49. data/doc/images/macFFBgHack.png +0 -0
  50. data/doc/images/package.png +0 -0
  51. data/doc/images/page_green.png +0 -0
  52. data/doc/images/page_white_text.png +0 -0
  53. data/doc/images/page_white_width.png +0 -0
  54. data/doc/images/plugin.png +0 -0
  55. data/doc/images/ruby.png +0 -0
  56. data/doc/images/tag_blue.png +0 -0
  57. data/doc/images/tag_green.png +0 -0
  58. data/doc/images/transparent.png +0 -0
  59. data/doc/images/wrench.png +0 -0
  60. data/doc/images/wrench_orange.png +0 -0
  61. data/doc/images/zoom.png +0 -0
  62. data/doc/index.html +116 -0
  63. data/doc/js/darkfish.js +153 -0
  64. data/doc/js/jquery.js +18 -0
  65. data/doc/js/navigation.js +142 -0
  66. data/doc/js/quicksearch.js +114 -0
  67. data/doc/js/search.js +94 -0
  68. data/doc/js/search_index.js +1 -0
  69. data/doc/js/searcher.js +228 -0
  70. data/doc/js/thickbox-compressed.js +10 -0
  71. data/doc/lib/app_controller_client_rb.html +60 -0
  72. data/doc/lib/appscale_tools_rb.html +88 -0
  73. data/doc/lib/common_functions_rb.html +78 -0
  74. data/doc/lib/custom_exceptions_rb.html +54 -0
  75. data/doc/lib/encryption_helper_rb.html +60 -0
  76. data/doc/lib/godinterface_rb.html +52 -0
  77. data/doc/lib/node_layout_rb.html +55 -0
  78. data/doc/lib/parse_args_rb.html +58 -0
  79. data/doc/lib/remote_log_rb.html +58 -0
  80. data/doc/lib/sshcopyid.html +174 -0
  81. data/doc/lib/usage_text_rb.html +58 -0
  82. data/doc/lib/user_app_client_rb.html +62 -0
  83. data/doc/lib/vm_tools_rb.html +62 -0
  84. data/doc/table_of_contents.html +496 -0
  85. data/lib/app_controller_client.rb +181 -0
  86. data/lib/appscale_tools.rb +403 -0
  87. data/lib/common_functions.rb +1467 -0
  88. data/lib/custom_exceptions.rb +25 -0
  89. data/lib/encryption_helper.rb +86 -0
  90. data/lib/godinterface.rb +152 -0
  91. data/lib/node_layout.rb +665 -0
  92. data/lib/parse_args.rb +415 -0
  93. data/lib/remote_log.rb +46 -0
  94. data/lib/sshcopyid +65 -0
  95. data/lib/usage_text.rb +144 -0
  96. data/lib/user_app_client.rb +245 -0
  97. data/lib/vm_tools.rb +549 -0
  98. data/test/tc_app_controller_client.rb +10 -0
  99. data/test/tc_appscale_add_keypair.rb +44 -0
  100. data/test/tc_appscale_describe_instances.rb +69 -0
  101. data/test/tc_appscale_remove_app.rb +128 -0
  102. data/test/tc_appscale_reset_pwd.rb +156 -0
  103. data/test/tc_appscale_run_instances.rb +48 -0
  104. data/test/tc_appscale_terminate_instances.rb +104 -0
  105. data/test/tc_appscale_upload_app.rb +166 -0
  106. data/test/tc_common_functions.rb +56 -0
  107. data/test/tc_encryption_helper.rb +10 -0
  108. data/test/tc_god_interface.rb +10 -0
  109. data/test/tc_node_layout.rb +93 -0
  110. data/test/tc_parse_args.rb +160 -0
  111. data/test/tc_user_app_client.rb +10 -0
  112. data/test/tc_vm_tools.rb +10 -0
  113. data/test/ts_all.rb +20 -0
  114. metadata +211 -0
@@ -0,0 +1,25 @@
1
+ # Programmer: Chris Bunch
2
+
3
+ # a generic class to represent exceptions thrown within AppScale
4
+ class AppScaleException < Exception
5
+ end
6
+
7
+ # a class representing exceptions related to bad command line arguments
8
+ # (see lib/parse_args)
9
+ class BadCommandLineArgException < AppScaleException
10
+ end
11
+
12
+ # a class representing exceptions related to incorrectly configured
13
+ # AppScale deployments
14
+ class BadConfigurationException < AppScaleException
15
+ end
16
+
17
+ # a class representing exceptions related to cloud infrastructures
18
+ # (e.g., if euca or ec2 throw errors)
19
+ class InfrastructureException < AppScaleException
20
+ end
21
+
22
+ # a class representing exceptions related to app engine apps
23
+ # to be uploaded
24
+ class AppEngineConfigException < AppScaleException
25
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/ruby -w
2
+ # Programmer: Chris Bunch
3
+
4
+ require 'fileutils'
5
+ require 'openssl'
6
+ $:.unshift File.join(File.dirname(__FILE__), ".", "lib")
7
+ require 'common_functions'
8
+
9
+ module EncryptionHelper
10
+ def self.generate_secret_key(keyname="appscale")
11
+ path="~/.appscale/#{keyname}.secret"
12
+ secret_key = ""
13
+ possible = "0123456789abcdefghijklmnopqrstuvxwyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
14
+ possibleLength = possible.length
15
+
16
+ 32.times { |index|
17
+ secret_key << possible[rand(possibleLength)]
18
+ }
19
+
20
+ full_path = File.expand_path(path)
21
+ File.open(full_path, "w") { |file|
22
+ file.puts(secret_key)
23
+ }
24
+
25
+ return secret_key, path
26
+ end
27
+
28
+ def self.generate_ssh_key(verbose, outputLocation, name, infrastructure, force)
29
+ ec2_output = ""
30
+ loop {
31
+ sleep(10) # to avoid euca replay error message
32
+ ec2_output = CommonFunctions.shell("#{infrastructure}-add-keypair #{name} 2>&1")
33
+ break if ec2_output.include?("BEGIN RSA PRIVATE KEY")
34
+ if force
35
+ puts "Trying again. Saw this from #{infrastructure}-add-keypair: #{ec2_output}" if verbose
36
+ sleep(10)
37
+ delete_output = CommonFunctions.shell("#{infrastructure}-delete-keypair #{name} 2>&1")
38
+ puts "Saw this from #{infrastructure}-delete-keypair: #{delete_output}" if verbose
39
+ else
40
+ abort("The keyname you chose is already in the system. Please either run this tool again with the --force flag or run the following:\n#{infrastructure}-delete-keypair #{name}")
41
+ end
42
+ }
43
+
44
+ # output is the ssh private key prepended with info we don't need
45
+ # delimited by the first \n, so rip it off first to get just the key
46
+
47
+ #first_newline = ec2_output.index("\n")
48
+ #ssh_private_key = ec2_output[first_newline+1, ec2_output.length-1]
49
+
50
+ if outputLocation.class == String
51
+ outputLocation = [outputLocation]
52
+ end
53
+
54
+ outputLocation.each { |path|
55
+ fullPath = File.expand_path(path)
56
+ File.open(fullPath, "w") { |file|
57
+ file.puts(ec2_output)
58
+ }
59
+ FileUtils.chmod(0600, fullPath) # else ssh won't use the key
60
+ }
61
+
62
+ return
63
+ end
64
+
65
+ def self.generate_pem_files(keyname)
66
+ key_loc = File.expand_path("~/.appscale/#{keyname}-key.pem")
67
+ cert_loc = File.expand_path("~/.appscale/#{keyname}-cert.pem")
68
+
69
+ key = OpenSSL::PKey::RSA.generate(2048)
70
+ pub = key.public_key
71
+ ca = OpenSSL::X509::Name.parse("/C=US/ST=Foo/L=Bar/O=AppScale/OU=User/CN=appscale.cs.ucsb.edu/emailAddress=test@test.com")
72
+ cert = OpenSSL::X509::Certificate.new
73
+ cert.version = 2
74
+ cert.serial = Time.now.to_i
75
+ cert.subject = ca
76
+ cert.issuer = ca
77
+ cert.public_key = pub
78
+ cert.not_before = Time.now
79
+ cert.not_after = Time.now + 3600
80
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
81
+
82
+ File.open(key_loc, "w") { |f| f.write key.to_pem }
83
+ File.open(cert_loc, "w") { |f| f.write cert.to_pem }
84
+ return key_loc, cert_loc
85
+ end
86
+ end
@@ -0,0 +1,152 @@
1
+
2
+ GOD_PORT = "17165"
3
+
4
+ module GodInterface
5
+ def self.start_god(remote_ip, remote_key)
6
+ self.run_god_command("god &", remote_ip, remote_key)
7
+ end
8
+
9
+ def self.start(watch, start_cmd, stop_cmd, ports, env_vars=nil, remote_ip=nil, remote_key=nil)
10
+
11
+ ports = [ports] unless ports.class == Array
12
+
13
+ prologue = <<BOO
14
+ WATCH = "#{watch}"
15
+ START_CMD = "#{start_cmd}"
16
+ STOP_CMD = "#{stop_cmd}"
17
+ PORTS = [#{ports.join(', ')}]
18
+
19
+ BOO
20
+
21
+ body = <<'BAZ'
22
+ PORTS.each do |port|
23
+ God.watch do |w|
24
+ w.name = "appscale-#{WATCH}-#{port}"
25
+ w.group = WATCH
26
+ w.interval = 30.seconds # default
27
+ w.start = START_CMD
28
+ w.stop = STOP_CMD
29
+ w.start_grace = 20.seconds
30
+ w.restart_grace = 20.seconds
31
+ w.log = "/var/log/appscale/#{WATCH}-#{port}.log"
32
+ w.pid_file = "/var/appscale/#{WATCH}-#{port}.pid"
33
+
34
+ w.behavior(:clean_pid_file)
35
+
36
+ w.start_if do |start|
37
+ start.condition(:process_running) do |c|
38
+ c.running = false
39
+ end
40
+ end
41
+
42
+ w.restart_if do |restart|
43
+ restart.condition(:memory_usage) do |c|
44
+ c.above = 150.megabytes
45
+ c.times = [3, 5] # 3 out of 5 intervals
46
+ end
47
+
48
+ restart.condition(:cpu_usage) do |c|
49
+ c.above = 50.percent
50
+ c.times = 5
51
+ end
52
+ end
53
+
54
+ # lifecycle
55
+ w.lifecycle do |on|
56
+ on.condition(:flapping) do |c|
57
+ c.to_state = [:start, :restart]
58
+ c.times = 5
59
+ c.within = 5.minute
60
+ c.transition = :unmonitored
61
+ c.retry_in = 10.minutes
62
+ c.retry_times = 5
63
+ c.retry_within = 2.hours
64
+ end
65
+ end
66
+ BAZ
67
+
68
+ if !env_vars.nil? and !env_vars.empty?
69
+ env_vars_str = ""
70
+
71
+ env_vars.each { |k, v|
72
+ env_vars_str += " \"" + k + "\" => \"" + v + "\",\n"
73
+ }
74
+
75
+ body += <<BOO
76
+
77
+ w.env = {
78
+ #{env_vars_str}
79
+ }
80
+ BOO
81
+ end
82
+
83
+ epilogue = <<BAZ
84
+ end
85
+ end
86
+ BAZ
87
+
88
+ config_file = prologue + body + epilogue
89
+ tempfile = "/tmp/god-#{rand(10000)}.god"
90
+
91
+ CommonFunctions.write_file(tempfile, config_file)
92
+
93
+ if remote_ip
94
+ CommonFunctions.scp_file(tempfile, tempfile, remote_ip, remote_key)
95
+ end
96
+
97
+ if remote_ip
98
+ ip = remote_ip
99
+ else
100
+ ip = CommonFunctions.local_ip
101
+ end
102
+
103
+ #unless CommonFunctions.is_port_open?(ip, GOD_PORT, use_ssl=false)
104
+ # self.run_god_command("god", remote_ip, remote_key)
105
+ # sleep(5)
106
+ #end
107
+
108
+ self.run_god_command("god load #{tempfile}", remote_ip, remote_key)
109
+
110
+ sleep(5)
111
+
112
+ FileUtils.rm_f(tempfile)
113
+ if remote_ip
114
+ remove = "rm -rf #{tempfile}"
115
+ CommonFunctions.run_remote_command(ip, remove, remote_key, false)
116
+ end
117
+
118
+ #god_info = "Starting #{watch} on ip #{ip}, port #{ports.join(', ')}" +
119
+ # " with start command [#{start_cmd}] and stop command [#{stop_cmd}]"
120
+ #puts god_info
121
+
122
+ self.run_god_command("god start #{watch}", remote_ip, remote_key)
123
+ end
124
+
125
+ def self.stop(watch, remote_ip=nil, remote_key=nil)
126
+ self.run_god_command("god stop #{watch}", remote_ip, remote_key)
127
+ end
128
+
129
+ def self.remove(watch, remote_ip=nil, remote_key=nil)
130
+ self.run_god_command("god remove #{watch}", remote_ip, remote_key)
131
+ end
132
+
133
+ def self.shutdown(remote_ip=nil, remote_key=nil)
134
+ %w{ uaserver pbserver memcached blobstore monitr loadbalancer }.each { |service|
135
+ self.run_god_command("god stop #{service}", remote_ip, remote_key)
136
+ }
137
+
138
+ self.run_god_command("god terminate", remote_ip, remote_key)
139
+ end
140
+
141
+ private
142
+ def self.run_god_command(cmd, ip, ssh_key)
143
+ local = ip.nil?
144
+
145
+ if local
146
+ puts cmd
147
+ else
148
+ CommonFunctions.run_remote_command(ip, cmd, ssh_key, true)
149
+ end
150
+ end
151
+ end
152
+
@@ -0,0 +1,665 @@
1
+ #!/usr/bin/ruby
2
+ # Programmer: Jonathan Kupferman
3
+ # Updated by Chris Bunch to add hybrid cloud support
4
+
5
+ USED_SIMPLE_AND_ADVANCED_KEYS = "Used both simple and advanced layout roles." +
6
+ " Only simple (controller, servers) or advanced (master, appengine, etc) " +
7
+ "can be used"
8
+ NO_INPUT_YAML_REQUIRES_MIN_IMAGES = "If no input yaml is specified, " +
9
+ "min_images must be specified."
10
+ NO_INPUT_YAML_REQUIRES_MAX_IMAGES = "If no input yaml is specified, " +
11
+ "max_images must be specified."
12
+ INPUT_YAML_REQUIRED = "An input yaml file is required for Xen, KVM, and " +
13
+ "hybrid cloud deployments"
14
+ DUPLICATE_IPS = "You specified some IP addresses more than once, which is " +
15
+ "not allowed in simple deployments."
16
+ NO_CONTROLLER = "No controller was specified"
17
+ ONLY_ONE_CONTROLLER = "Only one controller is allowed"
18
+
19
+ NODE_ID_REGEX = /(node|cloud(\d+))-(\d+)/
20
+ DEFAULT_NUM_NODES = 1
21
+ VALID_ROLES = [:master, :appengine, :database, :shadow, :open] +
22
+ [:load_balancer, :login, :db_master, :db_slave, :zookeeper, :memcache] +
23
+ [:rabbitmq, :rabbitmq_master, :rabbitmq_slave]
24
+
25
+ class NodeLayout
26
+ SIMPLE_FORMAT_KEYS = [:controller, :servers]
27
+ ADVANCED_FORMAT_KEYS = [:master, :database, :appengine, :open, :login, :zookeeper, :memcache, :rabbitmq]
28
+
29
+ # Required options are: database_type
30
+ def initialize(input_yaml, options, skip_replication=false)
31
+ @input_yaml = (input_yaml.kind_of?(String) ? YAML.load(input_yaml) : input_yaml)
32
+
33
+ @infrastructure = options[:infrastructure]
34
+ @database_type = options[:database]
35
+ @database_type = @database_type.to_sym if !@database_type.nil?
36
+ @min_images = options[:min_images]
37
+ @max_images = options[:max_images]
38
+ @replication = options[:replication]
39
+ @read_factor = options[:read_factor]
40
+ @write_factor = options[:write_factor]
41
+
42
+ @nodes = []
43
+ @skip_replication = skip_replication
44
+ end
45
+
46
+ def valid?
47
+ if is_simple_format?
48
+ valid_simple_format?[:result]
49
+ elsif is_advanced_format?
50
+ valid_advanced_format?[:result]
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ def errors
57
+ return [] if valid?
58
+
59
+ if is_simple_format?
60
+ valid_simple_format?[:message]
61
+ elsif is_advanced_format?
62
+ valid_advanced_format?[:message]
63
+ elsif @input_yaml.nil?
64
+ [INPUT_YAML_REQUIRED]
65
+ else
66
+ keys = @input_yaml.keys
67
+
68
+ keys.each { |key|
69
+ if !(SIMPLE_FORMAT_KEYS.include?(key) || ADVANCED_FORMAT_KEYS.include?(key))
70
+ return ["The flag #{key} is not a supported flag"]
71
+ end
72
+ }
73
+
74
+ return [USED_SIMPLE_AND_ADVANCED_KEYS]
75
+ end
76
+ end
77
+
78
+ def is_simple_format?
79
+ if @input_yaml.nil?
80
+ if VALID_CLOUD_TYPES.include?(@infrastructure) and @infrastructure != "hybrid"
81
+ # When used with the cloud, the simple format doesn't require a yaml
82
+ # Note this is not so in the hybrid model - a yaml is required in
83
+ # that scenario.
84
+ return true
85
+ else
86
+ return false
87
+ end
88
+ end
89
+
90
+ @input_yaml.keys.each do |key|
91
+ return false if !SIMPLE_FORMAT_KEYS.include?(key)
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ def is_advanced_format?
98
+ return false if @input_yaml.nil?
99
+
100
+ @input_yaml.keys.each do |key|
101
+ return false if !ADVANCED_FORMAT_KEYS.include?(key)
102
+ end
103
+
104
+ true
105
+ end
106
+
107
+ def parse_ip(ip)
108
+ id, cloud = nil, nil
109
+
110
+ match = NODE_ID_REGEX.match(ip)
111
+ if match.nil?
112
+ id = ip
113
+ cloud = "not-cloud"
114
+ else
115
+ id = match[0]
116
+ cloud = match[1]
117
+ end
118
+
119
+ return id, cloud
120
+ end
121
+
122
+ def valid_simple_format?
123
+ # We already computed the nodes, its valid
124
+ # cgb: an optimization to ensure we don't keep calling this
125
+ # when it always returns the same thing anyways
126
+ return valid if !@nodes.empty?
127
+
128
+ if @input_yaml.nil?
129
+ if VALID_CLOUD_TYPES.include?(@infrastructure) and @infrastructure != "hybrid"
130
+ if @min_images.nil?
131
+ return invalid(NO_INPUT_YAML_REQUIRES_MIN_IMAGES)
132
+ end
133
+
134
+ if @max_images.nil?
135
+ return invalid(NO_INPUT_YAML_REQUIRES_MAX_IMAGES)
136
+ end
137
+
138
+ # No yaml was created so we will create a generic one and then allow it to be validated
139
+ @input_yaml = generate_cloud_layout
140
+ else
141
+ return invalid(INPUT_YAML_REQUIRED)
142
+ end
143
+ end
144
+
145
+ nodes = []
146
+ @input_yaml.each_pair do |role, ips|
147
+ next if ips.nil?
148
+
149
+ ips.each do |ip|
150
+ id, cloud = parse_ip(ip)
151
+ node = SimpleNode.new id, cloud, [role]
152
+
153
+ # In simple deployments the db master and rabbitmq master is always on
154
+ # the shadow node, and db slave / rabbitmq slave is always on the other
155
+ # nodes
156
+ is_master = node.is_shadow?
157
+ node.add_db_role @database_type, is_master
158
+ node.add_rabbitmq_role is_master
159
+
160
+ return invalid(node.errors.join(",")) if !node.valid?
161
+
162
+ if VALID_CLOUD_TYPES.include?(@infrastructure)
163
+ error_message = "Invalid cloud node ID: #{node.id} \n" +
164
+ "Cloud node IDs must be in the format 'node-{IDNUMBER}'" +
165
+ "\nor of the form cloud{CLOUDNUMBER}-{IDNUMBER} for hybrid deployments"
166
+ return invalid(error_message) if NODE_ID_REGEX.match(node.id.to_s).nil?
167
+ else
168
+ # Xen/KVM should be using the ip address as the node id
169
+ error_message = "Invalid virtualized node ID: #{node.id} \n" +
170
+ "Virtualized node IDs must be a valid IP address"
171
+ return invalid(error_message) if IP_REGEX.match(node.id.to_s).nil?
172
+ end
173
+
174
+ nodes << node
175
+ end
176
+ end
177
+
178
+ # make sure that the user hasn't erroneously specified the same ip
179
+ # address more than once
180
+ all_ips = @input_yaml.values.flatten
181
+ duplicate_ips = all_ips.length - all_ips.uniq.length
182
+
183
+ unless duplicate_ips.zero?
184
+ return invalid(DUPLICATE_IPS)
185
+ end
186
+
187
+ if nodes.length == 1
188
+ # Singleton node should be master and app engine
189
+ nodes.first.add_role :appengine
190
+ nodes.first.add_role :memcache
191
+ end
192
+
193
+ # controller -> shadow
194
+ controller_count = 0
195
+ nodes.each do |node|
196
+ if node.is_shadow?
197
+ controller_count += 1
198
+ end
199
+ end
200
+
201
+ if controller_count == 0
202
+ return invalid(NO_CONTROLLER)
203
+ elsif controller_count > 1
204
+ return invalid(ONLY_ONE_CONTROLLER)
205
+ end
206
+
207
+ database_count = 0
208
+ nodes.each do |node|
209
+ if node.is_database?
210
+ database_count += 1
211
+ end
212
+ end
213
+
214
+ if @skip_replication
215
+ @nodes = nodes
216
+ return valid
217
+ end
218
+
219
+ rep = valid_database_replication? nodes
220
+ return rep unless rep[:result]
221
+
222
+ # Wait until it is validated to assign it
223
+ @nodes = nodes
224
+ valid
225
+ end
226
+
227
+ def valid_advanced_format?
228
+ # We already computed the nodes, its valid
229
+ return valid if !@nodes.empty?
230
+
231
+ node_hash = {}
232
+ @input_yaml.each_pair do |role, ips|
233
+
234
+ ips.each_with_index do |ip, index|
235
+ node = nil
236
+ if node_hash[ip].nil?
237
+ id, cloud = parse_ip(ip)
238
+ node = AdvancedNode.new(id, cloud)
239
+ else
240
+ node = node_hash[ip]
241
+ end
242
+
243
+ if role.to_sym == :database
244
+ # The first database node is the master
245
+ is_master = index.zero?
246
+ node.add_db_role @database_type, is_master
247
+ elsif role.to_sym == :db_master
248
+ node.add_role :zookeeper
249
+ node.add_role role
250
+ elsif role.to_sym == :rabbitmq
251
+ # Like the database, the first rabbitmq node is the master
252
+ is_master = index.zero?
253
+ node.add_role :rabbitmq
254
+ node.add_rabbitmq_role is_master
255
+ else
256
+ node.add_role role
257
+ end
258
+
259
+ node_hash[ip] = node
260
+ end
261
+ end
262
+
263
+ # Dont need the hash any more, make a nodes list
264
+ nodes = node_hash.values
265
+
266
+ nodes.each do |node|
267
+ return invalid(node.errors.join(",")) unless node.valid?
268
+
269
+ if VALID_CLOUD_TYPES.include?(@infrastructure)
270
+ error_message = "Invalid cloud node ID: #{node.id} \n" +
271
+ "Cloud node ID must be in the format 'node-{IDNUMBER}'" +
272
+ "\nor of the form cloud{CLOUDNUMBER}-{IDNUMBER} for hybrid deployments"
273
+ return invalid(error_message) if NODE_ID_REGEX.match(node.id.to_s).nil?
274
+ else
275
+ # Xen/KVM should be using the ip address as the node id
276
+ error_message = "Invalid virtualized node ID: #{node.id} \n" +
277
+ "Virtualized node IDs must be a valid IP address"
278
+ return invalid(error_message) if IP_REGEX.match(node.id.to_s).nil?
279
+ end
280
+ end
281
+
282
+ master_nodes = nodes.select { |node| node.is_shadow? }.compact
283
+
284
+ # need exactly one master
285
+ if master_nodes.length == 0
286
+ return invalid("No master was specified")
287
+ elsif master_nodes.length > 1
288
+ return invalid("Only one master is allowed")
289
+ end
290
+
291
+ master_node = master_nodes.first
292
+
293
+ login_node = nodes.select { |node| node.is_login? }.compact
294
+ # If a login node was not specified, make the master into the login node
295
+ if login_node.empty?
296
+ master_node.add_role :login
297
+ end
298
+
299
+ appengine_count = 0
300
+ nodes.each do |node|
301
+ if node.is_appengine?
302
+ appengine_count += 1
303
+ end
304
+ end
305
+
306
+ if appengine_count < 1
307
+ return invalid("Not enough appengine nodes were provided.")
308
+ end
309
+
310
+ memcache_count = 0
311
+ nodes.each do |node|
312
+ if node.is_memcache?
313
+ memcache_count += 1
314
+ end
315
+ end
316
+
317
+ # if no memcache nodes were specified, make all appengine nodes
318
+ # into memcache nodes
319
+ if memcache_count < 1
320
+ nodes.each { |node|
321
+ node.add_role :memcache if node.is_appengine?
322
+ }
323
+ end
324
+
325
+ if VALID_CLOUD_TYPES.include?(@infrastructure)
326
+ # If min and max aren't specified, they default to the number of nodes in the system
327
+ @min_images ||= nodes.length
328
+ @max_images ||= nodes.length
329
+
330
+ # TODO: look into if that first guard is really necessary with the preceding lines
331
+
332
+ if @min_images && nodes.length < @min_images
333
+ return invalid("Too few nodes were provided, #{nodes.length} were specified but #{@min_images} was the minimum")
334
+ end
335
+
336
+ if @max_images && nodes.length > @max_images
337
+ return invalid("Too many nodes were provided, #{nodes.length} were specified but #{@max_images} was the maximum")
338
+ end
339
+ end
340
+
341
+ zookeeper_count = 0
342
+ nodes.each do |node|
343
+ if node.is_zookeeper?
344
+ zookeeper_count += 1
345
+ end
346
+ end
347
+ master_node.add_role :zookeeper if zookeeper_count.zero?
348
+
349
+ # If no rabbitmq nodes are specified, make the shadow the rabbitmq_master
350
+ rabbitmq_count = 0
351
+ nodes.each do |node|
352
+ if node.is_rabbitmq?
353
+ rabbitmq_count += 1
354
+ end
355
+ end
356
+ if rabbitmq_count.zero?
357
+ master_node.add_role :rabbitmq
358
+ master_node.add_role :rabbitmq_master
359
+ end
360
+
361
+ # Any node that runs appengine needs rabbitmq to dispatch task requests to
362
+ # It's safe to add the slave role since we ensure above that somebody
363
+ # already has the master role
364
+ nodes.each do |node|
365
+ if node.is_appengine? and !node.is_rabbitmq?
366
+ node.add_role :rabbitmq_slave
367
+ end
368
+ end
369
+
370
+ database_count = 0
371
+ nodes.each do |node|
372
+ if node.is_database?
373
+ database_count += 1
374
+ end
375
+ end
376
+
377
+ if @skip_replication
378
+ @nodes = nodes
379
+ return valid
380
+ end
381
+
382
+ rep = valid_database_replication? nodes
383
+ return rep unless rep[:result]
384
+
385
+ # Wait until it is validated to assign it
386
+ @nodes = nodes
387
+
388
+ return valid
389
+ end
390
+
391
+ def valid_database_replication? nodes
392
+ database_node_count = 0
393
+ nodes.each do |node|
394
+ if node.is_database? or node.is_db_master?
395
+ database_node_count += 1
396
+ end
397
+ end
398
+
399
+ if database_node_count.zero?
400
+ return invalid("At least one database node must be provided.")
401
+ end
402
+
403
+ if @replication.nil?
404
+ if database_node_count > 3
405
+ # If there are a lot of database nodes, we default to 3x replication
406
+ @replication = 3
407
+ else
408
+ # If there are only a few nodes, replicate to each one of the nodes
409
+ @replication = database_node_count
410
+ end
411
+ end
412
+
413
+ if @replication > database_node_count
414
+ return invalid("The provided replication factor is too high. The replication factor (-n flag) cannot be greater than the number of database nodes.")
415
+ end
416
+
417
+ # Perform all the database specific checks here
418
+ if @database_type == :mysql && database_node_count % @replication != 0
419
+ return invalid("MySQL requires that the amount of replication be divisible by the number of nodes (e.g. with 6 nodes, 2 or 3 times replication). You specified #{database_node_count} database nodes which is not divisible by #{@replication} times replication.")
420
+ end
421
+
422
+ if @database_type == :voldemort
423
+ @read_factor ||= @replication
424
+ @write_factor ||= @replication
425
+
426
+ if @read_factor > @replication
427
+ return invalid("The provided read factor is too high. The read factor (-r flag) cannot be greater than the replication factor.")
428
+ elsif @write_factor > @replication
429
+ return invalid("The provided write factor is too high. The write factor (-w flag) cannot be greater than the replication factor.")
430
+ end
431
+ end
432
+
433
+ if @database_type == :simpledb
434
+ if ENV['SIMPLEDB_ACCESS_KEY'].nil?
435
+ return invalid("SimpleDB deployments require that the environment variable SIMPLEDB_ACCESS_KEY be set to your AWS access key.")
436
+ end
437
+
438
+ if ENV['SIMPLEDB_SECRET_KEY'].nil?
439
+ return invalid("SimpleDB deployments require that the environment variable SIMPLEDB_SECRET_KEY be set to your AWS secret key.")
440
+ end
441
+ end
442
+
443
+ valid
444
+ end
445
+
446
+ # Generates an yaml file for non-hybrid cloud layouts which don't have them
447
+ def generate_cloud_layout
448
+ layout = {:controller => "node-0"}
449
+ servers = []
450
+ num_slaves = @min_images - 1
451
+ num_slaves.times do |i|
452
+ servers << "node-#{i+1}"
453
+ end
454
+
455
+ layout[:servers] = servers
456
+ YAML.load(layout.to_yaml)
457
+ end
458
+
459
+ def replication_factor
460
+ return nil unless valid?
461
+
462
+ @replication
463
+ end
464
+
465
+ # TODO: can we just replace the if w/ unless and change ! to = ?
466
+ # or does that not exactly work due to the || ?
467
+
468
+ def read_factor
469
+ return nil if !valid? || @database_type != :voldemort
470
+
471
+ @read_factor
472
+ end
473
+
474
+ def write_factor
475
+ return nil if !valid? || @database_type != :voldemort
476
+
477
+ @write_factor
478
+ end
479
+
480
+ def min_images
481
+ return nil unless valid?
482
+
483
+ @min_images
484
+ end
485
+
486
+ def max_images
487
+ return nil unless valid?
488
+
489
+ @max_images
490
+ end
491
+
492
+ def nodes
493
+ return [] unless valid?
494
+
495
+ # Since the valid? check has succeded @nodes has been initialized
496
+ @nodes
497
+ end
498
+
499
+ # head node -> shadow
500
+ def head_node
501
+ return nil unless valid?
502
+
503
+ head_node = @nodes.select { |n| n.is_shadow? }.compact
504
+
505
+ # TODO: is the last guard necessary?
506
+ head_node.empty? ? nil : head_node[0]
507
+ end
508
+
509
+ def other_nodes
510
+ return [] unless valid?
511
+
512
+ other_nodes = @nodes.select { |n| !n.is_shadow? }.compact
513
+
514
+ other_nodes.empty? ? [] : other_nodes
515
+ end
516
+
517
+ def db_master
518
+ return nil unless valid?
519
+
520
+ db_master = @nodes.select { |n| n.is_db_master? }.compact
521
+
522
+ db_master.empty? ? nil : db_master[0]
523
+ end
524
+
525
+ def login_node
526
+ return nil unless valid?
527
+
528
+ login = @nodes.select { |n| n.is_login? }.compact
529
+
530
+ login.empty? ? nil : login[0]
531
+ end
532
+
533
+
534
+ def to_hash
535
+ result = {}
536
+ # Put all nodes except the head node in the hash
537
+ other_nodes.each do |node|
538
+ result[node.id] = node.roles.join(":")
539
+ end
540
+ result
541
+ end
542
+
543
+ private
544
+ def valid message=nil
545
+ { :result => true, :message => message }
546
+ end
547
+
548
+ def invalid message
549
+ { :result => false, :message => message }
550
+ end
551
+ end
552
+
553
+ class Node
554
+ attr_accessor :roles, :id, :cloud
555
+
556
+ def initialize id, cloud, roles=[]
557
+ # For Xen/KVM id is the public ip address
558
+ # For clouds, id is node-X since the ip is not known
559
+ @id = id
560
+ @cloud = cloud
561
+ @roles = roles.map { |r| r.to_sym }
562
+
563
+ expand_roles
564
+ end
565
+
566
+ def add_db_role db_type, is_master
567
+ if is_master
568
+ add_role :db_master
569
+ add_role :zookeeper
570
+ else
571
+ add_role :db_slave
572
+ end
573
+ end
574
+
575
+ def add_rabbitmq_role is_master
576
+ if is_master
577
+ add_role :rabbitmq_master
578
+ else
579
+ add_role :rabbitmq_slave
580
+ end
581
+ end
582
+
583
+ def add_role role
584
+ @roles << role.to_sym
585
+ expand_roles
586
+ end
587
+
588
+ VALID_ROLES.each do |role|
589
+ method = "is_#{role.to_s}?"
590
+ send :define_method, method do
591
+ @roles.include?(role)
592
+ end
593
+ end
594
+
595
+ def valid?
596
+ self.errors.empty?
597
+ end
598
+
599
+ def errors
600
+ @roles.map { |r| "Invalid role: #{r}" if !VALID_ROLES.include?(r) }.compact
601
+ end
602
+
603
+ def expand_roles
604
+ error_msg = "Expand roles should never be called on a node type." + \
605
+ " All nodes should be either a SimpleNode or AdvancedNode"
606
+ raise RuntimeError error_msg
607
+ end
608
+ end
609
+
610
+
611
+ class SimpleNode < Node
612
+ private
613
+ def expand_roles
614
+ if @roles.include?(:controller)
615
+ @roles.delete(:controller)
616
+ @roles << :shadow
617
+ @roles << :load_balancer
618
+ @roles << :database
619
+ @roles << :memcache # the database needs memcache
620
+ @roles << :login
621
+ @roles << :zookeeper
622
+ @roles << :rabbitmq
623
+ end
624
+
625
+ # If they specify a servers role, expand it out to
626
+ # be database, appengine, and memcache
627
+ if @roles.include?(:servers)
628
+ @roles.delete(:servers)
629
+ @roles << :appengine
630
+ @roles << :memcache
631
+ @roles << :database
632
+ @roles << :load_balancer
633
+ @roles << :rabbitmq
634
+ end
635
+
636
+ @roles.uniq!
637
+ end
638
+ end
639
+
640
+ class AdvancedNode < Node
641
+ private
642
+ def expand_roles
643
+ # make sure that deleting here doesn't screw things up
644
+ if @roles.include?(:master)
645
+ @roles.delete(:master)
646
+ @roles << :shadow
647
+ @roles << :load_balancer
648
+ @roles << :zookeeper
649
+ end
650
+
651
+ if @roles.include?(:login)
652
+ @roles << :load_balancer
653
+ end
654
+
655
+ if @roles.include?(:appengine)
656
+ @roles << :load_balancer
657
+ end
658
+
659
+ if @roles.include?(:database)
660
+ @roles << :memcache
661
+ end
662
+
663
+ @roles.uniq!
664
+ end
665
+ end