appscale-tools 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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,1467 @@
1
+ #!/usr/bin/ruby -w
2
+ # Programmer: Chris Bunch
3
+
4
+
5
+ require 'digest/sha1'
6
+ require 'net/http'
7
+ require 'openssl'
8
+ require 'open-uri'
9
+ require 'socket'
10
+ require 'timeout'
11
+ require 'yaml'
12
+
13
+
14
+ require 'app_controller_client'
15
+ require 'custom_exceptions'
16
+ require 'user_app_client'
17
+
18
+
19
+ require 'rubygems'
20
+ require 'json'
21
+
22
+
23
+ NO_SSH_KEY_FOUND = "No SSH key was found that could be used to log in to " +
24
+ "your machine."
25
+ MALFORMED_YAML = "The yaml file you provided was malformed. Please correct " +
26
+ "any errors in it and try again."
27
+ NO_CONFIG_FILE = "We could not find a valid app.yaml or web.xml file in " +
28
+ "your application."
29
+
30
+
31
+ # The username and password that will be used as the cloud administrator's
32
+ # credentials if the --test flag are used, which should only be used when
33
+ # developing AppScale (and not in production environments).
34
+ DEFAULT_USERNAME = "a@a.a"
35
+ DEFAULT_PASSWORD = "aaaaaa"
36
+
37
+
38
+ MAX_FILE_SIZE = 1000000
39
+
40
+
41
+ EMAIL_REGEX = /\A[[:print:]]+@[[:print:]]+\.[[:print:]]+\Z/
42
+ PASSWORD_REGEX = /\A[[:print:]]{6,}\Z/
43
+ IP_REGEX = /\d+\.\d+\.\d+\.\d+/
44
+ FQDN_REGEX = /[\w\d\.\-]+/
45
+ IP_OR_FQDN = /#{IP_REGEX}|#{FQDN_REGEX}/
46
+
47
+
48
+ CLOUDY_CREDS = ["ec2_access_key", "ec2_secret_key",
49
+ "aws_access_key_id", "aws_secret_access_key",
50
+ "SIMPLEDB_ACCESS_KEY", "SIMPLEDB_SECRET_KEY"]
51
+
52
+
53
+ VER_NUM = "1.5"
54
+ AS_VERSION = "AppScale Tools, Version #{VER_NUM}, http://appscale.cs.ucsb.edu"
55
+
56
+
57
+ PYTHON_CONFIG = "app.yaml"
58
+ JAVA_CONFIG = "war/WEB-INF/appengine-web.xml"
59
+
60
+
61
+ # When we try to ssh to other machines, we don't want to be asked for a password
62
+ # (since we always should have the right SSH key present), and we don't want to
63
+ # be asked to confirm the host's fingerprint, so set the options for that here.
64
+ SSH_OPTIONS = "-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no"
65
+
66
+
67
+ # A list of the databases that AppScale nodes can run, and a list of the cloud
68
+ # infrastructures that we can run over.
69
+ VALID_TABLE_TYPES = ["hbase", "hypertable", "mysql", "cassandra", "voldemort"] +
70
+ ["mongodb", "memcachedb", "scalaris", "simpledb", "redisdb"]
71
+ VALID_CLOUD_TYPES = ["ec2", "euca", "hybrid"]
72
+
73
+
74
+ # Some operations use an infinite timeout, and while -1 or 1.0/0 work in older
75
+ # versions of Ruby 1.8.7 (default on Ubuntu Lucid and before), they don't work
76
+ # in newer versions of Ruby 1.8.7 and Ruby 1.9. Instead, just use a large
77
+ # number, which will work on both.
78
+ INFINITY = 1000000
79
+
80
+
81
+ module CommonFunctions
82
+
83
+
84
+ # A convenience function that can be used to write a string to a file.
85
+ def self.write_file(location, contents)
86
+ File.open(location, "w+") { |file| file.write(contents) }
87
+ end
88
+
89
+
90
+ # A convenience function that returns a file's contents as a string.
91
+ def self.read_file(location, chomp=true)
92
+ file = File.open(location) { |f| f.read }
93
+ if chomp
94
+ return file.chomp
95
+ else
96
+ return file
97
+ end
98
+ end
99
+
100
+
101
+ # cgb: added in shell function for backticks so that we can unit test it
102
+ # Flexmock doesn't like backticks since its name is non-alphanumeric
103
+ # e.g., its name is Kernel:`
104
+ def self.shell(command)
105
+ `#{command}`
106
+ end
107
+
108
+
109
+ # Uses rsync to copy over a copy of the AppScale main codebase (e.g., not the
110
+ # AppScale Tools) from this machine to a remote machine.
111
+ # TODO(cgb): This function doesn't copy files in the main directory, like the
112
+ # firewall rules. It should be changed accordingly.
113
+ def self.rsync_files(dest_ip, ssh_key, local)
114
+ local = File.expand_path(local)
115
+ controller = "#{local}/AppController"
116
+ server = "#{local}/AppServer"
117
+ loadbalancer = "#{local}/AppLoadBalancer"
118
+ monitoring = "#{local}/AppMonitoring"
119
+ appdb = "#{local}/AppDB"
120
+ neptune = "#{local}/Neptune"
121
+ loki = "#{local}/Loki"
122
+ iaas_manager = "#{local}/InfrastructureManager"
123
+
124
+ if !File.exists?(controller)
125
+ raise BadConfigurationException.new("The location you " +
126
+ "specified to rsync from, #{local}, doesn't exist or contain " +
127
+ "AppScale data.")
128
+ end
129
+
130
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{controller}/* root@#{dest_ip}:/root/appscale/AppController")
131
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{server}/* root@#{dest_ip}:/root/appscale/AppServer")
132
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{loadbalancer}/* root@#{dest_ip}:/root/appscale/AppLoadBalancer")
133
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{monitoring}/* root@#{dest_ip}:/root/appscale/AppMonitoring")
134
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv --exclude='logs/*' --exclude='hadoop-*' --exclude='hbase/hbase-*' --exclude='voldemort/voldemort/*' --exclude='cassandra/cassandra/*' #{appdb}/* root@#{dest_ip}:/root/appscale/AppDB")
135
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{neptune}/* root@#{dest_ip}:/root/appscale/Neptune")
136
+ #self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{loki}/* root@#{dest_ip}:/root/appscale/Loki")
137
+ self.shell("rsync -e 'ssh -i #{ssh_key}' -arv #{iaas_manager}/* root@#{dest_ip}:/root/appscale/InfrastructureManager")
138
+ end
139
+
140
+
141
+ # This function tries to contact a node in the AppScale deployment to
142
+ # get the most up-to-date information on the current state of the
143
+ # deployment (as the number of nodes could change, or the roles that
144
+ # nodes have taken on).
145
+ def self.update_locations_file(keyname, ips=nil)
146
+ secret = self.get_secret_key(keyname)
147
+
148
+ # Don't worry about prioritizing the Shadow over any other nodes,
149
+ # as all nodes should be checking ZooKeeper and keeping themselves
150
+ # up-to-date already.
151
+ new_role_info = nil
152
+
153
+ if ips
154
+ all_ips = ips
155
+ else
156
+ all_ips = self.get_all_public_ips(keyname)
157
+ end
158
+
159
+ all_ips.each { |ip|
160
+ begin
161
+ acc = AppControllerClient.new(ip, secret)
162
+ new_role_info = acc.get_role_info()
163
+ break
164
+ rescue Exception
165
+ Kernel.puts("Couldn't contact AppController at #{ip}, skipping...")
166
+ end
167
+ }
168
+
169
+ if new_role_info.nil?
170
+ abort("Couldn't contact any AppControllers - is AppScale running?")
171
+ end
172
+
173
+ CommonFunctions.write_nodes_json(new_role_info, keyname)
174
+ end
175
+
176
+
177
+ def self.get_login_ip(head_node_ip, secret_key)
178
+ acc = AppControllerClient.new(head_node_ip, secret_key)
179
+ all_nodes = acc.get_all_public_ips()
180
+
181
+ all_nodes.each { |node|
182
+ acc_new = AppControllerClient.new(node, secret_key)
183
+ roles = acc_new.status()
184
+ return node if roles.match(/Is currently:(.*)login/)
185
+ }
186
+
187
+ raise AppScaleException.new("Unable to find login ip address!")
188
+ end
189
+
190
+
191
+ def self.clear_app(app_path, force=false)
192
+ return if !File.exists?(app_path)
193
+ return if app_path !~ /\A\/tmp/ and !force
194
+ path_to_remove = File.dirname(app_path)
195
+ FileUtils.rm_f(path_to_remove)
196
+ end
197
+
198
+
199
+ def self.validate_app_name(app_name, database)
200
+ disallowed = ["none", "auth", "login", "new_user", "load_balancer"]
201
+ if disallowed.include?(app_name)
202
+ raise AppEngineConfigException.new("App cannot be called " +
203
+ "'#{not_allowed}' - this is a reserved name.")
204
+ end
205
+
206
+ if app_name =~ /[^[a-z]-]/
207
+ raise AppEngineConfigException.new("App name can only contain " +
208
+ "numeric and lowercase alphabetical characters.")
209
+ end
210
+
211
+ if app_name.include?("-") and database == "hypertable"
212
+ raise AppEngineConfigException.new("App name cannot contain " +
213
+ "dashes when Hypertable is the underlying database.")
214
+ end
215
+
216
+ return app_name
217
+ end
218
+
219
+
220
+ def self.get_ips_from_yaml(ips)
221
+ return "using_tools" if ips.nil?
222
+
223
+ ips_to_use = []
224
+ if !ips[:servers].nil?
225
+ ips[:servers].each { |ip|
226
+ if ip =~ IP_REGEX
227
+ ips_to_use << ip
228
+ else
229
+ ips_to_use << CommonFunctions.convert_fqdn_to_ip(ip)
230
+ end
231
+ }
232
+ ips_to_use = ips_to_use.join(":")
233
+ end
234
+
235
+ return ips_to_use
236
+ end
237
+
238
+
239
+ def self.get_credentials(testing)
240
+ if testing
241
+ return DEFAULT_USERNAME, DEFAULT_PASSWORD
242
+ else
243
+ return CommonFunctions.get_email, CommonFunctions.get_password
244
+ end
245
+ end
246
+
247
+
248
+ def self.create_user(user, test, head_node_ip, secret_key, uac, pass=nil)
249
+ if pass
250
+ pass = pass # TODO - can we just remove this?
251
+ elsif test
252
+ pass = "aaaaaa"
253
+ else
254
+ pass = CommonFunctions.get_password
255
+ end
256
+
257
+ encrypted_pass = CommonFunctions.encrypt_password(user, pass)
258
+ uac.commit_new_user(user, encrypted_pass)
259
+
260
+ login_ip = CommonFunctions.get_login_ip(head_node_ip, secret_key)
261
+
262
+ # create xmpp account
263
+ # for user a@a.a, this translates to a@login_ip
264
+
265
+ pre = user.scan(/\A(.*)@/).flatten.to_s
266
+ xmpp_user = "#{pre}@#{login_ip}"
267
+ xmpp_pass = CommonFunctions.encrypt_password(xmpp_user, pass)
268
+ uac.commit_new_user(xmpp_user, xmpp_pass)
269
+ Kernel.puts "Your XMPP username is #{xmpp_user}"
270
+ end
271
+
272
+
273
+ def self.scp_app_to_ip(app_name, user, language, keyname, head_node_ip,
274
+ file_location, uac)
275
+
276
+ Kernel.puts "Uploading #{app_name}..."
277
+ uac.commit_new_app_name(user, app_name, language)
278
+
279
+ local_file_path = File.expand_path(file_location)
280
+
281
+ app_dir = "/var/apps/#{app_name}/app"
282
+ remote_file_path = "#{app_dir}/#{app_name}.tar.gz"
283
+ make_app_dir = "mkdir -p #{app_dir}"
284
+ true_key = File.expand_path("~/.appscale/#{keyname}.key")
285
+
286
+ Kernel.puts "Creating remote directory to copy app into"
287
+ CommonFunctions.run_remote_command(head_node_ip,
288
+ make_app_dir, true_key, false)
289
+ Kernel.sleep(1)
290
+ Kernel.puts "Copying over app"
291
+ CommonFunctions.scp_file(local_file_path, remote_file_path,
292
+ head_node_ip, true_key)
293
+
294
+ return remote_file_path
295
+ end
296
+
297
+
298
+ def self.update_appcontroller(head_node_ip, secret, app_name,
299
+ remote_file_path)
300
+
301
+ acc = AppControllerClient.new(head_node_ip, secret)
302
+ acc.done_uploading(app_name, remote_file_path)
303
+ Kernel.puts "Updating AppController"
304
+ acc.update([app_name])
305
+ end
306
+
307
+ def self.wait_for_nodes_to_load(head_node_ip, secret)
308
+ head_acc = AppControllerClient.new(head_node_ip, secret)
309
+
310
+ Kernel.puts "Please wait for AppScale to finish starting up."
311
+ all_ips = head_acc.get_all_public_ips()
312
+ loop {
313
+ done_starting = true
314
+ all_ips.each { |ip|
315
+ acc = AppControllerClient.new(ip, secret)
316
+ if !acc.is_done_initializing?
317
+ done_starting = false
318
+ end
319
+ }
320
+ break if done_starting
321
+ Kernel.sleep(5)
322
+ }
323
+ end
324
+
325
+ def self.wait_for_app_to_start(head_node_ip, secret, app_name)
326
+ acc = AppControllerClient.new(head_node_ip, secret)
327
+
328
+ Kernel.puts "Please wait for your app to start up."
329
+ loop {
330
+ break if acc.app_is_running?(app_name)
331
+ Kernel.sleep(5)
332
+ }
333
+
334
+ url_suffix = "/apps/#{app_name}"
335
+ url = "http://#{head_node_ip}#{url_suffix}"
336
+ Kernel.puts "\nYour app can be reached at the following URL: #{url}"
337
+ end
338
+
339
+
340
+ def self.wait_until_redirect(host, url_suffix)
341
+ uri = "http://#{host}#{url_suffix}"
342
+ loop {
343
+ response = ""
344
+ begin
345
+ response = Net::HTTP.get_response(URI.parse(uri))
346
+ rescue Errno::ECONNREFUSED, EOFError
347
+ Kernel.sleep(1)
348
+ next
349
+ rescue Exception => e
350
+ raise e
351
+ end
352
+
353
+ return if response['location'] != "http://#{host}/status"
354
+ Kernel.sleep(1)
355
+ }
356
+ end
357
+
358
+
359
+ def self.user_has_cmd?(command)
360
+ output = CommonFunctions.shell("which #{command}")
361
+ if output.empty?
362
+ return false
363
+ else
364
+ return true
365
+ end
366
+ end
367
+
368
+
369
+ def self.require_commands(commands)
370
+ commands.each { |cmd|
371
+ if !CommonFunctions.user_has_cmd?(cmd)
372
+ raise BadConfigurationException.new("You do not have the '#{cmd}' " +
373
+ "command in your PATH. Please ensure that it is in your path and " +
374
+ "try again.")
375
+ end
376
+ }
377
+ end
378
+
379
+
380
+ def self.convert_fqdn_to_ip(host)
381
+ nslookup = CommonFunctions.shell("nslookup #{host}")
382
+ ip = nslookup.scan(/#{host}\nAddress:\s+(#{IP_REGEX})/).flatten.to_s
383
+ if ip.nil? or ip.empty?
384
+ raise AppScaleException.new("Couldn't convert #{host} to an IP " +
385
+ "address. Result of nslookup was \n#{nslookup}")
386
+ end
387
+ return ip
388
+ end
389
+
390
+
391
+ def self.encrypt_password(user, pass)
392
+ Digest::SHA1.hexdigest(user + pass)
393
+ end
394
+
395
+
396
+ def self.sleep_until_port_is_open(ip, port, use_ssl=false)
397
+ loop {
398
+ return if CommonFunctions.is_port_open?(ip, port, use_ssl)
399
+ Kernel.sleep(1)
400
+ }
401
+ end
402
+
403
+
404
+ def self.sleep_until_port_is_closed(ip, port, use_ssl=false)
405
+ loop {
406
+ return unless CommonFunctions.is_port_open?(ip, port, use_ssl)
407
+ Kernel.sleep(1)
408
+ }
409
+ end
410
+
411
+
412
+ def self.is_port_open?(ip, port, use_ssl=false)
413
+ begin
414
+ Timeout::timeout(1) do
415
+ begin
416
+ sock = TCPSocket.new(ip, port)
417
+ if use_ssl
418
+ ssl_context = OpenSSL::SSL::SSLContext.new()
419
+ unless ssl_context.verify_mode
420
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
421
+ end
422
+ sslsocket = OpenSSL::SSL::SSLSocket.new(sock, ssl_context)
423
+ sslsocket.sync_close = true
424
+ sslsocket.connect
425
+ end
426
+ sock.close
427
+ return true
428
+ rescue Exception
429
+ return false
430
+ end
431
+ end
432
+ rescue Timeout::Error
433
+ end
434
+
435
+ return false
436
+ end
437
+
438
+
439
+ def self.run_remote_command(ip, command, public_key_loc, want_output)
440
+ if public_key_loc.class == Array
441
+ public_key_loc.each { |key|
442
+ key = File.expand_path(key)
443
+ }
444
+
445
+ remote_cmd = "ssh -i #{public_key_loc.join(' -i ')} #{SSH_OPTIONS} 2>&1 root@#{ip} '#{command}"
446
+ else
447
+ public_key_loc = File.expand_path(public_key_loc)
448
+ remote_cmd = "ssh -i #{public_key_loc} #{SSH_OPTIONS} root@#{ip} '#{command} "
449
+ end
450
+
451
+ if want_output
452
+ remote_cmd << "> /tmp/#{ip}.log 2>&1 &' &"
453
+ else
454
+ remote_cmd << "> /dev/null 2>&1 &' &"
455
+ end
456
+
457
+ Kernel.system remote_cmd
458
+ return remote_cmd
459
+ end
460
+
461
+
462
+ def self.get_remote_appscale_home(ip, key)
463
+ cat = "cat /etc/appscale/home"
464
+ remote_cmd = "ssh -i #{key} #{SSH_OPTIONS} 2>&1 root@#{ip} '#{cat}'"
465
+ possible_home = CommonFunctions.shell("#{remote_cmd}").chomp
466
+ if possible_home.nil? or possible_home.empty?
467
+ return "/root/appscale/"
468
+ else
469
+ return possible_home
470
+ end
471
+ end
472
+
473
+
474
+ def self.start_head_node(options, node_layout, apps_to_start)
475
+ # since we changed the key's name sometimes it works with storing the key
476
+ # in ssh.key and sometimes it needs to be in keyname.key{.private}
477
+ # always do both just in case
478
+
479
+ # TODO: check here to make sure that if hybrid, the keyname is free
480
+ # in all clouds specified
481
+
482
+ keyname = options['keyname']
483
+ named_key_loc = "~/.appscale/#{keyname}.key"
484
+ named_backup_key_loc = "~/.appscale/#{keyname}.private"
485
+ ssh_key_location = named_key_loc
486
+ ssh_keys = [ssh_key_location, named_key_loc, named_backup_key_loc]
487
+ secret_key, secret_key_location = EncryptionHelper.generate_secret_key(keyname)
488
+ self.verbose("New secret key is #{CommonFunctions.obscure_string(secret_key)}", options['verbose'])
489
+
490
+ # TODO: serialize via json instead of this hacky way
491
+ ips_hash = node_layout.to_hash
492
+ ips_to_use = ips_hash.map { |node,roles| "#{node}--#{roles}" }.join("..")
493
+
494
+ head_node = node_layout.head_node
495
+ infrastructure = options['infrastructure']
496
+ if infrastructure == "hybrid"
497
+ head_node_infra = VMTools.lookup_cloud_env(head_node.cloud)
498
+ VMTools.set_hybrid_creds(node_layout)
499
+ machine = VMTools.get_hybrid_machine(head_node_infra, "1")
500
+ else
501
+ head_node_infra = infrastructure
502
+ machine = options['machine']
503
+ end
504
+
505
+ locations = VMTools.spawn_head_node(head_node, head_node_infra, keyname,
506
+ ssh_key_location, ssh_keys, options['force'], machine,
507
+ options['instance_type'], options['group'], options['verbose'])
508
+
509
+ head_node_ip = locations.split(":")[0]
510
+ instance_id = locations.scan(/i-\w+/).flatten.to_s
511
+ locations = [locations]
512
+
513
+ true_key = CommonFunctions.find_real_ssh_key(ssh_keys, head_node_ip)
514
+
515
+ self.verbose("Log in to your head node: ssh -i #{true_key} " +
516
+ "root@#{head_node_ip}", options['verbose'])
517
+
518
+ CommonFunctions.ensure_image_is_appscale(head_node_ip, true_key)
519
+ CommonFunctions.ensure_db_is_supported(head_node_ip, options['table'],
520
+ true_key)
521
+
522
+ scp = options['scp']
523
+ if scp
524
+ Kernel.puts "Copying over local copy of AppScale from #{scp}"
525
+ CommonFunctions.rsync_files(head_node_ip, true_key, scp)
526
+ end
527
+
528
+ keypath = true_key.scan(/([\d|\w|\.]+)\Z/).flatten.to_s
529
+ remote_key_location = "/root/.appscale/#{keyname}.key"
530
+ CommonFunctions.scp_file(true_key, remote_key_location, head_node_ip, true_key)
531
+
532
+ creds = CommonFunctions.generate_appscale_credentials(options, node_layout,
533
+ head_node_ip, ips_to_use, true_key)
534
+ self.verbose(CommonFunctions.obscure_creds(creds).inspect, options['verbose'])
535
+
536
+ Kernel.puts "Head node successfully created at #{head_node_ip}. It is now " +
537
+ "starting up #{options['table']} via the command line arguments given."
538
+
539
+ RemoteLogging.remote_post(options['max_images'], options['table'],
540
+ infrastructure, "started headnode", "success")
541
+
542
+ Kernel.sleep(10) # sometimes this helps out with ec2 / euca deployments
543
+ # gives them an extra moment to come up and accept scp requests
544
+
545
+ CommonFunctions.copy_keys(secret_key_location, head_node_ip, true_key,
546
+ options)
547
+
548
+ CommonFunctions.start_appcontroller(head_node_ip, true_key,
549
+ options['verbose'])
550
+
551
+ acc = AppControllerClient.new(head_node_ip, secret_key)
552
+ creds = creds.to_a.flatten
553
+ acc.set_parameters(locations, creds, apps_to_start)
554
+
555
+ return {:acc => acc, :head_node_ip => head_node_ip,
556
+ :instance_id => instance_id, :true_key => true_key,
557
+ :secret_key => secret_key}
558
+ end
559
+
560
+
561
+ def self.find_real_ssh_key(ssh_keys, host)
562
+ ssh_keys.each { |key|
563
+ key = File.expand_path(key)
564
+ return_value = CommonFunctions.shell("ssh -i #{key} #{SSH_OPTIONS} 2>&1 root@#{host} 'touch /tmp/foo'; echo $? ").chomp
565
+ if return_value == "0"
566
+ return key
567
+ end
568
+ }
569
+
570
+ raise AppScaleException.new(NO_SSH_KEY_FOUND)
571
+ end
572
+
573
+
574
+ def self.scp_file(local_file_loc, remote_file_loc, target_ip, public_key_loc)
575
+ cmd = ""
576
+ local_file_loc = File.expand_path(local_file_loc)
577
+ retval_file = File.expand_path("~/.appscale/retval-#{rand()}")
578
+
579
+ if public_key_loc.class == Array
580
+ public_key_loc.each { |key|
581
+ key = File.expand_path(key)
582
+ }
583
+
584
+ cmd = "scp -i #{public_key_loc.join(' -i ')} #{SSH_OPTIONS} 2>&1 #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
585
+ else
586
+ public_key_loc = File.expand_path(public_key_loc)
587
+ cmd = "scp -i #{public_key_loc} #{SSH_OPTIONS} 2>&1 #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
588
+ end
589
+
590
+ cmd << "; echo $? > #{retval_file}"
591
+
592
+ FileUtils.rm_f(retval_file)
593
+
594
+ begin
595
+ Timeout::timeout(INFINITY) { CommonFunctions.shell("#{cmd}") }
596
+ rescue Timeout::Error
597
+ raise AppScaleException.new("Remotely copying over files failed. Is " +
598
+ "the destination machine on and reachable from this computer? We " +
599
+ "tried the following command:\n\n#{cmd}")
600
+ end
601
+
602
+ loop {
603
+ break if File.exists?(retval_file)
604
+ Kernel.sleep(5)
605
+ }
606
+
607
+ retval = (File.open(retval_file) { |f| f.read }).chomp
608
+
609
+ fails = 0
610
+ loop {
611
+ break if retval == "0"
612
+ Kernel.puts "\n\n[#{cmd}] returned #{retval} instead of 0 as expected. Will try to copy again momentarily..."
613
+ fails += 1
614
+ if fails >= 5
615
+ raise AppScaleException.new("SCP failed")
616
+ end
617
+ Kernel.sleep(1)
618
+ CommonFunctions.shell("#{cmd}")
619
+ retval = (File.open(retval_file) { |f| f.read }).chomp
620
+ }
621
+
622
+ FileUtils.rm_f(retval_file)
623
+ return cmd
624
+ end
625
+
626
+
627
+ def self.get_username_from_options(options)
628
+ if options['test']
629
+ return DEFAULT_USERNAME
630
+ elsif options['email']
631
+ return options['email']
632
+ else
633
+ return CommonFunctions.get_email
634
+ end
635
+ end
636
+
637
+
638
+ def self.get_email
639
+ email = nil
640
+ Kernel.puts "\nThis AppScale instance is linked to an e-mail address giving it administrator privileges."
641
+
642
+ loop {
643
+ Kernel.print "Enter your desired administrator e-mail address: "
644
+ STDOUT.flush
645
+ email = STDIN.gets.chomp
646
+
647
+ if email =~ EMAIL_REGEX
648
+ break
649
+ else
650
+ Kernel.puts "The response you typed in was not an e-mail address. Please try again.\n\n"
651
+ end
652
+ }
653
+
654
+ return email
655
+ end
656
+
657
+
658
+ def self.get_password()
659
+ pass = nil
660
+ Kernel.puts "\nThe new administrator password must be at least six characters long and can include non-alphanumeric characters."
661
+
662
+ loop {
663
+ Kernel.print "Enter your new password: "
664
+ new_pass = self.get_line_from_stdin_no_echo()
665
+ Kernel.print "\nEnter again to verify: "
666
+ verify_pass = self.get_line_from_stdin_no_echo()
667
+ Kernel.print "\n"
668
+
669
+ if new_pass == verify_pass
670
+ pass = new_pass
671
+
672
+ if pass =~ PASSWORD_REGEX
673
+ break
674
+ else
675
+ Kernel.puts "\n\nThe password you typed in was not at least six characters long. Please try again.\n\n"
676
+ end
677
+ else
678
+ Kernel.puts "\n\nPasswords entered do not match. Please try again.\n\n"
679
+ end
680
+ }
681
+
682
+ return pass
683
+ end
684
+
685
+
686
+ def self.get_line_from_stdin_no_echo
687
+ state = CommonFunctions.shell("stty -g")
688
+ system "stty -echo" # Turn off character echoing
689
+ STDOUT.flush
690
+ line = STDIN.gets.chomp
691
+ system "stty #{state}"
692
+ return line
693
+ end
694
+
695
+
696
+ def self.get_from_yaml(keyname, tag, required=true)
697
+ location_file = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
698
+
699
+ if !File.exists?(location_file)
700
+ raise AppScaleException.new("An AppScale instance is not currently " +
701
+ "running with the provided keyname, \"#{keyname}\".")
702
+ end
703
+
704
+ begin
705
+ tree = YAML.load_file(location_file)
706
+ rescue ArgumentError
707
+ if required
708
+ raise AppScaleException.new(MALFORMED_YAML)
709
+ else
710
+ return nil
711
+ end
712
+ end
713
+
714
+ if !tree
715
+ raise AppScaleException.new("The location file is in the wrong format.")
716
+ end
717
+
718
+ value = tree[tag]
719
+
720
+ if value.nil? and required
721
+ raise AppScaleException.new("The location file did not contain a #{tag} tag.")
722
+ end
723
+
724
+ return value
725
+ end
726
+
727
+
728
+ def self.get_role_from_nodes(keyname, role)
729
+ nodes = self.read_nodes_json(keyname)
730
+ nodes.each { |node|
731
+ if node['jobs'].include?(role)
732
+ return node['public_ip']
733
+ end
734
+ }
735
+
736
+ return ""
737
+ end
738
+
739
+
740
+ def self.get_load_balancer_ip(keyname, required=true)
741
+ return self.get_role_from_nodes(keyname, 'load_balancer')
742
+ end
743
+
744
+
745
+ def self.get_load_balancer_id(keyname, required=true)
746
+ return CommonFunctions.get_from_yaml(keyname, :instance_id)
747
+ end
748
+
749
+
750
+ def self.get_table(keyname, required=true)
751
+ return CommonFunctions.get_from_yaml(keyname, :table, required)
752
+ end
753
+
754
+
755
+ def self.get_all_public_ips(keyname, required=true)
756
+ ips = []
757
+ nodes = self.read_nodes_json(keyname)
758
+ nodes.each { |node|
759
+ ips << node['public_ip']
760
+ }
761
+ return ips
762
+ end
763
+
764
+
765
+ def self.get_db_master_ip(keyname, required=true)
766
+ return self.get_role_from_nodes(keyname, 'db_master')
767
+ end
768
+
769
+
770
+ def self.get_head_node_ip(keyname, required=true)
771
+ return self.get_role_from_nodes(keyname, 'shadow')
772
+ end
773
+
774
+
775
+ def self.get_secret_key(keyname, required=true)
776
+ CommonFunctions.get_from_yaml(keyname, :secret)
777
+ end
778
+
779
+
780
+ def self.get_infrastructure(keyname, required=true)
781
+ CommonFunctions.get_from_yaml(keyname, :infrastructure)
782
+ end
783
+
784
+
785
+ def self.make_appscale_directory()
786
+ # AppScale generates private keys for each cloud that machines are
787
+ # running in. It stores this data (and a YAML file detailing IP address
788
+ # mappings) in ~/.appscale - create this location if it doesn't exist.
789
+ appscale_path = File.expand_path("~/.appscale")
790
+ if !File.exists?(appscale_path)
791
+ FileUtils.mkdir(appscale_path)
792
+ end
793
+ end
794
+
795
+
796
+ # Reads the JSON file that stores information about which roles run on
797
+ # which nodes.
798
+ def self.read_nodes_json(keyname)
799
+ filename = File.expand_path("~/.appscale/locations-#{keyname}.json")
800
+ return JSON.load(self.read_file(filename))
801
+ end
802
+
803
+
804
+ # Writes the given JSON to the ~/.appscale directory so that we can read
805
+ # it later and determine what nodes run what services.
806
+ def self.write_nodes_json(new_role_info, keyname)
807
+ filename = File.expand_path("~/.appscale/locations-#{keyname}.json")
808
+ self.write_file(filename, JSON.dump(new_role_info))
809
+ end
810
+
811
+
812
+ def self.write_and_copy_node_file(options, node_layout, head_node_result)
813
+ keyname = options['keyname']
814
+ head_node_ip = head_node_result[:head_node_ip]
815
+
816
+ all_ips = head_node_result[:acc].get_all_public_ips()
817
+ db_master_ip = node_layout.db_master.id
818
+
819
+ locations_yaml = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
820
+ CommonFunctions.write_node_file(head_node_ip,
821
+ head_node_result[:instance_id], options['table'],
822
+ head_node_result[:secret_key], db_master_ip, all_ips,
823
+ options['infrastructure'], locations_yaml)
824
+ remote_locations_file = "/root/.appscale/locations-#{keyname}.yaml"
825
+ CommonFunctions.scp_file(locations_yaml, remote_locations_file,
826
+ head_node_ip, head_node_result[:true_key])
827
+ end
828
+
829
+
830
+ def self.write_node_file(head_node_ip, instance_id, table, secret, db_master,
831
+ ips, infrastructure, locations_yaml)
832
+
833
+ infrastructure ||= "xen"
834
+ tree = { :load_balancer => head_node_ip, :instance_id => instance_id ,
835
+ :table => table, :shadow => head_node_ip,
836
+ :secret => secret , :db_master => db_master,
837
+ :ips => ips , :infrastructure => infrastructure }
838
+ loc_path = File.expand_path(locations_yaml)
839
+ File.open(loc_path, "w") {|file| YAML.dump(tree, file)}
840
+ end
841
+
842
+
843
+ def self.get_random_alphanumeric(length=10)
844
+ random = ""
845
+ possible = "0123456789abcdefghijklmnopqrstuvxwyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
846
+ possibleLength = possible.length
847
+
848
+ length.times { |index|
849
+ random << possible[rand(possibleLength)]
850
+ }
851
+
852
+ return random
853
+ end
854
+
855
+
856
+ def self.get_app_info_from_options(options)
857
+ file_location = options['file_location']
858
+ table = options['table']
859
+
860
+ if file_location.nil?
861
+ apps_to_start = ["none"]
862
+ return apps_to_start, {}
863
+ else
864
+ app_info = CommonFunctions.get_app_name_from_tar(file_location)
865
+ apps_to_start = [CommonFunctions.validate_app_name(app_info[:app_name], table)]
866
+ return apps_to_start, app_info
867
+ end
868
+ end
869
+
870
+
871
+ def self.get_app_name_from_tar(fullpath)
872
+ app_name, file, language = CommonFunctions.get_app_info(fullpath, PYTHON_CONFIG)
873
+
874
+ if app_name.nil? or file.nil? or language.nil?
875
+ app_name, file, language = CommonFunctions.get_app_info(fullpath, JAVA_CONFIG)
876
+ end
877
+
878
+ if app_name.nil? or file.nil? or language.nil?
879
+ raise AppScaleException.new(NO_CONFIG_FILE)
880
+ end
881
+
882
+ return {:app_name => app_name, :file => file, :language => language}
883
+ end
884
+
885
+
886
+ def self.move_app(temp_dir, filename, app_file, fullpath)
887
+ if File.directory?(fullpath)
888
+ CommonFunctions.shell("cp -r #{fullpath}/* /tmp/#{temp_dir}/")
889
+ return
890
+ else
891
+ FileUtils.cp(fullpath, "/tmp/#{temp_dir}/#{filename}")
892
+ FileUtils.rm_f("/tmp/#{temp_dir}/#{app_file}")
893
+ tar_file = CommonFunctions.shell("cd /tmp/#{temp_dir}; tar zxvfm #{filename} 2>&1; echo $?").chomp
894
+ tar_ret_val = tar_file.scan(/\d+\Z/).to_s
895
+ if tar_ret_val != "0"
896
+ raise AppScaleException.new("Untar'ing the given tar file in /tmp failed")
897
+ end
898
+ end
899
+ return
900
+ end
901
+
902
+
903
+ def self.warn_on_large_app_size(fullpath)
904
+ size = File.size(fullpath)
905
+ if size > MAX_FILE_SIZE
906
+ Kernel.puts "Warning: Your application is large enough that it may take a while to upload."
907
+ end
908
+ end
909
+
910
+
911
+ def self.get_app_info(fullpath, app_file)
912
+ if !File.exists?(fullpath)
913
+ raise AppEngineConfigException.new("AppEngine file not found")
914
+ end
915
+ filename = fullpath.scan(/\/?([\w\.]+\Z)/).flatten.to_s
916
+
917
+ temp_dir = CommonFunctions.get_random_alphanumeric
918
+ FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
919
+ FileUtils.mkdir_p("/tmp/#{temp_dir}")
920
+
921
+ CommonFunctions.move_app(temp_dir, filename, app_file, fullpath)
922
+ app_yaml_loc = app_file
923
+ if !File.exists?("/tmp/#{temp_dir}/#{app_file}")
924
+ FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
925
+ return nil, nil, nil
926
+ end
927
+
928
+ if app_file == PYTHON_CONFIG
929
+ app_name = CommonFunctions.get_app_name_via_yaml(temp_dir, app_yaml_loc)
930
+ language = "python"
931
+ if File.directory?(fullpath)
932
+ temp_dir2 = CommonFunctions.get_random_alphanumeric
933
+ FileUtils.rm_rf("/tmp/#{temp_dir2}", :secure => true)
934
+ FileUtils.mkdir_p("/tmp/#{temp_dir2}")
935
+ CommonFunctions.shell("cd /tmp/#{temp_dir}; tar -czf ../#{temp_dir2}/#{app_name}.tar.gz .")
936
+ file = "/tmp/#{temp_dir2}/#{app_name}.tar.gz"
937
+ else
938
+ file = fullpath
939
+ end
940
+ elsif app_file == JAVA_CONFIG
941
+ app_name = CommonFunctions.get_app_name_via_xml(temp_dir, app_yaml_loc)
942
+ language = "java"
943
+ # don't remove user's jar files, they may have their own jars in it
944
+ #FileUtils.rm_rf("/tmp/#{temp_dir}/war/WEB-INF/lib/", :secure => true)
945
+ FileUtils.mkdir_p("/tmp/#{temp_dir}/war/WEB-INF/lib")
946
+ temp_dir2 = CommonFunctions.get_random_alphanumeric
947
+ FileUtils.rm_rf("/tmp/#{temp_dir2}", :secure => true)
948
+ FileUtils.mkdir_p("/tmp/#{temp_dir2}")
949
+ FileUtils.rm_f("/tmp/#{temp_dir}/#{filename}")
950
+ CommonFunctions.shell("cd /tmp/#{temp_dir}; tar -czf ../#{temp_dir2}/#{app_name}.tar.gz .")
951
+ file = "/tmp/#{temp_dir2}/#{app_name}.tar.gz"
952
+ else
953
+ FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
954
+ raise AppEngineConfigException.new("app_name was #{app_file}, " +
955
+ "which was not a recognized value.")
956
+ end
957
+
958
+ if app_name.nil?
959
+ FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
960
+ raise AppEngineConfigException.new("AppEngine tar file is invalid - " +
961
+ "Doesn't have an app name in #{app_file}")
962
+ end
963
+
964
+ FileUtils.rm_rf("/tmp/#{temp_dir}", :secure => true)
965
+ CommonFunctions.warn_on_large_app_size(file)
966
+ return app_name, file, language
967
+ end
968
+
969
+
970
+ def self.get_app_name_via_yaml(temp_dir, app_yaml_loc)
971
+ app_yaml_loc = "/tmp/" + temp_dir + "/" + app_yaml_loc
972
+
973
+ begin
974
+ tree = YAML.load_file(app_yaml_loc.chomp)
975
+ rescue ArgumentError
976
+ raise AppScaleException.new(MALFORMED_YAML)
977
+ end
978
+
979
+ app_name = String(tree["application"])
980
+ return app_name
981
+ end
982
+
983
+
984
+ def self.get_app_name_via_xml(temp_dir, xml_loc)
985
+ xml_loc = "/tmp/" + temp_dir + "/" + xml_loc
986
+ web_xml_contents = (File.open(xml_loc) { |f| f.read }).chomp
987
+ app_name = web_xml_contents.scan(/<application>([\w\d-]+)<\/application>/).flatten.to_s
988
+ app_name = nil if app_name == ""
989
+ return app_name
990
+ end
991
+
992
+
993
+ private
994
+
995
+
996
+ def self.grab_file filename
997
+ filename = File.expand_path(filename)
998
+ content = nil
999
+ begin
1000
+ content = File.open(filename) { |f| f.read.chomp! }
1001
+ rescue Errno::ENOENT
1002
+ end
1003
+ content
1004
+ end
1005
+
1006
+
1007
+ def self.obscure_string(string)
1008
+ return string if string.length < 4
1009
+ last_four = string[string.length-4, string.length]
1010
+ obscured = "*" * (string.length-4)
1011
+ return obscured + last_four
1012
+ end
1013
+
1014
+
1015
+ def self.obscure_array(array)
1016
+ return array.map {|string| obscure_string(string)}
1017
+ end
1018
+
1019
+
1020
+ def self.obscure_creds(creds)
1021
+ obscured = {}
1022
+ creds.each { |k, v|
1023
+ if CLOUDY_CREDS.include?(k)
1024
+ obscured[k] = self.obscure_string(v)
1025
+ else
1026
+ obscured[k] = v
1027
+ end
1028
+ }
1029
+
1030
+ return obscured
1031
+ end
1032
+
1033
+
1034
+ def self.does_image_have_location?(ip, location, key)
1035
+ ret_val = CommonFunctions.shell("ssh -i #{key} #{SSH_OPTIONS} 2>&1 root@#{ip} 'ls #{location}'; echo $?").chomp[-1]
1036
+ return ret_val.chr == "0"
1037
+ end
1038
+
1039
+
1040
+ def self.ensure_image_is_appscale(ip, key)
1041
+ return if self.does_image_have_location?(ip, "/etc/appscale", key)
1042
+ raise AppScaleException.new("The image at #{ip} is not an AppScale image." +
1043
+ " Please install AppScale on it and try again.")
1044
+ end
1045
+
1046
+
1047
+ def self.ensure_db_is_supported(ip, db, key)
1048
+ return if self.does_image_have_location?(ip, "/etc/appscale/#{VER_NUM}/#{db}", key)
1049
+ raise AppScaleException.new("The image at #{ip} does not have support for #{db}." +
1050
+ " Please install support for this database and try again.")
1051
+ end
1052
+
1053
+
1054
+ def self.confirm_app_removal(confirm, app_name)
1055
+ if confirm
1056
+ return "YES"
1057
+ end
1058
+
1059
+ Kernel.print "We are about to attempt to remove your application, #{app_name}." +
1060
+ "\nAre you sure you want to remove this application (Y/N)? "
1061
+ STDOUT.flush
1062
+
1063
+ loop {
1064
+ result = STDIN.gets.chomp.upcase
1065
+ if result == "Y" or result == "YES"
1066
+ return "YES"
1067
+ end
1068
+ if result == "N" or result == "NO"
1069
+ return "NO"
1070
+ end
1071
+ Kernel.print "Please type in 'yes' or 'no'.\nAre you sure you want to remove this application (Y/N)? "
1072
+ }
1073
+ end
1074
+
1075
+
1076
+ def self.remove_app(app_name, keyname)
1077
+ secret_key = CommonFunctions.get_secret_key(keyname)
1078
+ head_node_ip = CommonFunctions.get_head_node_ip(keyname)
1079
+ acc = AppControllerClient.new(head_node_ip, secret_key)
1080
+ userappserver_ip = acc.get_userappserver_ip()
1081
+
1082
+ uac = UserAppClient.new(userappserver_ip, secret_key)
1083
+ app_exists = uac.does_app_exist?(app_name, retry_on_except=true)
1084
+
1085
+ if !app_exists
1086
+ raise AppEngineConfigException.new(AppScaleTools::APP_NOT_RUNNING)
1087
+ end
1088
+
1089
+ load_balancer_ip = CommonFunctions.get_load_balancer_ip(keyname)
1090
+ acc.stop_app(app_name)
1091
+
1092
+ Kernel.puts "Please wait for your app to shut down."
1093
+ loop {
1094
+ if !acc.app_is_running?(app_name)
1095
+ break
1096
+ end
1097
+ Kernel.sleep(5)
1098
+ }
1099
+ end
1100
+
1101
+
1102
+ def self.scp_ssh_key_to_ip(ip, ssh_key, pub_key)
1103
+ Kernel.puts CommonFunctions.shell("scp -i #{ssh_key} #{ssh_key} root@#{ip}:.ssh/id_rsa")
1104
+ # this is needed for EC2 integration.
1105
+ Kernel.puts CommonFunctions.shell("scp -i #{ssh_key} #{ssh_key} root@#{ip}:.ssh/id_dsa")
1106
+ Kernel.puts CommonFunctions.shell("scp -i #{ssh_key} #{pub_key} root@#{ip}:.ssh/id_rsa.pub")
1107
+ end
1108
+
1109
+
1110
+ def self.generate_rsa_key(keyname)
1111
+ path = File.expand_path("~/.appscale/#{keyname}")
1112
+ backup_key = File.expand_path("~/.appscale/#{keyname}.key")
1113
+ pub_key = File.expand_path("~/.appscale/#{keyname}.pub")
1114
+
1115
+ #FileUtils.rm_f([path, backup_key, pub_key])
1116
+ unless File.exists?(path) and File.exists?(pub_key)
1117
+ FileUtils.rm_f([path, backup_key, pub_key])
1118
+ Kernel.puts CommonFunctions.shell("ssh-keygen -t rsa -N '' -f #{path}")
1119
+ end
1120
+ FileUtils.chmod(0600, [path, pub_key])
1121
+ return pub_key, backup_key
1122
+ end
1123
+
1124
+
1125
+ def self.ssh_copy_id(ip, path, auto, expect_script, password)
1126
+ Kernel.puts "\n\n"
1127
+ Kernel.puts "Executing ssh-copy-id for host : " + ip
1128
+ Kernel.puts "------------------------------"
1129
+
1130
+ if auto
1131
+ Kernel.puts CommonFunctions.shell("#{expect_script} root@#{ip} #{path} #{password}")
1132
+ else
1133
+ Kernel.puts CommonFunctions.shell("ssh-copy-id -i #{path} root@#{ip}")
1134
+ end
1135
+
1136
+ # Check the exit status of the above shell command
1137
+ if $?.to_i != 0
1138
+ raise AppScaleException.new("ERROR ! Unable to ssh-copy-id to host : #{ip}")
1139
+ end
1140
+ end
1141
+
1142
+
1143
+ def self.validate_run_instances_options(options)
1144
+ infrastructure = options['infrastructure']
1145
+ machine = options['machine']
1146
+ # In non-hybrid cloud deployments, the user must specify the machine image
1147
+ # (emi or ami) to use. Abort if they didn't.
1148
+ if infrastructure && infrastructure != "hybrid" && machine.nil?
1149
+ raise BadConfigurationException.new(AppScaleTools::NO_MACHINE_SET)
1150
+ end
1151
+
1152
+ if infrastructure && infrastructure != "hybrid"
1153
+ VMTools.verify_credentials_are_set_correctly(infrastructure)
1154
+ end
1155
+
1156
+ # If the user hasn't given us an ips.yaml file, then they're running in a cloud
1157
+ # deployment. They need to give us a machine image id to spawn up - if they
1158
+ # haven't, fail.
1159
+ if options['ips'].nil? and machine.nil?
1160
+ raise BadConfigurationException.new(EC2_USAGE_MSG)
1161
+ end
1162
+
1163
+ keyname = options['keyname']
1164
+ locations_yaml = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
1165
+ if File.exists?(locations_yaml) and !options['force']
1166
+ error_msg = "An AppScale instance is already running with the given" +
1167
+ " keyname, #{keyname}. Please terminate that instance first with the" +
1168
+ " following command:\n\nappscale-terminate-instances --keyname " +
1169
+ "#{keyname} <--ips path-to-ips.yaml if using non-cloud deployment>"
1170
+ raise BadConfigurationException.new(error_msg)
1171
+ end
1172
+ end
1173
+
1174
+
1175
+ def self.print_starting_message(infrastructure, instance_type)
1176
+ if infrastructure and infrastructure != "hybrid"
1177
+ deployment = "cloud environment with the #{infrastructure} tools" +
1178
+ " with instance type #{instance_type}"
1179
+ elsif infrastructure and infrastructure == "hybrid"
1180
+ # TODO - say which environments
1181
+ deployment = "hybrid environment"
1182
+ else
1183
+ deployment = "non-cloud environment"
1184
+ end
1185
+
1186
+ Kernel.puts "About to start AppScale over a #{deployment}."
1187
+ end
1188
+
1189
+ def self.generate_node_layout(options)
1190
+ remote_options = {
1191
+ :infrastructure => options['infrastructure'],
1192
+ :database => options['table'],
1193
+ :min_images => options['min_images'],
1194
+ :max_images => options['max_images'],
1195
+ :replication => options['replication'],
1196
+ :read_factor => options['voldemort_r'],
1197
+ :write_factor => options['voldemort_w'],
1198
+ }
1199
+
1200
+ node_layout = NodeLayout.new(options['ips'], remote_options)
1201
+
1202
+ if !node_layout.valid?
1203
+ raise BadConfigurationException.new("There were errors with the yaml " +
1204
+ "file: \n#{node_layout.errors}")
1205
+ end
1206
+
1207
+ return node_layout
1208
+ end
1209
+
1210
+
1211
+ def self.generate_appscale_credentials(options, node_layout, head_node_ip,
1212
+ ips_to_use, ssh_key)
1213
+
1214
+ table = options['table']
1215
+ keypath = ssh_key.scan(/([\d|\w|\.]+)\Z/).flatten.to_s
1216
+
1217
+ creds = {
1218
+ "table" => "#{table}",
1219
+ "hostname" => "#{head_node_ip}",
1220
+ "ips" => "#{ips_to_use}",
1221
+ "keyname" => "#{options['keyname']}",
1222
+ "keypath" => "#{keypath}",
1223
+ "replication" => "#{node_layout.replication_factor}",
1224
+ "appengine" => "#{options['appengine']}",
1225
+ "autoscale" => "#{options['autoscale']}",
1226
+ "group" => "#{options['group']}"
1227
+ }
1228
+
1229
+ if table == "voldemort"
1230
+ voldemort_creds = {
1231
+ "voldemortr" => "#{node_layout.read_factor}",
1232
+ "voldemortw" => "#{node_layout.write_factor}"
1233
+ }
1234
+ creds.merge!(voldemort_creds)
1235
+ end
1236
+
1237
+ if table == "simpledb"
1238
+ simpledb_creds = {
1239
+ "SIMPLEDB_ACCESS_KEY" => ENV['SIMPLEDB_ACCESS_KEY'],
1240
+ "SIMPLEDB_SECRET_KEY" => ENV['SIMPLEDB_SECRET_KEY']
1241
+ }
1242
+ creds.merge!(simpledb_creds)
1243
+ end
1244
+
1245
+ infrastructure = options['infrastructure']
1246
+ if VALID_CLOUD_TYPES.include?(infrastructure)
1247
+ if infrastructure == "hybrid"
1248
+ creds.merge!(VMTools.get_hybrid_creds(node_layout))
1249
+ else
1250
+ creds.merge!(VMTools.get_cloud_creds(node_layout, options))
1251
+ end
1252
+ end
1253
+
1254
+ if options['restore_from_tar']
1255
+ db_backup_location = "/root/db-backup.tar.gz"
1256
+ tar_creds = {"restore_from_tar" => db_backup_location}
1257
+ creds.merge!(tar_creds)
1258
+
1259
+ CommonFunctions.scp_file(options['restore_from_tar'], db_backup_location, head_node_ip, true_key)
1260
+ end
1261
+
1262
+ if options['restore_from_ebs']
1263
+ ebs_creds = {"restore_from_tar" => options['restore_from_ebs']}
1264
+ creds.merge!(tar_creds)
1265
+ end
1266
+
1267
+ if options['restore_neptune_info']
1268
+ remote_neptune_info_location = "/etc/appscale/neptune_info.txt"
1269
+ CommonFunctions.scp_file(options['restore_neptune_info'],
1270
+ remote_neptune_info_location, head_node_ip, ssh_key)
1271
+ Kernel.puts "Neptune data restored!"
1272
+ end
1273
+
1274
+ return creds
1275
+ end
1276
+
1277
+
1278
+ def self.copy_keys(secret_key_location, ip, ssh_key, options)
1279
+ remote_secret_key_location = "/etc/appscale/secret.key"
1280
+ CommonFunctions.scp_file(secret_key_location, remote_secret_key_location,
1281
+ ip, ssh_key)
1282
+
1283
+ remote_ssh_key_location = "/etc/appscale/ssh.key"
1284
+ CommonFunctions.scp_file(ssh_key, remote_ssh_key_location, ip, ssh_key)
1285
+
1286
+ cert_loc, key_loc = VMTools.get_vmm_keys(options)
1287
+
1288
+ remote_cert_loc = "/etc/appscale/certs/mycert.pem"
1289
+ CommonFunctions.scp_file(cert_loc, remote_cert_loc, ip, ssh_key)
1290
+
1291
+ remote_key_loc = "/etc/appscale/certs/mykey.pem"
1292
+ CommonFunctions.scp_file(key_loc, remote_key_loc, ip, ssh_key)
1293
+
1294
+ infrastructure = options["infrastructure"]
1295
+ if VALID_CLOUD_TYPES.include?(infrastructure) and infrastructure == "hybrid"
1296
+ cloud_num = 1
1297
+
1298
+ loop {
1299
+ cloud_type = ENV["CLOUD#{cloud_num}_TYPE"]
1300
+ break if cloud_type.nil?
1301
+
1302
+ Kernel.puts "Copying over credentials for cloud #{cloud_num}"
1303
+ cert = ENV["CLOUD#{cloud_num}_EC2_CERT"]
1304
+ private_key = ENV["CLOUD#{cloud_num}_EC2_PRIVATE_KEY"]
1305
+ CommonFunctions.copy_cloud_keys(cloud_num, ip, ssh_key,
1306
+ options['verbose'], cert, private_key)
1307
+ cloud_num += 1
1308
+ }
1309
+ elsif VALID_CLOUD_TYPES.include?(infrastructure)
1310
+ Kernel.puts "Copying over credentials for cloud"
1311
+ cloud_num = 1
1312
+ cert = ENV["EC2_CERT"]
1313
+ private_key = ENV["EC2_PRIVATE_KEY"]
1314
+ CommonFunctions.copy_cloud_keys(cloud_num, ip, ssh_key,
1315
+ options['verbose'], cert, private_key)
1316
+ else
1317
+ cloud_num = 1
1318
+ CommonFunctions.copy_cloud_keys(cloud_num, ip, ssh_key,
1319
+ options['verbose'], cert_loc, key_loc)
1320
+ end
1321
+ end
1322
+
1323
+
1324
+ def self.copy_cloud_keys(cloud_num, ip, ssh_key, verbose, cert, private_key)
1325
+ cloud_key_dir = "/etc/appscale/keys/cloud#{cloud_num}"
1326
+ make_cloud_key_dir = "mkdir -p #{cloud_key_dir}"
1327
+ CommonFunctions.run_remote_command(ip, make_cloud_key_dir, ssh_key, verbose)
1328
+ CommonFunctions.scp_file(cert, "#{cloud_key_dir}/mycert.pem", ip, ssh_key)
1329
+ CommonFunctions.scp_file(private_key, "#{cloud_key_dir}/mykey.pem", ip, ssh_key)
1330
+ end
1331
+
1332
+
1333
+ def self.start_appcontroller(ip, ssh_key, verbose)
1334
+ # TODO(cgb) - check the return value of the following command. a user could
1335
+ # accidentally remove that file and cause this to return a bad value
1336
+ remote_home = CommonFunctions.get_remote_appscale_home(ip, ssh_key)
1337
+ start = "ruby #{remote_home}/AppController/djinnServer.rb"
1338
+ stop = "ruby #{remote_home}/AppController/terminate.rb"
1339
+
1340
+ Kernel.puts "Starting server at #{ip}"
1341
+
1342
+ # remove any possible appcontroller state that may not have been
1343
+ # properly removed in non-cloud runs
1344
+ remove_state = "rm -rf /etc/appscale/appcontroller-state.json"
1345
+ CommonFunctions.run_remote_command(ip, remove_state, ssh_key, verbose)
1346
+
1347
+ GodInterface.start_god(ip, ssh_key)
1348
+ Kernel.sleep(1)
1349
+
1350
+ begin
1351
+ Timeout::timeout(60) {
1352
+ GodInterface.start(:controller, start, stop,
1353
+ AppScaleTools::DJINN_SERVER_PORT,
1354
+ {'APPSCALE_HOME' => remote_home}, ip, ssh_key)
1355
+
1356
+ Kernel.puts "Please wait for the controller to finish " +
1357
+ "pre-processing tasks."
1358
+
1359
+ CommonFunctions.sleep_until_port_is_open(ip,
1360
+ AppScaleTools::DJINN_SERVER_PORT, AppScaleTools::USE_SSL)
1361
+ }
1362
+ rescue Timeout::Error
1363
+ retry
1364
+ end
1365
+ end
1366
+
1367
+
1368
+ def self.backup_neptune_info(keyname, shadow_ip, backup_neptune_info)
1369
+ remote_file = "/etc/appscale/neptune_info.txt"
1370
+ command = "scp -i ~/.appscale/#{keyname}.key " +
1371
+ "root@#{shadow_ip}:#{remote_file} #{backup_neptune_info}"
1372
+ CommonFunctions.shell("#{command}")
1373
+ Kernel.puts "Your Neptune information has been backed up to #{backup_neptune_info}\n"
1374
+ end
1375
+
1376
+
1377
+ def self.terminate_via_infrastructure(infrastructure, keyname, shadow_ip, secret)
1378
+ Kernel.puts "About to terminate instances spawned via #{infrastructure} " +
1379
+ "with keyname '#{keyname}'..."
1380
+ Kernel.sleep(2)
1381
+
1382
+ acc = AppControllerClient.new(shadow_ip, secret)
1383
+ if acc.is_live?
1384
+ acc.kill()
1385
+ # TODO(cgb): read the group from the locations.yaml file and delete that
1386
+ cmd = "#{infrastructure}-delete-group appscale"
1387
+ Kernel.puts CommonFunctions.shell("#{cmd}")
1388
+ Kernel.puts "Terminated AppScale in cloud deployment."
1389
+ else
1390
+ VMTools.terminate_infrastructure_machines(infrastructure, keyname)
1391
+ end
1392
+ end
1393
+
1394
+
1395
+ def self.terminate_via_vmm(keyname, verbose)
1396
+ Kernel.puts "About to terminate instances spawned via Xen/KVM with " +
1397
+ "keyname '#{keyname}'..."
1398
+ Kernel.sleep(2)
1399
+
1400
+ ssh_key_location = "~/.appscale/#{keyname}"
1401
+ live_ips = CommonFunctions.stop_appcontrollers(keyname, ssh_key_location,
1402
+ verbose)
1403
+ CommonFunctions.wait_for_appcontrollers_to_stop(live_ips, ssh_key_location)
1404
+ end
1405
+
1406
+
1407
+ def self.stop_appcontrollers(keyname, ssh_key, verbose)
1408
+ command = "service appscale-controller stop"
1409
+
1410
+ ips = CommonFunctions.get_all_public_ips(keyname)
1411
+ live_ips = []
1412
+
1413
+ threads = []
1414
+ ips.each { |ip|
1415
+ threads << Thread.new {
1416
+ CommonFunctions.run_remote_command(ip, command, ssh_key, verbose)
1417
+ Kernel.sleep(5)
1418
+ CommonFunctions.run_remote_command(ip, command, ssh_key, verbose)
1419
+ live_ips << ip
1420
+ }
1421
+ }
1422
+
1423
+ threads.each { |t| t.join }
1424
+ return live_ips
1425
+ end
1426
+
1427
+
1428
+ def self.wait_for_appcontrollers_to_stop(ips, ssh_key)
1429
+ boxes_shut_down = 0
1430
+ ips.each { |ip|
1431
+ Kernel.print "Shutting down AppScale components at #{ip}"
1432
+ STDOUT.flush
1433
+ loop {
1434
+ remote_cmd = "ssh root@#{ip} #{SSH_OPTIONS} -i #{ssh_key} 'ps x'"
1435
+ ps = CommonFunctions.shell(remote_cmd)
1436
+ processes_left = ps.scan(/appscale-controller stop/).length
1437
+ break if processes_left.zero?
1438
+ Kernel.print '.'
1439
+ STDOUT.flush
1440
+ Kernel.sleep(0.3)
1441
+ }
1442
+ boxes_shut_down += 1
1443
+ Kernel.print "\n"
1444
+ }
1445
+
1446
+ if boxes_shut_down.zero?
1447
+ raise AppScaleException.new(
1448
+ AppScaleTools::UNABLE_TO_TERMINATE_ANY_MACHINES)
1449
+ end
1450
+
1451
+ Kernel.puts "Terminated AppScale across #{boxes_shut_down} boxes."
1452
+ end
1453
+
1454
+
1455
+ def self.delete_appscale_files(keyname)
1456
+ locations_yaml = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
1457
+ FileUtils.rm_f(locations_yaml) if File.exists?(locations_yaml)
1458
+
1459
+ retval_file = File.expand_path("~/.appscale/retval")
1460
+ FileUtils.rm_f(retval_file) if File.exists?(retval_file)
1461
+ end
1462
+
1463
+
1464
+ def self.verbose(msg, verbose)
1465
+ Kernel.puts msg if verbose
1466
+ end
1467
+ end