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,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