osc-machete 1.1.3

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.
@@ -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