neptune 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/ruby
2
+ # Programmer: Chris Bunch (cgb@cs.ucsb.edu)
3
+
4
+ require 'app_controller_client'
5
+ require 'common_functions'
6
+ require 'custom_exceptions'
7
+ require 'neptune'
8
+
9
+
10
+ # The promise gem gives us futures / promises out-of-the-box, which we need
11
+ # to hide the fact that babel jobs are asynchronous.
12
+ require 'rubygems'
13
+ require 'promise'
14
+ require 'future'
15
+
16
+
17
+ # If the user doesn't give us enough info to infer what bucket we should place
18
+ # their code in, this message is displayed and execution aborts.
19
+ NEEDS_BUCKET_INFO = "When running Babel jobs with local inputs / code, the " +
20
+ "bucket to store them in must be specified by either the :bucket_name " +
21
+ "parameter or the BABEL_BUCKET_NAME environment variable."
22
+
23
+
24
+ # The constant string that a Neptune output job returns if the output does not
25
+ # yet exist.
26
+ DOES_NOT_EXIST = "error: output does not exist"
27
+
28
+
29
+ # The initial amount of time, in seconds, to sleep between output job requests.
30
+ # An exponential backoff is used with this value as the starting sleep time.
31
+ SLEEP_TIME = 5 # seconds
32
+
33
+
34
+ # The maximum amount of time that we should sleep to, when waiting for output
35
+ # job requests.
36
+ MAX_SLEEP_TIME = 60 # seconds
37
+
38
+ # Babel provides a nice wrapper around Neptune jobs. Instead of making users
39
+ # write multiple Neptune jobs to actually run code (e.g., putting input in the
40
+ # datastore, run the job, get the output back), Babel automatically handles
41
+ # this.
42
+ def babel(params)
43
+ # Since this whole function should run asynchronously, we run it as a future.
44
+ # It automatically starts running in a new thread, and attempting to get the
45
+ # value of what this returns causes it to block until the job completes.
46
+ future {
47
+ job_data = BabelHelper.convert_from_neptune_params(params)
48
+ NeptuneHelper.validate_storage_params(job_data) # adds in S3 storage params
49
+
50
+ # :code is the only required parameter - everything else can use default vals
51
+ NeptuneHelper.require_param("@code", job_data)
52
+
53
+ if job_data["@output"].nil? or job_data["@output"].empty?
54
+ job_data["@output"] = BabelHelper.generate_output_location(job_data)
55
+ end
56
+ BabelHelper.ensure_output_does_not_exist(job_data)
57
+
58
+ if job_data["@is_remote"]
59
+ BabelHelper.validate_inputs(job_data)
60
+ else
61
+ BabelHelper.put_code(job_data)
62
+ BabelHelper.put_inputs(job_data)
63
+ end
64
+
65
+ BabelHelper.run_job(job_data)
66
+ # So actually retrieving the job's output is done via a promise, so only if
67
+ # the user actually uses the value do we actually go and poll for output.
68
+ # The running of the job is done above, outside of the promise, so
69
+ # the job is always run, regardless of whether or not we get its output.
70
+ BabelHelper.wait_and_get_output(job_data)
71
+ # promise { BabelHelper.wait_and_get_output(job_data) }
72
+ }
73
+ end
74
+
75
+
76
+ # This module provides convenience functions for babel().
77
+ module BabelHelper
78
+ # If the user fails to give us an output location, this function will generate
79
+ # one for them, based on either the location of their code (for remotely
80
+ # specified code), or a babel parameter (for locally specified code).
81
+ def self.generate_output_location(job_data)
82
+ if job_data["@is_remote"]
83
+ # We already know the bucket name - the same one that the user
84
+ # has told us their code is located in.
85
+ prefix = job_data["@code"].scan(/\/(.*?)\//)[0].to_s
86
+ else
87
+ prefix = self.get_bucket_for_local_data(job_data)
88
+ end
89
+
90
+ return "/#{prefix}/babel/temp-#{CommonFunctions.get_random_alphanumeric()}"
91
+ end
92
+
93
+ # Provides a common way for callers to get the name of the bucket that
94
+ # should be used for Neptune jobs where the code is stored locally.
95
+ def self.get_bucket_for_local_data(job_data)
96
+ bucket_name = job_data["@bucket_name"] || ENV['BABEL_BUCKET_NAME']
97
+
98
+ if bucket_name.nil?
99
+ raise BadConfigurationException.new(NEEDS_BUCKET_INFO)
100
+ end
101
+
102
+ # If the bucket name starts with a slash, remove it
103
+ if bucket_name[0].chr == "/"
104
+ bucket_name = bucket_name[1, bucket_name.length]
105
+ end
106
+
107
+ return bucket_name
108
+ end
109
+
110
+ # For jobs where the code is stored remotely, this method ensures that
111
+ # the code and any possible inputs actually do exist, before attempting to
112
+ # use them for computation.
113
+ def self.validate_inputs(job_data)
114
+ controller = self.get_appcontroller(job_data)
115
+
116
+ # First, make sure the code exists
117
+ NeptuneHelper.require_file_to_exist(job_data["@code"], job_data, controller)
118
+
119
+ if job_data["@argv"].nil? or job_data["@argv"].empty?
120
+ return
121
+ end
122
+
123
+ # We assume anything that begins with a slash is a remote file
124
+ job_data["@argv"].each { |arg|
125
+ if arg[0].chr == "/"
126
+ NeptuneHelper.require_file_to_exist(arg, job_data, controller)
127
+ end
128
+ }
129
+ end
130
+
131
+ # To avoid accidentally overwriting outputs from previous jobs, we first
132
+ # check to make sure an output file doesn't exist before starting a new job
133
+ # with the given name.
134
+ def self.ensure_output_does_not_exist(job_data)
135
+ file = job_data["@output"]
136
+ controller = self.get_appcontroller(job_data)
137
+ puts job_data.inspect
138
+ NeptuneHelper.require_file_to_not_exist(file, job_data, controller)
139
+ end
140
+
141
+ # Returns an AppControllerClient for the given job data.
142
+ def self.get_appcontroller(job_data)
143
+ keyname = job_data["@keyname"] || "appscale"
144
+ shadow_ip = CommonFunctions.get_from_yaml(keyname, :shadow)
145
+ secret = CommonFunctions.get_secret_key(keyname)
146
+ return AppControllerClient.new(shadow_ip, secret)
147
+ end
148
+
149
+ # Stores the user's code (and the directory it's in, and directories in the
150
+ # same directory as the user's code, since there could be libraries used)
151
+ # in the remote datastore.
152
+ def self.put_code(job_data)
153
+ code_dir = File.dirname(job_data["@code"])
154
+ code = File.basename(job_data["@code"])
155
+ remote_code_dir = self.put_file(code_dir, job_data)
156
+ job_data["@code"] = remote_code_dir + "/" + code
157
+ return job_data["@code"]
158
+ end
159
+
160
+ # If any input files are specified, they are copied to the remote datastore
161
+ # via Neptune 'input' jobs. Inputs are assumed to be files on the local
162
+ # filesystem if they begin with a slash, and job_data gets updated with
163
+ # the remote location of these files.
164
+ def self.put_inputs(job_data)
165
+ if job_data["@argv"].nil? or job_data["@argv"].empty?
166
+ return job_data
167
+ end
168
+
169
+ job_data["@argv"].each_index { |i|
170
+ arg = job_data["@argv"][i]
171
+ if arg[0].chr == "/"
172
+ job_data["@argv"][i] = self.put_file(arg, job_data)
173
+ end
174
+ }
175
+
176
+ return job_data
177
+ end
178
+
179
+ # If the user gives us local code or local inputs, this function will
180
+ # run a Neptune 'input' job to store the data remotely.
181
+ def self.put_file(local_path, job_data)
182
+ input_data = self.convert_to_neptune_params(job_data)
183
+ input_data[:type] = "input"
184
+ input_data[:local] = local_path
185
+
186
+ bucket_name = self.get_bucket_for_local_data(job_data)
187
+ input_data[:remote] = "/#{bucket_name}/babel#{local_path}"
188
+
189
+ Kernel.neptune(input_data)
190
+
191
+ return input_data[:remote]
192
+ end
193
+
194
+ # Neptune internally uses job_data with keys of the form @name, but since the
195
+ # user has given them to us in the form :name, we convert it here.
196
+ # TODO(cgb): It looks like this conversion to/from may be unnecessary since
197
+ # neptune() just re-converts it - how can we remove it?
198
+ def self.convert_from_neptune_params(params)
199
+ job_data = {}
200
+ params.each { |k, v|
201
+ key = "@#{k}"
202
+ job_data[key] = v
203
+ }
204
+ return job_data
205
+ end
206
+
207
+ # Neptune input jobs expect keys of the form :name, but since we've already
208
+ # converted them to the form @name, this function reverses that conversion.
209
+ def self.convert_to_neptune_params(job_data)
210
+ neptune_params = {}
211
+
212
+ job_data.each { |k, v|
213
+ key = k.delete("@").to_sym
214
+ neptune_params[key] = v
215
+ }
216
+
217
+ return neptune_params
218
+ end
219
+
220
+ # Constructs a Neptune job to run the user's code as a Babel job (task queue)
221
+ # from the given parameters.
222
+ def self.run_job(job_data)
223
+ run_data = self.convert_to_neptune_params(job_data)
224
+ run_data[:type] = "babel"
225
+
226
+ # TODO(cgb): Once AppScale+Babel gets support for RabbitMQ, change this to
227
+ # exec tasks over it, instead of locally.
228
+ if job_data["@run_local"].nil?
229
+ run_data[:run_local] = true
230
+ run_data[:engine] = "executor-sqs"
231
+ end
232
+
233
+ return Kernel.neptune(run_data)
234
+ end
235
+
236
+ # Constructs a Neptune job to get the output of a Babel job. If the job is not
237
+ # yet finished, this function waits until it does, and then returns the output
238
+ # of the job.
239
+ def self.wait_and_get_output(job_data)
240
+ output_data = self.convert_to_neptune_params(job_data)
241
+ output_data[:type] = "output"
242
+
243
+ output = ""
244
+ time_to_sleep = SLEEP_TIME
245
+ loop {
246
+ output = Kernel.neptune(output_data)[:output]
247
+ if output == DOES_NOT_EXIST
248
+ # Exponentially back off, up to a limit of MAX_SLEEP_TIME
249
+ Kernel.sleep(time_to_sleep)
250
+ if time_to_sleep < MAX_SLEEP_TIME
251
+ time_to_sleep *= 2
252
+ end
253
+ else
254
+ break
255
+ end
256
+ }
257
+
258
+ return output
259
+ end
260
+ end
@@ -9,11 +9,7 @@ require 'socket'
9
9
  require 'timeout'
10
10
  require 'yaml'
11
11
 
12
- module Kernel
13
- def shell(command)
14
- return `#{command}`
15
- end
16
- end
12
+ require 'custom_exceptions'
17
13
 
18
14
  # A helper module that aggregates functions that are not part of Neptune's
19
15
  # core functionality. Specifically, this module contains methods to scp
@@ -21,6 +17,27 @@ end
21
17
  # often needed to determine which machine should be used for computation
22
18
  # or to copy over code and input files.
23
19
  module CommonFunctions
20
+ # Executes a command and returns the result. Is needed to get around
21
+ # Flexmock's inability to mock out Kernel:` (the standard shell exec
22
+ # method).
23
+ def self.shell(cmd)
24
+ return `#{cmd}`
25
+ end
26
+
27
+ # Returns a random string composed of alphanumeric characters, as long
28
+ # as the user requests.
29
+ def self.get_random_alphanumeric(length=10)
30
+ random = ""
31
+ possible = "0123456789abcdefghijklmnopqrstuvxwyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
32
+ possibleLength = possible.length
33
+
34
+ length.times { |index|
35
+ random << possible[rand(possibleLength)]
36
+ }
37
+
38
+ return random
39
+ end
40
+
24
41
  # Copies a file to the Shadow node (head node) within AppScale.
25
42
  # The caller specifies
26
43
  # the local file location, the destination where the file should be
@@ -30,14 +47,11 @@ module CommonFunctions
30
47
  def self.scp_to_shadow(local_file_loc,
31
48
  remote_file_loc,
32
49
  keyname,
33
- is_dir=false,
34
- file=File,
35
- get_from_yaml=CommonFunctions.method(:get_from_yaml),
36
- scp_file=CommonFunctions.method(:scp_file))
50
+ is_dir=false)
37
51
 
