osc-machete 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ # helper class to create job directories
2
+ class OSC::Machete::JobDir
3
+ def initialize(parent_directory)
4
+ @target = Pathname.new(parent_directory).cleanpath
5
+ end
6
+
7
+ # Returns a unique path for a job
8
+ #
9
+ # @return [String] A path of a unique job directory as string.
10
+ def new_jobdir
11
+ @target + unique_dir
12
+ end
13
+
14
+ #FIXME: BELOW METHODS SHOULD BE PRIVATE
15
+
16
+ # return true if the string is a job dir name
17
+ def jobdir_name?(name)
18
+ name[/^\d+$/]
19
+ end
20
+
21
+ # return true if Pathname is a job directory
22
+ # FIXME: this is not used anywhere; remove it?
23
+ def jobdir?(path)
24
+ jobdir_name?(path.basename.to_s) && path.directory?
25
+ end
26
+
27
+ # get a list of all job directories
28
+ # FIXME: this is not used anywhere; remove it?
29
+ def jobdirs
30
+ @target.exist? ? @target.children.select { |i| jobdir?(i) } : []
31
+ end
32
+
33
+
34
+ # get a list of directories in the target directory
35
+ # FIXME: this is not used anywhere; remove it?
36
+ def targetdirs
37
+ @target.exist? ? @target.children.select(&:directory?) : []
38
+ end
39
+
40
+ # find the next unique integer name for a job directory
41
+ def unique_dir
42
+ taken_ints = taken_paths.map { |path| path.basename.to_s.to_i }
43
+ (taken_ints.count > 0) ? (taken_ints.max + 1).to_s : 1.to_s
44
+ end
45
+
46
+ private
47
+
48
+ # paths that are unavailable for creating a new job directory
49
+ def taken_paths
50
+ if @target.exist?
51
+ @target.children.select { |path| jobdir_name?(path.basename.to_s) }
52
+ else
53
+ []
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,91 @@
1
+ require 'pathname'
2
+ require 'mustache'
3
+
4
+ # A util class with methods used with staging a simulation template directory.
5
+ # Use it by wrapping a file path (either string or Pathname object).
6
+ # For example, if I have a template directory at "/nfs/05/efranz/template"
7
+ # I can recursivly copy the template directory:
8
+ #
9
+ # target = "/nfs/05/efranz/simulations/1"
10
+ # simulation = Location.new("/nfs/05/efranz/template").copy_to(target)
11
+ #
12
+ # Then I can recursively render all the mustache templates in the copied directory,
13
+ # renaming each file from XXXX.mustache to XXXX:
14
+ #
15
+ # simulation.render(iterations: 20, geometry: "/nfs/05/efranz/geos/fan.stl")
16
+ #
17
+ class OSC::Machete::Location
18
+ # URIs, Paths, rendering, and copying
19
+ # this should be refactored into separate objects
20
+
21
+ # @param path either string, Pathname, or Machete::Location object
22
+ def initialize(path)
23
+ @path = Pathname.new(path.to_s).cleanpath
24
+ @template_ext = ".mustache"
25
+ end
26
+
27
+ # @return [String] The location path as String.
28
+ def to_s
29
+ @path.to_s
30
+ end
31
+
32
+ # Copies the data in a Location to a destination path using rsync.
33
+ #
34
+ # @param [String, Pathname] dest The target location path.
35
+ # @return [Location] The target location path wrapped by Location instance.
36
+ def copy_to(dest)
37
+ # @path has / auto-dropped, so we add it to make sure we copy everything
38
+ # in the old directory to the new
39
+ destloc = self.class.new(dest)
40
+ `rsync -r --exclude='.svn' --exclude='.git' --exclude='.gitignore' --filter=':- .gitignore' #{@path.to_s}/ #{destloc.to_s}`
41
+
42
+ # return target location so we can chain method
43
+ destloc
44
+ end
45
+
46
+ # **This should be a private method**
47
+ #
48
+ # Get a list of template files in this Location, where a template file is a
49
+ # file with the extension .mustache
50
+ #
51
+ # @return [Array<String>] list of template files in directory (recursively searched)
52
+ def template_files
53
+ if @path.directory?
54
+ Dir.glob(File.join(@path, "**/*#{@template_ext}"))
55
+ else
56
+ @path.to_s.end_with? @template_ext ? [@path.to_s] : []
57
+ end
58
+ end
59
+
60
+ # Render each mustache template and rename the file, removing the extension
61
+ # that indicates it is a template file i.e. `.mustache`.
62
+ #
63
+ # @param [Hash] params the "context" or "hash" for use when rendering mustache templates
64
+ # @param [Hash] options to modify rendering behavior
65
+ # @option options [Boolean] :replace (true) if true will delete the template file after the rendered file is created
66
+ # @return [self] returns self for optional chaining
67
+ def render(params, options = {})
68
+ # custom_delimiters = options['delimeters'] || nil
69
+ replace_template_files = options[:replace].nil? ? true : options[:replace]
70
+
71
+ renderer = Mustache.new
72
+
73
+ template_files.each do |template|
74
+ rendered_file = template.chomp(@template_ext)
75
+
76
+ rendered_string = nil
77
+ File.open(template, 'r') do |f|
78
+ rendered_string = renderer.render(f.read, params)
79
+ end
80
+ # boo...
81
+ # rendered_string = renderer.render_file(template, params)
82
+
83
+ File.open(rendered_file, 'w') { |f| f.write(rendered_string) }
84
+
85
+ FileUtils.rm template if replace_template_files
86
+ end
87
+
88
+ # return self so this can be at the end of a chained method
89
+ self
90
+ end
91
+ end
@@ -0,0 +1,32 @@
1
+ # Class that maintains the User and additional methods for the process.
2
+ # Helper methods provided use the Process module underneath.
3
+ #
4
+ class OSC::Machete::Process
5
+
6
+ def initialize
7
+ @user = OSC::Machete::User.from_uid(Process.uid)
8
+ end
9
+
10
+ # The system name of the process user
11
+ def username
12
+ @user.name
13
+ end
14
+
15
+ # use gid not egid
16
+ def groupname
17
+ Etc.getgrgid(Process.gid).name
18
+ end
19
+
20
+ # has the group membership changed since this process started?
21
+ def group_membership_changed?
22
+ Process.groups.uniq.sort != @user.groups
23
+ end
24
+
25
+ # The home directory path of the process user.
26
+ #
27
+ # @return [String] The directory path.
28
+ def home
29
+ @user.home
30
+ end
31
+
32
+ end
@@ -0,0 +1,190 @@
1
+ # Value object representing job status independent of underlying resource manager.
2
+ #
3
+ # All of the possible Torque statuses are not represented here. Its the
4
+ # responsibility of the Torque adapter (TorqueHelper) to create the appropriate
5
+ # Status object to represent the Torque status.
6
+ #
7
+ # The possible values are: Undetermined, Not Submitted, Passed, Failed, Held, Queued, Running, Suspended
8
+ #
9
+ class OSC::Machete::Status
10
+ include Comparable
11
+
12
+ attr_reader :char
13
+
14
+ # C Job is passed (completed successfully)
15
+ # F Job is failed (completed with errors)
16
+ # H Job is held.
17
+ # Q Job is queued, eligible to run or routed.
18
+ # R Job is running.
19
+ #
20
+ # U Status is unavailable (null status object)
21
+ VALUES = [["U", "undetermined"], [nil, "not_submitted"], ["C", "passed"], ["F", "failed"],
22
+ ["H", "held"], ["Q", "queued"], ["R", "running"], ["S", "suspended"]]
23
+ private_constant :VALUES
24
+
25
+ # A hashed version of the values array.
26
+ VALUES_HASH = Hash[VALUES]
27
+ private_constant :VALUES_HASH
28
+
29
+ # An array of status char values by precedence.
30
+ #
31
+ # @example
32
+ # OSC::Machete::Status::PRECEDENCE #=> ["U", nil, "C", "F", "H", "Q", "R", "S"]
33
+ PRECEDENCE = VALUES.map(&:first)
34
+ private_constant :PRECEDENCE
35
+
36
+ # Get an array of all the possible Status values
37
+ #
38
+ # @return [Array<Status>] - all possible Status values
39
+ def self.values
40
+ VALUES.map{ |v| OSC::Machete::Status.new(v.first) }
41
+ end
42
+
43
+ # Get an array of all the possible active Status values
44
+ #
45
+ # @return [Array<Status>] - all possible active Status values
46
+ def self.active_values
47
+ values.select(&:active?)
48
+ end
49
+
50
+ # Get an array of all the possible completed Status values
51
+ #
52
+ # @return [Array<Status>] - all possible completed Status values
53
+ def self.completed_values
54
+ values.select(&:completed?)
55
+ end
56
+
57
+
58
+ # TODO: these methods are previously declared so we can document them easily
59
+ # if there is a better way to document class methods we'll do that
60
+
61
+ # @return [Status]
62
+ def self.undetermined() end
63
+ # @return [Status]
64
+ def self.not_submitted() end
65
+ # @return [Status]
66
+ def self.passed() end
67
+ # @return [Status]
68
+ def self.failed() end
69
+ # @return [Status]
70
+ def self.running() end
71
+ # @return [Status]
72
+ def self.queued() end
73
+ # @return [Status]
74
+ def self.held() end
75
+ # @return [Status]
76
+ def self.suspended() end
77
+
78
+ class << self
79
+ VALUES_HASH.each do |char, name|
80
+ define_method(name) do
81
+ OSC::Machete::Status.new(char)
82
+ end
83
+ end
84
+ end
85
+
86
+ # @!method undetermined?
87
+ # the Status value Null object
88
+ # @return [Boolean] true if undetermined
89
+ # @!method not_submitted?
90
+ # @return [Boolean] true if not_submitted
91
+ # @!method failed?
92
+ # @return [Boolean] true if failed
93
+ # @!method passed?
94
+ # @return [Boolean] true if passed
95
+ # @!method held?
96
+ # @return [Boolean] true if held
97
+ # @!method queued?
98
+ # @return [Boolean] true if queued
99
+ # @!method running?
100
+ # @return [Boolean] true if running
101
+ # @!method suspended?
102
+ # @return [Boolean] true if suspended
103
+ VALUES_HASH.each do |char, name|
104
+ define_method("#{name}?") do
105
+ self == OSC::Machete::Status.new(char)
106
+ end
107
+ end
108
+
109
+ def initialize(char)
110
+ # char could be a status object or a string
111
+ @char = (char.respond_to?(:char) ? char.char : char).to_s.upcase
112
+ @char = nil if @char.empty?
113
+
114
+ # if invalid status value char, default to undetermined
115
+ @char = self.class.undetermined.char unless VALUES_HASH.has_key?(@char)
116
+ end
117
+
118
+ # @return [Boolean] true if submitted
119
+ def submitted?
120
+ ! (not_submitted? || undetermined?)
121
+ end
122
+
123
+ # @return [Boolean] true if in active state (running, queued, held, suspended)
124
+ def active?
125
+ running? || queued? || held? || suspended?
126
+ end
127
+
128
+ # @return [Boolean] true if in completed state (passed or failed)
129
+ def completed?
130
+ passed? || failed?
131
+ end
132
+
133
+ # Return a readable string of the status
134
+ #
135
+ # @example Running
136
+ # OSC::Machete::Status.running.to_s #=> "Running"
137
+ #
138
+ # @return [String] The status value as a formatted string
139
+ def to_s
140
+ # FIXME: ActiveSupport replace with .humanize and simpler datastructure
141
+ VALUES_HASH[@char].split("_").map(&:capitalize).join(" ")
142
+ end
143
+
144
+ # Return the a StatusValue object based on the highest precedence of the two objects.
145
+ #
146
+ # @example One job is running and a dependent job is queued.
147
+ # OSC::Machete::Status.running + OSC::Machete::Status.queued #=> Running
148
+ #
149
+ # Return [OSC::Machete::Status] The max status by precedence
150
+ def +(other)
151
+ [self, other].max
152
+ end
153
+
154
+ # The comparison operator for sorting values.
155
+ #
156
+ # @return [Integer] Comparison value based on precedence
157
+ def <=>(other)
158
+ precedence <=> other.precedence
159
+ end
160
+
161
+ # Boolean evaluation of Status object equality.
162
+ #
163
+ # @return [Boolean] True if the values are the same
164
+ def eql?(other)
165
+ # compare Status to Status OR "C" to Status
166
+ (other.respond_to?(:char) ? other.char : other) == char
167
+ end
168
+
169
+ # Boolean evaluation of Status object equality.
170
+ #
171
+ # @return [Boolean] True if the values are the same
172
+ def ==(other)
173
+ self.eql?(other)
174
+ end
175
+
176
+ # Return a hash based on the char value of the object.
177
+ #
178
+ # @return [Fixnum] A hash value of the status char
179
+ def hash
180
+ @char.hash
181
+ end
182
+
183
+ # Return the ordinal position of the status in the precidence list
184
+ #
185
+ # @return [Integer] The order of precedence for the object
186
+ def precedence
187
+ # Hashes enumerate their values in the order that the corresponding keys were inserted
188
+ PRECEDENCE.index(@char)
189
+ end
190
+ end
@@ -0,0 +1,190 @@
1
+ require 'pbs'
2
+
3
+ # == Helper object: ruby interface to torque shell commands
4
+ # in the same vein as stdlib's Shell which
5
+ # "implements an idiomatic Ruby interface for common UNIX shell commands"
6
+ # also helps to have these separate so we can use a mock shell for unit tests
7
+ #
8
+ # == FIXME: This contains no state whatsoever. It should probably be changed into a module.
9
+ class OSC::Machete::TorqueHelper
10
+
11
+ # Alias to initialize a new object.
12
+ def self.default
13
+ self::new()
14
+ end
15
+
16
+ # Returns an OSC::Machete::Status ValueObject for a char
17
+ #
18
+ # @param [String] char The Torque status char
19
+ #
20
+ # @example Completed
21
+ # status_for_char("C") #=> OSC::Machete::Status.completed
22
+ # @example Queued
23
+ # status_for_char("W") #=> OSC::Machete::Status.queued
24
+ #
25
+ # @return [OSC::Machete::Status] The status corresponding to the char
26
+ def status_for_char(char)
27
+ case char
28
+ when "C", nil
29
+ OSC::Machete::Status.passed
30
+ when "Q", "T", "W" # T W happen before job starts
31
+ OSC::Machete::Status.queued
32
+ when "H"
33
+ OSC::Machete::Status.held
34
+ else
35
+ # all other statuses considerd "running" state
36
+ # including S, E, etc.
37
+ # see http://docs.adaptivecomputing.com/torque/4-1-3/Content/topics/commands/qstat.htm
38
+ OSC::Machete::Status.running
39
+ end
40
+ end
41
+
42
+ #*TODO:*
43
+ # consider using cocaine gem
44
+ # consider using Shellwords and other tools
45
+
46
+ # usage: <tt>qsub("/path/to/script")</tt> or
47
+ # <tt>qsub("/path/to/script", depends_on: { afterany: ["1234.oak-batch.osc.edu"] })</tt>
48
+ #
49
+ # Where depends_on is a hash with key being dependency type and array containing the
50
+ # arguments. See documentation on dependency_list in qsub man pages for details.
51
+ #
52
+ # Bills against the project specified by the primary group of the user.
53
+ def qsub(script, host: nil, depends_on: {}, account_string: nil)
54
+ # if the script is set to run on Oakley in PBS headers
55
+ # this is to obviate current torque filter defect in which
56
+ # a script with PBS header set to specify oak-batch ends
57
+ # isn't properly handled and the job gets limited to 4GB
58
+ pbs_job = get_pbs_job( host.nil? ? get_pbs_conn(script: script) : get_pbs_conn(host: host) )
59
+
60
+ headers = { depend: qsub_dependencies_header(depends_on) }
61
+ headers.clear if headers[:depend].empty?
62
+
63
+ # currently we set the billable project to the name of the primary group
64
+ # this will probably be both SUPERCOMPUTER CENTER SPECIFIC and must change
65
+ # when we want to enable our users at OSC to specify which billable project
66
+ # to bill against
67
+ if account_string
68
+ headers[PBS::ATTR[:A]] = account_string
69
+ elsif account_string_valid_project?(default_account_string)
70
+ headers[PBS::ATTR[:A]] = default_account_string
71
+ end
72
+
73
+ pbs_job.submit(file: script, headers: headers, qsub: true).id
74
+ end
75
+
76
+ # convert dependencies hash to a PBS header string
77
+ def qsub_dependencies_header(depends_on = {})
78
+ depends_on.map { |x|
79
+ x.first.to_s + ":" + Array(x.last).join(":") unless Array(x.last).empty?
80
+ }.compact.join(",")
81
+ end
82
+
83
+ # return the account string required for accounting purposes
84
+ # having this in a separate method is useful for monkeypatching in short term
85
+ # or overridding with a subclass you pass into OSC::Machete::Job
86
+ #
87
+ # FIXME: this may belong on OSC::Machete::User; but it is OSC specific...
88
+ #
89
+ # @return [String] the project name that job submission should be billed against
90
+ def default_account_string
91
+ OSC::Machete::Process.new.groupname
92
+ end
93
+
94
+ def account_string_valid_project?(account_string)
95
+ /^P./ =~ account_string
96
+ end
97
+
98
+ # Performs a qstat request on a single job.
99
+ #
100
+ # **FIXME: this might not belong here!**
101
+ #
102
+ # @param [String] pbsid The pbsid of the job to inspect.
103
+ #
104
+ # @return [Status] The job state
105
+ def qstat(pbsid, host: nil)
106
+
107
+ # Create a PBS::Job object based on the pbsid or the optional host param
108
+ pbs_conn = host.nil? ? get_pbs_conn(pbsid: pbsid.to_s) : get_pbs_conn(host: host)
109
+ pbs_job = get_pbs_job(pbs_conn, pbsid)
110
+
111
+ job_status = pbs_job.status
112
+ # Get the status char value from the job.
113
+ status_for_char job_status[:attribs][:job_state][0]
114
+
115
+ rescue PBS::UnkjobidError => err
116
+ OSC::Machete::Status.passed
117
+ end
118
+
119
+ # Perform a qdel command on a single job.
120
+ #
121
+ # @param [String] pbsid The pbsid of the job to be deleted.
122
+ #
123
+ # @return [nil]
124
+ def qdel(pbsid, host: nil)
125
+
126
+ pbs_conn = host.nil? ? get_pbs_conn(pbsid: pbsid.to_s) : get_pbs_conn(host: host)
127
+ pbs_job = get_pbs_job(pbs_conn, pbsid.to_s)
128
+
129
+ pbs_job.delete
130
+
131
+ rescue PBS::UnkjobidError => err
132
+ # Common use case where trying to delete a job that is no longer in the system.
133
+ end
134
+
135
+ private
136
+
137
+ # Factory to return a PBS::Job object
138
+ def get_pbs_job(conn, pbsid=nil)
139
+ pbsid.nil? ? PBS::Job.new(conn: conn) : PBS::Job.new(conn: conn, id: pbsid.to_s)
140
+ end
141
+
142
+ # Returns a PBS connection object
143
+ #
144
+ # @option [:script] A PBS script with headers as string
145
+ # @option [:pbsid] A valid pbsid as string
146
+ #
147
+ # @return [PBS::Conn] A connection option for the PBS host (Default: Oakley)
148
+ def get_pbs_conn(options={})
149
+ if options[:script]
150
+ PBS::Conn.batch(host_from_script_pbs_header(options[:script]))
151
+ elsif options[:pbsid]
152
+ PBS::Conn.batch(host_from_pbsid(options[:pbsid]))
153
+ elsif options[:host]
154
+ PBS::Conn.batch(options[:host])
155
+ else
156
+ PBS::Conn.batch("oakley")
157
+ end
158
+ end
159
+
160
+ # return the name of the host to use based on the pbs header
161
+ # TODO: Think of a more efficient way to do this.
162
+ def host_from_script_pbs_header(script)
163
+ if (File.open(script) { |f| f.read =~ /#PBS -q @oak-batch/ })
164
+ "oakley"
165
+ elsif (File.open(script) { |f| f.read =~ /#PBS -q @opt-batch/ })
166
+ "glenn"
167
+ elsif (File.open(script) { |f| f.read =~ /#PBS -q @ruby-batch/ })
168
+ "ruby"
169
+ elsif (File.open(script) { |f| f.read =~ /#PBS -q @quick-batch/ })
170
+ "quick"
171
+ else
172
+ "oakley" # DEFAULT
173
+ end
174
+ end
175
+
176
+ # Return the PBS host string based on a full pbsid string
177
+ def host_from_pbsid(pbsid)
178
+ if (pbsid =~ /oak-batch/ )
179
+ "oakley"
180
+ elsif (pbsid =~ /opt-batch/ )
181
+ "glenn"
182
+ elsif (pbsid.to_s =~ /^\d+$/ )
183
+ "ruby"
184
+ elsif (pbsid =~ /quick/ )
185
+ "quick"
186
+ else
187
+ "oakley" # DEFAULT
188
+ end
189
+ end
190
+ end