38
- shadow_ip = get_from_yaml.call(keyname, :shadow, file)
39
- ssh_key = file.expand_path("~/.appscale/#{keyname}.key")
40
- scp_file.call(local_file_loc, remote_file_loc, shadow_ip, ssh_key, is_dir)
52
+ shadow_ip = CommonFunctions.get_from_yaml(keyname, :shadow)
53
+ ssh_key = File.expand_path("~/.appscale/#{keyname}.key")
54
+ CommonFunctions.scp_file(local_file_loc, remote_file_loc, shadow_ip, ssh_key, is_dir)
41
55
  end
42
56
 
43
57
  # Performs the actual remote copying of files: given the IP address
@@ -47,22 +61,22 @@ module CommonFunctions
47
61
  # wrong IP is given. If the user specifies that the file to copy is
48
62
  # actually a directory, we append the -r flag to scp as well.
49
63
  def self.scp_file(local_file_loc, remote_file_loc, target_ip, public_key_loc,
50
- is_dir=false, file=File, fileutils=FileUtils, kernel=Kernel)
64
+ is_dir=false)
51
65
  cmd = ""
52
- local_file_loc = file.expand_path(local_file_loc)
66
+ local_file_loc = File.expand_path(local_file_loc)
53
67
 
54
68
  ssh_args = "-o StrictHostkeyChecking=no 2>&1"
55
69
  ssh_args << " -r " if is_dir
56
70
 
57
- public_key_loc = file.expand_path(public_key_loc)
71
+ public_key_loc = File.expand_path(public_key_loc)
58
72
  cmd = "scp -i #{public_key_loc} #{ssh_args} #{local_file_loc} root@#{target_ip}:#{remote_file_loc}"
59
73
  cmd << "; echo $? >> ~/.appscale/retval"
60
74
 
61
- retval_loc = file.expand_path("~/.appscale/retval")
62
- fileutils.rm_f(retval_loc)
75
+ retval_loc = File.expand_path("~/.appscale/retval")
76
+ FileUtils.rm_f(retval_loc)
63
77
 
64
78
  begin
65
- Timeout::timeout(-1) { kernel.shell("#{cmd}") }
79
+ Timeout::timeout(-1) { CommonFunctions.shell("#{cmd}") }
66
80
  rescue Timeout::Error
67
81
  abort("Remotely copying over files failed. Is the destination machine" +
68
82
  " on and reachable from this computer? We tried the following" +
@@ -70,11 +84,11 @@ module CommonFunctions
70
84
  end
71
85
 
72
86
  loop {
73
- break if file.exists?(retval_loc)
87
+ break if File.exists?(retval_loc)
74
88
  sleep(5)
75
89
  }
76
90
 
77
- retval = (file.open(retval_loc) { |f| f.read }).chomp
91
+ retval = (File.open(retval_loc) { |f| f.read }).chomp
78
92
  if retval != "0"
79
93
  abort("\n\n[#{cmd}] returned #{retval} instead of 0 as expected. Is " +
80
94
  "your environment set up properly?")
@@ -88,16 +102,16 @@ module CommonFunctions
88
102
  # method aborts if the value doesn't exist or the YAML file is malformed.
89
103
  # If the required flag is set to false, it returns nil in either scenario
90
104
  # instead.
91
- def self.get_from_yaml(keyname, tag, required=true, file=File, yaml=YAML)
92
- location_file = file.expand_path("~/.appscale/locations-#{keyname}.yaml")
105
+ def self.get_from_yaml(keyname, tag, required=true)
106
+ location_file = File.expand_path("~/.appscale/locations-#{keyname}.yaml")
93
107
 
94
- if !file.exists?(location_file)
95
- abort("An AppScale instance is not currently running with the provided" +
96
- " keyname, \"#{keyname}\".")
108
+ if !File.exists?(location_file)
109
+ raise BadConfigurationException.new("An AppScale instance is not " +
110
+ "currently running with the provided keyname, \"#{keyname}\".")
97
111
  end
98
112
 
99
113
  begin
100
- tree = yaml.load_file(location_file)
114
+ tree = YAML.load_file(location_file)
101
115
  rescue ArgumentError
102
116
  if required
103
117
  abort("The yaml file you provided was malformed. Please correct any" +
@@ -121,7 +135,7 @@ module CommonFunctions
121
135
  # Returns the secret key needed for communication with AppScale's
122
136
  # Shadow node. This method is a nice frontend to the get_from_yaml
123
137
  # function, as the secret is stored in a YAML file.
124
- def self.get_secret_key(keyname, required=true, file=File, yaml=YAML)
125
- return CommonFunctions.get_from_yaml(keyname, :secret, required, file, yaml)
138
+ def self.get_secret_key(keyname, required=true)
139
+ return CommonFunctions.get_from_yaml(keyname, :secret, required)
126
140
  end
127
141
  end
@@ -0,0 +1,10 @@
1
+ # Programmer: Chris Bunch
2
+
3
+ class AppControllerException < Exception
4
+ end
5
+
6
+ class BadConfigurationException < Exception
7
+ end
8
+
9
+ class FileNotFoundException < Exception
10
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'app_controller_client'
5
5
  require 'common_functions'
6
+ require 'custom_exceptions'
6
7
 
7
8
  # Setting verbose to nil here suppresses the otherwise
8
9
  # excessive SSL cert warning messages that will pollute
@@ -20,6 +21,12 @@ $VERBOSE = nil
20
21
  #MR_RUN_JOB_REQUIRED = %w{ }
21
22
  #MR_REQUIRED = %w{ output }
22
23
 
24
+ # A list of all the Neptune job types that we support
25
+ ALLOWED_JOB_TYPES = %w{acl cicero compile erlang mpi input output ssa babel upc x10}
26
+
27
+ # The string to display for disallowed job types.
28
+ JOB_TYPE_NOT_ALLOWED = "The job type you specified is not supported."
29
+
23
30
  # A list of Neptune jobs that do not require nodes to be spawned
24
31
  # up for computation
25
32
  NO_NODES_NEEDED = ["acl", "input", "output", "compile"]
@@ -34,7 +41,7 @@ ALLOWED_STORAGE_TYPES = ["appdb", "gstorage", "s3", "walrus"]
34
41
 
35
42
  # A list of jobs that require some kind of work to be done before
36
43
  # the actual computation can be performed.
37
- NEED_PREPROCESSING = ["compile", "erlang", "mpi", "ssa"]
44
+ NEED_PREPROCESSING = ["babel", "compile", "erlang", "mpi", "ssa"]
38
45
 
39
46
  # A set of methods and constants that we've monkey-patched to enable Neptune
40
47
  # support. In the future, it is likely that the only exposed / monkey-patched
@@ -45,385 +52,443 @@ NEED_PREPROCESSING = ["compile", "erlang", "mpi", "ssa"]
45
52
  class Object
46
53
  end
47
54
 
48
- # Certain types of jobs need steps to be taken before they
49
- # can be started (e.g., copying input data or code over).
50
- # This method dispatches the right method to use based
51
- # on the type of the job that the user has asked to run.
52
- def do_preprocessing(job_data)
53
- job_type = job_data["@type"]
54
- if !NEED_PREPROCESSING.include?(job_type)
55
- return
56
- end
57
-
58
- preprocess = "preprocess_#{job_type}".to_sym
59
- send(preprocess, job_data)
60
- end
55
+ module NeptuneHelper
56
+ # Certain types of jobs need steps to be taken before they
57
+ # can be started (e.g., copying input data or code over).
58
+ # This method dispatches the right method to use based
59
+ # on the type of the job that the user has asked to run.
60
+ def self.do_preprocessing(job_data, controller)
61
+ job_type = job_data["@type"]
62
+ if !NEED_PREPROCESSING.include?(job_type)
63
+ return
64
+ end
61
65
 
62
- # This preprocessing method copies over the user's code to the
63
- # Shadow node so that it can be compiled there. A future version
64
- # of this method may also copy over libraries as well.
65
- def preprocess_compile(job_data, shell=Kernel.method(:`))
66
- code = File.expand_path(job_data["@code"])
67
- if !File.exists?(code)
68
- abort("The source file #{code} does not exist.")
66
+ # Don't worry about adding on the self. prefix - send will resolve
67
+ # it the right way
68
+ preprocess = "preprocess_#{job_type}".to_sym
69
+ send(preprocess, job_data, controller)
69
70
  end
70
71
 
71
- suffix = code.split('/')[-1]
72
- dest = "/tmp/#{suffix}"
73
- keyname = job_data["@keyname"]
74
- shadow_ip = CommonFunctions.get_from_yaml(keyname, :shadow)
75
-
76
- ssh_args = "-i ~/.appscale/#{keyname}.key -o StrictHostkeyChecking=no root@#{shadow_ip}"
77
- remove_dir = "ssh #{ssh_args} 'rm -rf #{dest}' 2>&1"
78
- puts remove_dir
79
- shell.call(remove_dir)
72
+ # This preprocessing method copies over the user's code to the
73
+ # Shadow node so that it can be compiled there. A future version
74
+ # of this method may also copy over libraries as well.
75
+ def self.preprocess_compile(job_data, controller)
76
+ code = File.expand_path(job_data["@code"])
77
+ if !File.exists?(code)
78
+ raise BadConfigurationException.new("The source file #{code} does not exist.")
79
+ end
80
80
 
81
- CommonFunctions.scp_to_shadow(code, dest, keyname, is_dir=true)
81
+ suffix = code.split('/')[-1]
82
+ dest = "/tmp/#{suffix}"
83
+ keyname = job_data["@keyname"]
84
+ shadow_ip = CommonFunctions.get_from_yaml(keyname, :shadow)
82
85
 
83
- job_data["@code"] = dest
84
- end
86
+ ssh_args = "-i ~/.appscale/#{keyname}.key -o StrictHostkeyChecking=no root@#{shadow_ip}"
87
+ remove_dir = "ssh #{ssh_args} 'rm -rf #{dest}' 2>&1"
88
+ Kernel.puts remove_dir
89
+ CommonFunctions.shell(remove_dir)
90
+ CommonFunctions.scp_to_shadow(code, dest, keyname, is_dir=true)
85
91
 
86
- def preprocess_erlang(job_data, file=File, common_functions=CommonFunctions)
87
- if !job_data["@code"]
88
- abort("When running Erlang jobs, :code must be specified.")
92
+ job_data["@code"] = dest
89
93
  end
90
94
 
91
- source_code = file.expand_path(job_data["@code"])
92
- if !file.exists?(source_code)
93
- abort("The specified code, #{job_data['@code']}," +
94
- " didn't exist. Please specify one that exists and try again")
95
- end
96
- dest_code = "/tmp/"
95
+ def self.preprocess_erlang(job_data, controller)
96
+ self.require_param("@code", job_data)
97
97
 
98
- keyname = job_data["@keyname"]
99
- common_functions.scp_to_shadow(source_code, dest_code, keyname)
100
- end
98
+ source_code = File.expand_path(job_data["@code"])
99
+ if !File.exists?(source_code)
100
+ raise BadConfigurationException.new("The specified code, #{job_data['@code']}," +
101
+ " didn't exist. Please specify one that exists and try again")
102
+ end
103
+ dest_code = "/tmp/"
101
104
 
102
- # This preprocessing method verifies that the user specified the number of nodes
103
- # to use. If they also specified the number of processes to use, we also verify
104
- # that this value is at least as many as the number of nodes (that is, nodes
105
- # can't be underprovisioned in MPI).
106
- def preprocess_mpi(job_data)
107
- if !job_data["@nodes_to_use"]
108
- abort("When running MPI jobs, :nodes_to_use must be specified.")
105
+ keyname = job_data["@keyname"]
106
+ CommonFunctions.scp_to_shadow(source_code, dest_code, keyname)
109
107
  end
110
108
 
111
- if !job_data["@procs_to_use"]
112
- abort("When running MPI jobs, :procs_to_use must be specified.")
113
- end
109
+ # This preprocessing method verifies that the user specified the number of nodes
110
+ # to use. If they also specified the number of processes to use, we also verify
111
+ # that this value is at least as many as the number of nodes (that is, nodes
112
+ # can't be underprovisioned in MPI).
113
+ def self.preprocess_mpi(job_data, controller)
114
+ self.require_param("@nodes_to_use", job_data)
115
+ self.require_param("@procs_to_use", job_data)
116
+
117
+ if job_data["@procs_to_use"]
118
+ p = job_data["@procs_to_use"]
119
+ n = job_data["@nodes_to_use"]
120
+ if p < n
121
+ raise BadConfigurationException.new(":procs_to_use must be at least as " +
122
+ "large as :nodes_to_use.")
123
+ end
124
+ end
114
125
 
115
- if job_data["@procs_to_use"]
116
- p = job_data["@procs_to_use"]
117
- n = job_data["@nodes_to_use"]
118
- if p < n
119
- abort("When specifying both :procs_to_use and :nodes_to_use" +
120
- ", :procs_to_use must be at least as large as :nodes_to_use. Please " +
121
- "change this and try again. You specified :procs_to_use = #{p} and" +
122
- ":nodes_to_use = #{n}.")
126
+ if job_data["@argv"]
127
+ argv = job_data["@argv"]
128
+
129
+ if argv.class == String
130
+ job_data["@argv"] = argv
131
+ elsif argv.class == Array
132
+ job_data["@argv"] = argv.join(' ')
133
+ else
134
+ raise BadConfigurationException.new(":argv must be either a String or Array")
135
+ end
123
136
  end
137
+
138
+ return job_data
124
139
  end
125
140
 
126
- if job_data["@argv"]
127
- argv = job_data["@argv"]
128
- if argv.class != String and argv.class != Array
129
- abort("The value specified for :argv must be either a String or Array")
141
+ # This preprocessing method verifies that the user specified the number of
142
+ # trajectories to run, via either :trajectories or :simulations. Both should
143
+ # not be specified - only one or the other, and regardless of which they
144
+ # specify, convert it to be :trajectories.
145
+ def self.preprocess_ssa(job_data, controller)
146
+ if job_data["@simulations"] and job_data["@trajectories"]
147
+ raise BadConfigurationException.new(":simulations and :trajectories " +
148
+ "not both be specified.")
130
149
  end
131
150
 
132
- if argv.class == Array
133
- job_data["@argv"] = argv.join(' ')
151
+ if job_data["@simulations"]
152
+ job_data["@trajectories"] = job_data["@simulations"]
153
+ job_data.delete("@simulations")
134
154
  end
135
- end
136
155
 
137
- return job_data
138
- end
156
+ self.require_param("@trajectories", job_data)
157
+ return job_data
158
+ end
139
159
 
140
- # This preprocessing method verifies that the user specified the number of
141
- # trajectories to run, via either :trajectories or :simulations. Both should
142
- # not be specified - only one or the other, and regardless of which they
143
- # specify, convert it to be :trajectories.
144
- def preprocess_ssa(job_data)
145
- if job_data["@simulations"] and job_data["@trajectories"]
146
- abort("Both :simulations and :trajectories cannot be specified - use one" +
147
- " or the other.")
160
+ def self.require_param(param, job_data)
161
+ if !job_data[param]
162
+ raise BadConfigurationException.new("#{param} must be specified")
163
+ end
148
164
  end
149
165
 
150
- if job_data["@simulations"]
151
- job_data["@trajectories"] = job_data["@simulations"]
152
- job_data.delete("@simulations")
166
+ def self.require_file_to_exist(file, job_data, controller)
167
+ if controller.does_file_exist?(file, job_data)
168
+ return
169
+ else
170
+ raise FileNotFoundException
171
+ end
153
172
  end
154
173
 
155
- if !job_data["@trajectories"]
156
- abort(":trajectories needs to be specified when running ssa jobs")
174
+ def self.require_file_to_not_exist(file, job_data, controller)
175
+ begin
176
+ self.require_file_to_exist(file, job_data, controller)
177
+ # no exception thrown previously means that the output file exists
178
+ raise BadConfigurationException.new('Output specified already exists')
179
+ rescue FileNotFoundException
180
+ return
181
+ end
157
182
  end
158
183
 
159
- return job_data
160
- end
184
+ # This preprocessing method verifies that the user specified code that
185
+ # should be run, where the output should be placed, and an engine to run over.
186
+ # It also verifies that all files to be used are actually reachable.
187
+ # Supported engines can be found by contacting an AppScale node.
188
+ def self.preprocess_babel(job_data, controller)
189
+ self.require_param("@code", job_data)
190
+ self.require_param("@engine", job_data)
191
+ self.require_param("@output", job_data)
192
+
193
+ # For most code types, the file's name given is the thing to exec.
194
+ # For Java, the actual file to search for is whatever the user gives
195
+ # us, with a .class extension.
196
+ code_file_name = job_data["@code"]
197
+ if !job_data["@executable"].nil? and job_data["@executable"] == "java"
198
+ code_file_name += ".class"
199
+ end
200
+
201
+ self.require_file_to_exist(code_file_name, job_data, controller)
202
+ self.require_file_to_not_exist(job_data["@output"], job_data, controller)
203
+
204
+ if job_data["@argv"]
205
+ argv = job_data["@argv"]
206
+ if argv.class != Array
207
+ raise BadConfigurationException.new("argv must be an array")
208
+ end
161
209
 
162
- def get_job_data(params)
163
- job_data = {}
164
- params.each { |k, v|
165
- key = "@#{k}"
166
- job_data[key] = v
167
- }
210
+ argv.each { |arg|
211
+ if arg =~ /\/.*\/.*/
212
+ self.require_file_to_exist(arg, job_data, controller)
213
+ end
214
+ }
215
+ end
168
216
 
169
- job_data.delete("@job")
170
- job_data["@keyname"] = params[:keyname] || "appscale"
217
+ if job_data["@appcfg_cookies"]
218
+ self.require_file_to_exist(job_data["@appcfg_cookies"], job_data, controller)
219
+ end
171
220
 
172
- job_data["@type"] = job_data["@type"].to_s
173
- type = job_data["@type"]
221
+ user_specified_engine = job_data["@engine"]
174
222
 
175
- if type == "upc" or type == "x10"
176
- job_data["@type"] = "mpi"
177
- type = "mpi"
223
+ # validate the engine here
224
+ engines = controller.get_supported_babel_engines(job_data)
225
+ if !engines.include?(user_specified_engine)
226
+ raise BadConfigurationException.new("The engine you specified, " +
227
+ "#{user_specified_engine}, is not a supported engine. Supported engines" +
228
+ " are: #{engines.join(', ')}")
229
+ end
178
230
  end
179
231
 
180
- # kdt jobs also run as mpi jobs, but need to pass along an executable
181
- # parameter to let mpiexec know to use python to exec it
182
- if type == "kdt"
183
- job_data["@type"] = "mpi"
184
- type = "mpi"
232
+ def self.get_job_data(params)
233
+ job_data = {}
234
+ params.each { |k, v|
235
+ key = "@#{k}"
236
+ job_data[key] = v
237
+ }
185
238
 
186
- job_data["@executable"] = "python"
187
- end
239
+ job_data.delete("@job")
240
+ job_data["@keyname"] = params[:keyname] || "appscale"
188
241
 
189
- if job_data["@nodes_to_use"].class == Hash
190
- job_data["@nodes_to_use"] = job_data["@nodes_to_use"].to_a.flatten
191
- end
242
+ job_data["@type"] = job_data["@type"].to_s
243
+ type = job_data["@type"]
192
244
 
193
- if !NO_OUTPUT_NEEDED.include?(type)
194
- if (job_data["@output"].nil? or job_data["@output"] == "")
195
- abort("Job output must be specified")
245
+ if !ALLOWED_JOB_TYPES.include?(type)
246
+ raise BadConfigurationException.new(JOB_TYPE_NOT_ALLOWED)
196
247
  end
197
248
 
198
- if job_data["@output"][0].chr != "/"
199
- abort("Job output must begin with a slash ('/')")
249
+ if type == "upc" or type == "x10"
250
+ job_data["@type"] = "mpi"
251
+ type = "mpi"
200
252
  end
201
- end
202
253
 
203
- return job_data
204
- end
254
+ # kdt jobs also run as mpi jobs, but need to pass along an executable
255
+ # parameter to let mpiexec know to use python to exec it
256
+ if type == "kdt"
257
+ job_data["@type"] = "mpi"
258
+ type = "mpi"
205
259
 
206
- def validate_storage_params(job_data)
207
- if !job_data["@storage"]
208
- job_data["@storage"] = "appdb"
209
- end
260
+ job_data["@executable"] = "python"
261
+ end
210
262
 
211
- storage = job_data["@storage"]
212
- if !ALLOWED_STORAGE_TYPES.include?(storage)
213
- abort("Supported storage types are #{ALLOWED_STORAGE_TYPES.join(', ')}" +
214
- " - we do not support #{storage}.")
215
- end
263
+ if job_data["@nodes_to_use"].class == Hash
264
+ job_data["@nodes_to_use"] = job_data["@nodes_to_use"].to_a.flatten
265
+ end
216
266
 
217
- # Our implementation for storing / retrieving via Google Storage
218
- # and Walrus uses
219
- # the same library as we do for S3 - so just tell it that it's S3
220
- if storage == "gstorage" or storage == "walrus"
221
- storage = "s3"
222
- job_data["@storage"] = "s3"
223
- end
267
+ if !NO_OUTPUT_NEEDED.include?(type)
268
+ if (job_data["@output"].nil? or job_data["@output"].empty?)
269
+ raise BadConfigurationException.new("Job output must be specified")
270
+ end
224
271
 
225
- if storage == "s3"
226
- ["EC2_ACCESS_KEY", "EC2_SECRET_KEY", "S3_URL"].each { |item|
227
- if job_data["@#{item}"]
228
- puts "Using specified #{item}"
229
- else
230
- if ENV[item]
231
- puts "Using #{item} from environment"
232
- job_data["@#{item}"] = ENV[item]
233
- else
234
- abort("When storing data to S3, #{item} must be specified or be in " +
235
- "your environment. Please do so and try again.")
236
- end
272
+ if job_data["@output"][0].chr != "/"
273
+ raise BadConfigurationException.new("Job output must begin with a slash ('/')")
237
274
  end
238
- }
275
+ end
276
+
277
+ return job_data
239
278
  end
240
279
 
241
- return job_data
242
- end
280
+ def self.validate_storage_params(job_data)
281
+ job_data["@storage"] ||= "appdb"
243
282
 
244
- # This method takes a file on the local user's computer and stores it remotely
245
- # via AppScale. It returns a hash map indicating whether or not the job
246
- # succeeded and if it failed, the reason for it.
247
- def get_input(job_data, ssh_args, shadow_ip, controller, file=File,
248
- shell=Kernel.method(:`))
249
- result = {:result => :success}
283
+ storage = job_data["@storage"]
284
+ if !ALLOWED_STORAGE_TYPES.include?(storage)
285
+ raise BadConfigurationException.new("Supported storage types are " +
286
+ "#{ALLOWED_STORAGE_TYPES.join(', ')} - #{storage} is not supported.")
287
+ end
250
288
 
251
- if !job_data["@local"]
252
- abort("You failed to specify a file to copy over via the :local flag.")
253
- end
289
+ # Our implementation for storing / retrieving via Google Storage
290
+ # and Walrus uses
291
+ # the same library as we do for S3 - so just tell it that it's S3
292
+ if storage == "gstorage" or storage == "walrus"
293
+ storage = "s3"
294
+ job_data["@storage"] = "s3"
295
+ end
254
296
 
255
- local_file = file.expand_path(job_data["@local"])
256
- if !file.exists?(local_file)
257
- reason = "the file you specified to copy, #{local_file}, doesn't exist." +
258
- " Please specify a file that exists and try again."
259
- return {:result => :failure, :reason => reason}
260
- end
297
+ if storage == "s3"
298
+ ["EC2_ACCESS_KEY", "EC2_SECRET_KEY", "S3_URL"].each { |item|
299
+ if job_data["@#{item}"]
300
+ Kernel.puts "Using specified #{item}"
301
+ else
302
+ if ENV[item]
303
+ Kernel.puts "Using #{item} from environment"
304
+ job_data["@#{item}"] = ENV[item]
305
+ else
306
+ raise BadConfigurationException.new("When storing data to S3, #{item} must be specified or be in " +
307
+ "your environment. Please do so and try again.")
308
+ end
309
+ end
310
+ }
311
+ end
261
312
 
262
- remote = "/tmp/neptune-input-#{rand(100000)}"
263
- scp_cmd = "scp -r #{ssh_args} #{local_file} root@#{shadow_ip}:#{remote}"
264
- puts scp_cmd
265
- shell.call(scp_cmd)
266
-
267
- job_data["@local"] = remote
268
- puts "job data = #{job_data.inspect}"
269
- response = controller.put_input(job_data)
270
- if response
271
- return {:result => :success}
272
- else
273
- # TODO - expand this to include the reason why it failed
274
- return {:result => :failure}
313
+ return job_data
275
314
  end
276
- end
277
315
 
278
- # This method waits for AppScale to finish compiling the user's code, indicated
279
- # by AppScale copying the finished code to a pre-determined location.
280
- def wait_for_compilation_to_finish(ssh_args, shadow_ip, compiled_location,
281
- shell=Kernel.method(:`))
282
- loop {
283
- ssh_command = "ssh #{ssh_args} root@#{shadow_ip} 'ls #{compiled_location}' 2>&1"
284
- puts ssh_command
285
- ssh_result = shell.call(ssh_command)
286
- puts "result was [#{ssh_result}]"
287
- if ssh_result =~ /No such file or directory/
288
- puts "Still waiting for code to be compiled..."
289
- else
290
- puts "compilation complete! Copying compiled code to #{copy_to}"
291
- return
292
- end
293
- sleep(5)
294
- }
295
- end
316
+ # This method takes a file on the local user's computer and stores it remotely
317
+ # via AppScale. It returns a hash map indicating whether or not the job
318
+ # succeeded and if it failed, the reason for it.
319
+ def self.get_input(job_data, ssh_args, shadow_ip, controller)
320
+ result = {:result => :success}
296
321
 
297
- # This method sends out a request to compile code, waits for it to finish, and
298
- # gets the standard out and error returned from the compilation. This method
299
- # returns a hash containing the standard out, error, and a result that indicates
300
- # whether or not the compilation was successful.
301
- def compile_code(job_data, ssh_args, shadow_ip, shell=Kernel.method(:`))
302
- compiled_location = controller.compile_code(job_data)
322
+ self.require_param("@local", job_data)
303
323
 
304
- copy_to = job_data["@copy_to"]
324
+ local_file = File.expand_path(job_data["@local"])
325
+ if !File.exists?(local_file)
326
+ reason = "the file you specified to copy, #{local_file}, doesn't exist." +
327
+ " Please specify a file that exists and try again."
328
+ return {:result => :failure, :reason => reason}
329
+ end
305
330
 
306
- wait_for_compilation_to_finish(ssh_args, shadow_ip, compiled_location)
331
+ remote = "/tmp/neptune-input-#{rand(100000)}"
332
+ scp_cmd = "scp -r #{ssh_args} #{local_file} root@#{shadow_ip}:#{remote}"
333
+ Kernel.puts scp_cmd
334
+ CommonFunctions.shell(scp_cmd)
307
335
 
308
- FileUtils.rm_rf(copy_to)
336
+ job_data["@local"] = remote
337
+ Kernel.puts "job data = #{job_data.inspect}"
338
+ response = controller.put_input(job_data)
339
+ if response
340
+ return {:result => :success}
341
+ else
342
+ # TODO - expand this to include the reason why it failed
343
+ return {:result => :failure}
344
+ end
345
+ end
309
346
 
310
- scp_command = "scp -r #{ssh_args} root@#{shadow_ip}:#{compiled_location} #{copy_to} 2>&1"
311
- puts scp_command
312
- shell.call(scp_command)
347
+ # This method waits for AppScale to finish compiling the user's code, indicated
348
+ # by AppScale copying the finished code to a pre-determined location.
349
+ def self.wait_for_compilation_to_finish(ssh_args, shadow_ip, compiled_location)
350
+ loop {
351
+ ssh_command = "ssh #{ssh_args} root@#{shadow_ip} 'ls #{compiled_location}' 2>&1"
352
+ Kernel.puts ssh_command
353
+ ssh_result = CommonFunctions.shell(ssh_command)
354
+ Kernel.puts "result was [#{ssh_result}]"
355
+ if ssh_result =~ /No such file or directory/
356
+ Kernel.puts "Still waiting for code to be compiled..."
357
+ else
358
+ Kernel.puts "compilation complete! Copying compiled code to #{copy_to}"
359
+ return
360
+ end
361
+ sleep(5)
362
+ }
363
+ end
313
364
 
314
- code = job_data["@code"]
315
- dirs = code.split(/\//)
316
- remote_dir = "/tmp/" + dirs[-1]
365
+ # This method sends out a request to compile code, waits for it to finish, and
366
+ # gets the standard out and error returned from the compilation. This method
367
+ # returns a hash containing the standard out, error, and a result that indicates
368
+ # whether or not the compilation was successful.
369
+ def self.compile_code(job_data, ssh_args, shadow_ip)
370
+ compiled_location = controller.compile_code(job_data)
371
+ copy_to = job_data["@copy_to"]
372
+ self.wait_for_compilation_to_finish(ssh_args, shadow_ip, compiled_location)
373
+
374
+ FileUtils.rm_rf(copy_to)
375
+
376
+ scp_command = "scp -r #{ssh_args} root@#{shadow_ip}:#{compiled_location} #{copy_to} 2>&1"
377
+ Kernel.puts scp_command
378
+ CommonFunctions.shell(scp_command)
379
+
380
+ code = job_data["@code"]
381
+ dirs = code.split(/\//)
382
+ remote_dir = "/tmp/" + dirs[-1]
383
+
384
+ [remote_dir, compiled_location].each { |remote_files|
385
+ ssh_command = "ssh #{ssh_args} root@#{shadow_ip} 'rm -rf #{remote_files}' 2>&1"
386
+ Kernel.puts ssh_command
387
+ CommonFunctions.shell(ssh_command)
388
+ }
317
389
 
318
- [remote_dir, compiled_location].each { |remote_files|
319
- ssh_command = "ssh #{ssh_args} root@#{shadow_ip} 'rm -rf #{remote_files}' 2>&1"
320
- puts ssh_command
321
- shell.call(ssh_command)
322
- }
390
+ return get_std_out_and_err(copy_to)
391
+ end
323
392
 
324
- return get_std_out_and_err(copy_to)
325
- end
393
+ # This method returns a hash containing the standard out and standard error
394
+ # from a completed job, as well as a result field that indicates whether or
395
+ # not the job completed successfully (success = no errors).
396
+ def self.get_std_out_and_err(location)
397
+ result = {}
326
398
 
327
- # This method returns a hash containing the standard out and standard error
328
- # from a completed job, as well as a result field that indicates whether or
329
- # not the job completed successfully (success = no errors).
330
- def get_std_out_and_err(location)
331
- result = {}
399
+ out = File.open("#{location}/compile_out") { |f| f.read.chomp! }
400
+ result[:out] = out
332
401
 
333
- out = File.open("#{location}/compile_out") { |f| f.read.chomp! }
334
- result[:out] = out
402
+ err = File.open("#{location}/compile_err") { |f| f.read.chomp! }
403
+ result[:err] = err
335
404
 
336
- err = File.open("#{location}/compile_err") { |f| f.read.chomp! }
337
- result[:err] = err
405
+ if result[:err]
406
+ result[:result] = :failure
407
+ else
408
+ result[:result] = :success
409
+ end
338
410
 
339
- if result[:err]
340
- result[:result] = :failure
341
- else
342
- result[:result] = :success
343
- end
411
+ return result
412
+ end
344
413
 
345
- return result
346
- end
414
+ def self.upload_app_for_cicero(job_data)
415
+ if !job_data["@app"]
416
+ Kernel.puts "No app specified, not uploading..."
417
+ return
418
+ end
347
419
 
348
- def upload_app_for_cicero(job_data)
349
- if !job_data["@app"]
350
- puts "No app specified, not uploading..."
351
- return
352
- end
420
+ app_location = File.expand_path(job_data["@app"])
421
+ if !File.exists?(app_location)
422
+ raise BadConfigurationException.new("The app you specified, #{app_location}, does not exist." +
423
+ "Please specify one that does and try again.")
424
+ end
353
425
 
354
- app_location = File.expand_path(job_data["@app"])
355
- if !File.exists?(app_location)
356
- abort("The app you specified, #{app_location}, does not exist." +
357
- "Please specify one that does and try again.")
358
- end
426
+ keyname = job_data["@keyname"] || "appscale"
427
+ if job_data["@appscale_tools"]
428
+ upload_app = File.expand_path(job_data["@appscale_tools"]) +
429
+ File::SEPARATOR + "bin" + File::SEPARATOR + "appscale-upload-app"
430
+ else
431
+ upload_app = "appscale-upload-app"
432
+ end
359
433
 
360
- keyname = job_data["@keyname"] || "appscale"
361
- if job_data["@appscale_tools"]
362
- upload_app = File.expand_path(job_data["@appscale_tools"]) +
363
- File::SEPARATOR + "bin" + File::SEPARATOR + "appscale-upload-app"
364
- else
365
- upload_app = "appscale-upload-app"
434
+ Kernel.puts "Uploading AppEngine app at #{app_location}"
435
+ upload_command = "#{upload_app} --file #{app_location} --test --keyname #{keyname}"
436
+ Kernel.puts upload_command
437
+ Kernel.puts `#{upload_command}`
366
438
  end
367
439
 
368
- puts "Uploading AppEngine app at #{app_location}"
369
- upload_command = "#{upload_app} --file #{app_location} --test --keyname #{keyname}"
370
- puts upload_command
371
- puts `#{upload_command}`
372
- end
440
+ # This method actually runs the Neptune job, given information about the job
441
+ # as well as information about the node to send the request to.
442
+ def self.run_job(job_data, ssh_args, shadow_ip, secret)
443
+ controller = AppControllerClient.new(shadow_ip, secret)
444
+
445
+ # TODO - right now the job is assumed to succeed in many cases
446
+ # need to investigate the various failure scenarios
447
+ result = { :result => :success }
448
+
449
+ case job_data["@type"]
450
+ when "input"
451
+ result = self.get_input(job_data, ssh_args, shadow_ip, controller)
452
+ when "output"
453
+ result[:output] = controller.get_output(job_data)
454
+ when "get-acl"
455
+ job_data["@type"] = "acl"
456
+ result[:acl] = controller.get_acl(job_data)
457
+ when "set-acl"
458
+ job_data["@type"] = "acl"
459
+ result[:acl] = controller.set_acl(job_data)
460
+ when "compile"
461
+ result = self.compile_code(job_data, ssh_args, shadow_ip)
462
+ when "cicero"
463
+ self.upload_app_for_cicero(job_data)
464
+ msg = controller.start_neptune_job(job_data)
465
+ result[:msg] = msg
466
+ result[:result] = :failure if result[:msg] !~ /job is now running\Z/
467
+ else
468
+ msg = controller.start_neptune_job(job_data)
469
+ result[:msg] = msg
470
+ result[:result] = :failure if result[:msg] !~ /job is now running\Z/
471
+ end
373
472
 
374
- # This method actually runs the Neptune job, given information about the job
375
- # as well as information about the node to send the request to.
376
- def run_job(job_data, ssh_args, shadow_ip, secret,
377
- controller=AppControllerClient, file=File)
378
- controller = controller.new(shadow_ip, secret)
379
-
380
- # TODO - right now the job is assumed to succeed in many cases
381
- # need to investigate the various failure scenarios
382
- result = { :result => :success }
383
-
384
- case job_data["@type"]
385
- when "input"
386
- result = get_input(job_data, ssh_args, shadow_ip, controller, file)
387
- when "output"
388
- result[:output] = controller.get_output(job_data)
389
- when "get-acl"
390
- job_data["@type"] = "acl"
391
- result[:acl] = controller.get_acl(job_data)
392
- when "set-acl"
393
- job_data["@type"] = "acl"
394
- result[:acl] = controller.set_acl(job_data)
395
- when "compile"
396
- result = compile_code(job_data, ssh_args, shadow_ip)
397
- when "cicero"
398
- upload_app_for_cicero(job_data)
399
- msg = controller.start_neptune_job(job_data)
400
- result[:msg] = msg
401
- result[:result] = :failure if result[:msg] !~ /job is now running\Z/
402
- else
403
- msg = controller.start_neptune_job(job_data)
404
- result[:msg] = msg
405
- result[:result] = :failure if result[:msg] !~ /job is now running\Z/
473
+ return result
406
474
  end
407
-
408
- return result
409
475
  end
410
476
 
411
- # This method is the heart of Neptune - here, we take
412
- # blocks of code that the user has written and convert them
413
- # into HPC job requests. At a high level, the user can
414
- # request to run a job, retrieve a job's output, or
415
- # modify the access policy (ACL) for the output of a
416
- # job. By default, job data is private, but a Neptune
417
- # job can be used to set it to public later (and
418
- # vice-versa).
477
+ # Make neptune() public so that babel() can call it
478
+ public
479
+
480
+ # This method is the heart of Neptune - here, we take blocks of code that the
481
+ # user has written and convert them into HPC job requests. At a high level,
482
+ # the user can request to run a job, retrieve a job's output, or modify the
483
+ # access policy (ACL) for the output of a job. By default, job data is private,
484
+ # but a Neptune job can be used to set it to public later (and vice-versa).
419
485
  def neptune(params)
420
- puts "Received a request to run a job."
421
- puts params[:type]
486
+ Kernel.puts "Received a request to run a job."
487
+ Kernel.puts params[:type]
422
488
 
423
- job_data = get_job_data(params)
424
- validate_storage_params(job_data)
425
- puts "job data = #{job_data.inspect}"
426
- do_preprocessing(job_data)
489
+ job_data = NeptuneHelper.get_job_data(params)
490
+ NeptuneHelper.validate_storage_params(job_data)
491
+ Kernel.puts "job data = #{job_data.inspect}"
427
492
  keyname = job_data["@keyname"]
428
493
 
429
494
  shadow_ip = CommonFunctions.get_from_yaml(keyname, :shadow)
@@ -431,5 +496,7 @@ def neptune(params)
431
496
  ssh_key = File.expand_path("~/.appscale/#{keyname}.key")
432
497
  ssh_args = "-i ~/.appscale/#{keyname}.key -o StrictHostkeyChecking=no "
433
498
 
434
- return run_job(job_data, ssh_args, shadow_ip, secret)
499
+ controller = AppControllerClient.new(shadow_ip, secret)
500
+ NeptuneHelper.do_preprocessing(job_data, controller)
501
+ return NeptuneHelper.run_job(job_data, ssh_args, shadow_ip, secret)
435
502
  end