bookie_accounting 1.2.3 → 2.0.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.
- checksums.yaml +4 -4
- data/Gemfile +4 -3
- data/README.md +4 -24
- data/Rakefile +9 -116
- data/bin/bookie-data +48 -7
- data/bin/bookie-send +6 -14
- data/bookie_accounting.gemspec +4 -3
- data/lib/bookie/database/group.rb +33 -0
- data/lib/bookie/database/job.rb +201 -0
- data/lib/bookie/database/job_summary.rb +268 -0
- data/lib/bookie/database/lock.rb +36 -0
- data/lib/bookie/database/system.rb +166 -0
- data/lib/bookie/database/system_type.rb +80 -0
- data/lib/bookie/database/user.rb +54 -0
- data/lib/bookie/database.rb +7 -805
- data/lib/bookie/extensions.rb +23 -44
- data/lib/bookie/formatter.rb +8 -4
- data/lib/bookie/sender.rb +12 -14
- data/lib/bookie/version.rb +1 -1
- data/snapshot/test_config.json +2 -2
- data/spec/config_spec.rb +2 -2
- data/spec/database/group_spec.rb +36 -0
- data/spec/database/job_spec.rb +308 -0
- data/spec/database/job_summary_spec.rb +302 -0
- data/spec/database/lock_spec.rb +41 -0
- data/spec/database/migration_spec.rb +44 -0
- data/spec/database/system_spec.rb +232 -0
- data/spec/database/system_type_spec.rb +68 -0
- data/spec/database/user_spec.rb +69 -0
- data/spec/formatter_spec.rb +44 -37
- data/spec/{comma_dump_formatter_spec.rb → formatters/comma_dump_spec.rb} +16 -30
- data/spec/formatters/spreadsheet_spec.rb +98 -0
- data/spec/{stdout_formatter_spec.rb → formatters/stdout_spec.rb} +15 -29
- data/spec/sender_spec.rb +92 -66
- data/spec/{standalone_sender_spec.rb → senders/standalone_spec.rb} +10 -9
- data/spec/{torque_cluster_sender_spec.rb → senders/torque_cluster_spec.rb} +9 -13
- data/spec/spec_helper.rb +111 -57
- data/todo.txt +13 -0
- metadata +38 -23
- data/rpm/activesupport.erb +0 -151
- data/rpm/bundle.erb +0 -71
- data/rpm/default.erb +0 -147
- data/rpm/mysql2.erb +0 -149
- data/rpm/pacct.erb +0 -147
- data/rpm/rspec-core.erb +0 -149
- data/rpm/sqlite3.erb +0 -147
- data/spec/database_spec.rb +0 -1078
- data/spec/spreadsheet_formatter_spec.rb +0 -114
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d726372976ed3f86e19d29e2a391d233c972d5d
|
|
4
|
+
data.tar.gz: 99ea6555c0e94a6be3d04c5168a4a45dde020d90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e123b58f99c5529014a135948ccfbbcde6c2090a2436d5b9076b49bcb839fce55d1f37636713301c7eb783b3aeffbc7444d5f4b424a757ab92c3dd30464c13b5
|
|
7
|
+
data.tar.gz: 3fad9d7b47452444935ea70b821229c066789a04f8b929d2cf33cae6e10b8573c7ff87ebe2d581e33f5bd7082b6b4efabc5faefd85825d353feb72c17eb10308
|
data/Gemfile
CHANGED
|
@@ -3,8 +3,9 @@ source 'https://rubygems.org'
|
|
|
3
3
|
gemspec
|
|
4
4
|
|
|
5
5
|
gem 'bundler', :group => :development
|
|
6
|
-
gem '
|
|
7
|
-
gem '
|
|
8
|
-
gem '
|
|
6
|
+
gem 'byebug', :group => :test
|
|
7
|
+
gem 'mocha', :group => :test
|
|
8
|
+
gem 'rspec', :group => :test
|
|
9
|
+
gem 'simplecov', :group => :test
|
|
9
10
|
gem 'sqlite3', :group => :development
|
|
10
11
|
|
data/README.md
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
# Bookie
|
|
2
|
-
|
|
3
|
-
TODO: Write a gem description
|
|
2
|
+
A simple system to consolidate and analyze process accounting records
|
|
4
3
|
|
|
5
4
|
## Installation
|
|
5
|
+
See the [Installation](https://github.com/blm768/bookie/wiki/Installation) page on the wiki.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
gem 'bookie'
|
|
10
|
-
|
|
11
|
-
And then execute:
|
|
12
|
-
|
|
13
|
-
$ bundle
|
|
14
|
-
|
|
15
|
-
Or install it yourself as:
|
|
16
|
-
|
|
17
|
-
$ gem install bookie
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
TODO: Write usage instructions here
|
|
22
|
-
|
|
23
|
-
## Contributing
|
|
7
|
+
##Usage
|
|
8
|
+
The wiki contains a [command reference](https://github.com/blm768/bookie/wiki/Command-line-utilities
|
|
24
9
|
|
|
25
|
-
1. Fork it
|
|
26
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
27
|
-
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
|
29
|
-
5. Create new Pull Request
|
data/Rakefile
CHANGED
|
@@ -3,130 +3,23 @@ require 'bundler'
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rspec/core/rake_task"
|
|
5
5
|
|
|
6
|
-
require '
|
|
6
|
+
require 'find'
|
|
7
7
|
|
|
8
8
|
task :default => :spec
|
|
9
9
|
|
|
10
10
|
desc "Run specs"
|
|
11
11
|
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
12
|
-
task.rspec_opts =%w{--color --format progress}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
system("rdoc rdoc lib")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
#Returns whether the given file is older than its dependencies (or doesn't even exist)
|
|
21
|
-
def older(file, *dep_files)
|
|
22
|
-
dep_files.each do |dep_file|
|
|
23
|
-
return true if !File.exists?(file) || File.mtime(file) < File.mtime(dep_file)
|
|
24
|
-
end
|
|
25
|
-
false
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
desc "Build RPM and dependencies (designed for use on CentOS)"
|
|
29
|
-
task :rpm_deps do
|
|
30
|
-
gem_filenames = {
|
|
31
|
-
"pacct" => "pacct-0.8.4-universal-linux.gem"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
lockfile = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
|
|
35
|
-
|
|
36
|
-
home_dir = ENV['HOME']
|
|
37
|
-
spec_dir = File.join(home_dir, 'rpmbuild/SPECS')
|
|
38
|
-
src_dir = File.join(home_dir, 'rpmbuild/SOURCES')
|
|
39
|
-
rpm_dir = File.join(home_dir, 'rpmbuild/RPMS')
|
|
40
|
-
|
|
41
|
-
FileUtils.mkdir_p(spec_dir)
|
|
42
|
-
FileUtils.mkdir_p(src_dir)
|
|
43
|
-
|
|
44
|
-
lockfile.specs.each do |spec|
|
|
45
|
-
puts "#{spec.name} (#{spec.version})"
|
|
46
|
-
|
|
47
|
-
spec_filename = File.join(spec_dir, "rubygem-#{spec.name}.spec")
|
|
48
|
-
|
|
49
|
-
gem_filename = gem_filenames[spec.name] || "#{spec.name}-#{spec.version}.gem"
|
|
50
|
-
|
|
51
|
-
system("gem fetch #{spec.name} -v #{spec.version}") unless File.exists?(gem_filename)
|
|
52
|
-
|
|
53
|
-
template = "rpm/#{spec.name}.erb"
|
|
54
|
-
template = "rpm/default.erb" unless File.exists?(template)
|
|
55
|
-
|
|
56
|
-
system("gem2rpm #{gem_filename} -t #{template} > #{spec_filename}") if older(spec_filename, gem_filename, template)
|
|
57
|
-
|
|
58
|
-
src = File.join(src_dir, gem_filename)
|
|
59
|
-
FileUtils.cp(gem_filename, src_dir) if older(src, gem_filename)
|
|
60
|
-
|
|
61
|
-
rpm_glob = File.join(rpm_dir, "*/rubygem-#{spec.name}-#{spec.version}-*.rpm")
|
|
62
|
-
rpm_glob_results = Dir.glob(rpm_glob)
|
|
63
|
-
if rpm_glob_results.length > 1
|
|
64
|
-
puts "Multiple RPMs exist for #{spec.name} (#{spec.version}):"
|
|
65
|
-
rpm_glob_results.each{ |r| puts " #{r}" }
|
|
66
|
-
puts "Unable to resolve dependency due to ambiguous target"
|
|
67
|
-
exit 1
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if rpm_glob_results.length == 0 || older(rpm_glob_results[0], spec_filename)
|
|
71
|
-
msg = `rpmbuild -ba #{spec_filename}`
|
|
72
|
-
unless $?.success?
|
|
73
|
-
puts msg
|
|
74
|
-
exit 1
|
|
75
|
-
end
|
|
12
|
+
task.rspec_opts =%w{--color --order rand --format progress}
|
|
13
|
+
paths = []
|
|
14
|
+
Find.find('spec') do |path|
|
|
15
|
+
if File.directory?(path)
|
|
16
|
+
paths << "#{path}/*_spec.rb"
|
|
76
17
|
end
|
|
77
18
|
end
|
|
19
|
+
task.pattern = paths.join(" ")
|
|
78
20
|
end
|
|
79
21
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
gem_filenames = {
|
|
83
|
-
"pacct" => "pacct-0.8.4-universal-linux.gem"
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
lockfile = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
|
|
87
|
-
|
|
88
|
-
home_dir = ENV['HOME']
|
|
89
|
-
spec_dir = File.join(home_dir, 'rpmbuild/SPECS')
|
|
90
|
-
src_dir = File.join(home_dir, 'rpmbuild/SOURCES')
|
|
91
|
-
rpm_dir = File.join(home_dir, 'rpmbuild/RPMS')
|
|
92
|
-
|
|
93
|
-
FileUtils.mkdir_p(spec_dir)
|
|
94
|
-
FileUtils.mkdir_p(src_dir)
|
|
95
|
-
|
|
96
|
-
spec_filename = File.join(spec_dir, "bookie_accounting.spec")
|
|
97
|
-
|
|
98
|
-
template = ERB.new(File.read("rpm/bundle.erb"))
|
|
99
|
-
|
|
100
|
-
specs = lockfile.specs
|
|
101
|
-
|
|
102
|
-
#To do: ensure that the rubygems version matches the local version?
|
|
103
|
-
bookie_spec = Gem::Specification.load("bookie_accounting.gemspec")
|
|
104
|
-
|
|
105
|
-
sources = []
|
|
106
|
-
|
|
107
|
-
pwd = Dir.pwd
|
|
108
|
-
Dir.chdir(src_dir)
|
|
109
|
-
specs.each do |spec|
|
|
110
|
-
puts "#{spec.name} (#{spec.version})"
|
|
111
|
-
|
|
112
|
-
#bookie_spec = spec if spec.name == "bookie_accounting"
|
|
113
|
-
|
|
114
|
-
gem_filename = gem_filenames[spec.name] || "#{spec.name}-#{spec.version}.gem"
|
|
115
|
-
|
|
116
|
-
system("gem fetch #{spec.name} -v #{spec.version}") unless File.exists?(gem_filename)
|
|
117
|
-
|
|
118
|
-
sources << gem_filename
|
|
119
|
-
end
|
|
120
|
-
Dir.chdir(pwd)
|
|
121
|
-
|
|
122
|
-
File.open(spec_filename, "w") do |f|
|
|
123
|
-
f.write(template.result(binding))
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
msg = `rpmbuild -ba #{spec_filename}`
|
|
127
|
-
unless $?.success?
|
|
128
|
-
puts msg
|
|
129
|
-
exit 1
|
|
130
|
-
end
|
|
22
|
+
task :rdoc do
|
|
23
|
+
system("rdoc rdoc lib")
|
|
131
24
|
end
|
|
132
25
|
|
data/bin/bookie-data
CHANGED
|
@@ -11,8 +11,12 @@ jobs = Bookie::Database::Job
|
|
|
11
11
|
summaries = Bookie::Database::JobSummary
|
|
12
12
|
systems = Bookie::Database::System
|
|
13
13
|
|
|
14
|
+
DEFAULT_DETAILS_PER_PAGE = 20
|
|
15
|
+
|
|
14
16
|
config_filename = ENV['BOOKIE_CONFIG'] || '/etc/bookie/config.json'
|
|
15
17
|
include_details = false
|
|
18
|
+
details_per_page = nil
|
|
19
|
+
details_page = 1
|
|
16
20
|
|
|
17
21
|
output_type = :stdout
|
|
18
22
|
filename = nil
|
|
@@ -21,11 +25,11 @@ time_range = nil
|
|
|
21
25
|
|
|
22
26
|
#Process arguments
|
|
23
27
|
|
|
24
|
-
#The first run only gets the configuration filename
|
|
28
|
+
#The first run only gets the configuration filename.
|
|
25
29
|
ARGV.each_with_index do |value, i|
|
|
26
30
|
if value == '-c' || value == '--config'
|
|
27
31
|
v = ARGV[i + 1]
|
|
28
|
-
#If the argument is missing, ignore
|
|
32
|
+
#If the argument is missing or invalid, ignore the problem; OptionParser will catch it later.
|
|
29
33
|
config_filename = v if v
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -43,6 +47,25 @@ opts = OptionParser.new do |opts|
|
|
|
43
47
|
opts.on('-d', '--details', "include full details") do
|
|
44
48
|
include_details = true
|
|
45
49
|
end
|
|
50
|
+
|
|
51
|
+
opts.on('-p', '--page PAGE', Integer, "show only the given page of details") do |page_num|
|
|
52
|
+
if page_num < 1
|
|
53
|
+
STDERR.puts "invalid page number: #{page_num}"
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
56
|
+
include_details = true
|
|
57
|
+
details_per_page ||= DEFAULT_DETAILS_PER_PAGE
|
|
58
|
+
details_page = page_num
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.on('-l', '--limit COUNT', Integer, "limit the number of jobs per page") do |count|
|
|
62
|
+
if count < 1
|
|
63
|
+
STDERR.PUTS "invalid page length: #{count}"
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
include_details = true
|
|
67
|
+
details_per_page = count
|
|
68
|
+
end
|
|
46
69
|
|
|
47
70
|
opts.on('-u', '--user NAME', "filter by username") do |name|
|
|
48
71
|
jobs = jobs.by_user_name(name)
|
|
@@ -68,7 +91,7 @@ opts = OptionParser.new do |opts|
|
|
|
68
91
|
opts.on('-t', '--type TYPE', "filter by system type") do |type|
|
|
69
92
|
t = Bookie::Database::SystemType.find_by_name(type)
|
|
70
93
|
unless t
|
|
71
|
-
STDERR.puts "
|
|
94
|
+
STDERR.puts "unknown system type '#{type}'"
|
|
72
95
|
exit 1
|
|
73
96
|
end
|
|
74
97
|
jobs = jobs.by_system_type(t)
|
|
@@ -89,11 +112,12 @@ opts = OptionParser.new do |opts|
|
|
|
89
112
|
when /\.csv$/
|
|
90
113
|
output_type = :comma_dump
|
|
91
114
|
else
|
|
92
|
-
|
|
115
|
+
STDERR.puts "unrecognized output file extension"
|
|
93
116
|
exit 1
|
|
94
117
|
end
|
|
95
118
|
end
|
|
96
119
|
end
|
|
120
|
+
|
|
97
121
|
begin
|
|
98
122
|
opts.parse!(ARGV)
|
|
99
123
|
rescue OptionParser::ParseError => e
|
|
@@ -104,7 +128,24 @@ end
|
|
|
104
128
|
|
|
105
129
|
formatter = Bookie::Formatter.new(output_type, filename)
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
formatter.print_summary(jobs, summaries, systems, time_range)
|
|
132
|
+
|
|
133
|
+
if include_details
|
|
134
|
+
#TODO: include separator or blank line.
|
|
135
|
+
|
|
136
|
+
jobs = jobs.by_time_range(time_range) if time_range
|
|
137
|
+
jobs = jobs.order(:start_time)
|
|
138
|
+
|
|
139
|
+
if details_per_page
|
|
140
|
+
page_start = details_per_page * (details_page - 1)
|
|
141
|
+
if jobs.count <= page_start
|
|
142
|
+
puts "No jobs on page #{details_page}"
|
|
143
|
+
else
|
|
144
|
+
jobs = jobs.offset(page_start).limit(details_per_page)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
formatter.print_jobs(jobs.all_with_associations)
|
|
148
|
+
end
|
|
149
|
+
|
|
110
150
|
formatter.flush
|
|
151
|
+
|
data/bin/bookie-send
CHANGED
|
@@ -2,17 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
|
|
5
|
-
#For development:
|
|
6
|
-
#$LOAD_PATH << 'lib'
|
|
7
|
-
|
|
8
|
-
#To consider: restore for production?
|
|
9
|
-
=begin
|
|
10
|
-
unless Process.uid == 0
|
|
11
|
-
$stderr.puts "This command must be run as root."
|
|
12
|
-
exit 1
|
|
13
|
-
end
|
|
14
|
-
=end
|
|
15
|
-
|
|
16
5
|
require 'bookie/sender'
|
|
17
6
|
|
|
18
7
|
config_file = ENV['BOOKIE_CONFIG'] || '/etc/bookie/config.json'
|
|
@@ -60,7 +49,10 @@ config = Bookie::Config.new(config_file)
|
|
|
60
49
|
config.connect
|
|
61
50
|
|
|
62
51
|
filename = ARGV[0]
|
|
63
|
-
|
|
52
|
+
unless filename || will_create || will_decommission
|
|
53
|
+
STDERR.puts "No operation specified"
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
64
56
|
|
|
65
57
|
if filename
|
|
66
58
|
sender = Bookie::Sender.new(config)
|
|
@@ -75,7 +67,7 @@ if will_decommission
|
|
|
75
67
|
system_hostname ||= config.hostname
|
|
76
68
|
system_end_time ||= Time.now
|
|
77
69
|
Bookie::Database::Lock[:systems].synchronize do
|
|
78
|
-
system = Bookie::Database::System.active_systems.
|
|
70
|
+
system = Bookie::Database::System.active_systems.where(:name => system_hostname).first
|
|
79
71
|
if system
|
|
80
72
|
puts "Note: make sure that all of this system's jobs have been recorded in the database before decommissioning it."
|
|
81
73
|
STDOUT.write "Decommission this system? "
|
|
@@ -102,7 +94,7 @@ end
|
|
|
102
94
|
if will_create
|
|
103
95
|
system_start_time ||= Time.now
|
|
104
96
|
Bookie::Database::Lock[:systems].synchronize do
|
|
105
|
-
system = Bookie::Database::System.active_systems.
|
|
97
|
+
system = Bookie::Database::System.active_systems.where(:name => config.hostname).first
|
|
106
98
|
if system
|
|
107
99
|
stderr.puts "An active system is already in the database with hostname '#{config.hostname}'."
|
|
108
100
|
exit 1
|
data/bookie_accounting.gemspec
CHANGED
|
@@ -5,8 +5,8 @@ Gem::Specification.new do |gem|
|
|
|
5
5
|
gem.authors = ["Ben Merritt"]
|
|
6
6
|
gem.email = ["blm768@gmail.com"]
|
|
7
7
|
gem.license = "MIT"
|
|
8
|
-
gem.description = %q{A simple system to
|
|
9
|
-
gem.summary = %q{A simple system to
|
|
8
|
+
gem.description = %q{A simple system to consolidate and analyze process accounting records}
|
|
9
|
+
gem.summary = %q{A simple system to consolidate and analyze process accounting records}
|
|
10
10
|
gem.homepage = "https://github.com/blm768/bookie/"
|
|
11
11
|
|
|
12
12
|
gem.files = `git ls-files`.split($\)
|
|
@@ -20,11 +20,12 @@ Gem::Specification.new do |gem|
|
|
|
20
20
|
gem.add_dependency('json')
|
|
21
21
|
#We need this because Bundler has no concept of optional dependencies
|
|
22
22
|
#and complains about using non-dependency gems.
|
|
23
|
-
#
|
|
23
|
+
#TODO: figure out how to remove
|
|
24
24
|
gem.add_dependency('mysql2')
|
|
25
25
|
gem.add_dependency('pacct')
|
|
26
26
|
#Introduces the old ActiveRecord mass assignment security methods
|
|
27
27
|
#(until I update the database code for the new methods)
|
|
28
|
+
#TODO: get rid of this.
|
|
28
29
|
gem.add_dependency('protected_attributes')
|
|
29
30
|
gem.add_dependency('spreadsheet')
|
|
30
31
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
|
|
3
|
+
require 'bookie/database/lock.rb'
|
|
4
|
+
|
|
5
|
+
module Bookie
|
|
6
|
+
module Database
|
|
7
|
+
##
|
|
8
|
+
#A group of users
|
|
9
|
+
class Group < ActiveRecord::Base
|
|
10
|
+
has_many :users
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
#Finds a group by name, creating it if it doesn't exist
|
|
14
|
+
#
|
|
15
|
+
#If <tt>known_groups</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
|
|
16
|
+
#
|
|
17
|
+
#This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
|
|
18
|
+
def self.find_or_create!(name, known_groups = nil)
|
|
19
|
+
group = known_groups[name] if known_groups
|
|
20
|
+
unless group
|
|
21
|
+
Lock[:groups].synchronize do
|
|
22
|
+
group = find_by_name(name)
|
|
23
|
+
group ||= create!(:name => name)
|
|
24
|
+
end
|
|
25
|
+
known_groups[name] = group if known_groups
|
|
26
|
+
end
|
|
27
|
+
group
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
validates_presence_of :name
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
|
|
3
|
+
require 'bookie/database/lock.rb'
|
|
4
|
+
require 'bookie/database/user.rb'
|
|
5
|
+
require 'bookie/database/system.rb'
|
|
6
|
+
|
|
7
|
+
module Bookie
|
|
8
|
+
module Database
|
|
9
|
+
##
|
|
10
|
+
#A reported job
|
|
11
|
+
#
|
|
12
|
+
#The various filter methods can be chained to produce more complex queries.
|
|
13
|
+
#
|
|
14
|
+
#===Examples
|
|
15
|
+
# Bookie::Database::Job.by_user_name('root').by_system_name('localhost').find_each do |job|
|
|
16
|
+
# puts job.inspect
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class Job < ActiveRecord::Base
|
|
20
|
+
belongs_to :user
|
|
21
|
+
belongs_to :system
|
|
22
|
+
has_one :group, :through => :user
|
|
23
|
+
has_one :system_type, :through => :system
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
#The time at which the job ended
|
|
27
|
+
def end_time
|
|
28
|
+
return start_time + wall_time
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def end_time=(time)
|
|
32
|
+
self.wall_time = (time - start_time)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#To consider: disable #end_time= ?
|
|
36
|
+
|
|
37
|
+
def self.by_user(user)
|
|
38
|
+
where('jobs.user_id = ?', user.id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
#Filters by user name
|
|
43
|
+
def self.by_user_name(user_name)
|
|
44
|
+
joins(:user).where('users.name = ?', user_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.by_system(system)
|
|
48
|
+
where('jobs.system_id = ?', system.id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
#Filters by system name
|
|
53
|
+
def self.by_system_name(system_name)
|
|
54
|
+
joins(:system).where('systems.name = ?', system_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
#Filters by group name
|
|
59
|
+
def self.by_group_name(group_name)
|
|
60
|
+
group = Group.find_by_name(group_name)
|
|
61
|
+
return joins(:user).where('users.group_id = ?', group.id) if group
|
|
62
|
+
self.none
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
#Filters by system type
|
|
67
|
+
def self.by_system_type(system_type)
|
|
68
|
+
joins(:system).where('systems.system_type_id = ?', system_type.id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
#Filters by command name
|
|
73
|
+
def self.by_command_name(c_name)
|
|
74
|
+
where('jobs.command_name = ?', c_name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
#Finds all jobs that were running at some point in a given time range
|
|
79
|
+
def self.by_time_range(time_range)
|
|
80
|
+
if time_range.empty?
|
|
81
|
+
self.none
|
|
82
|
+
else
|
|
83
|
+
time_range = time_range.exclusive
|
|
84
|
+
where('jobs.end_time > ? AND jobs.start_time < ?', time_range.begin, time_range.end)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
#Similar to #by_time_range, but only includes jobs that are completely contained within the
|
|
90
|
+
#time range
|
|
91
|
+
def self.within_time_range(time_range)
|
|
92
|
+
if time_range.empty?
|
|
93
|
+
self.none
|
|
94
|
+
else
|
|
95
|
+
time_range = time_range.exclusive
|
|
96
|
+
#The second "<=" operator _is_ intentional.
|
|
97
|
+
#If the job's end_time is one second past the last value in the range, it
|
|
98
|
+
#is still considered to be contained within time_range because it did not
|
|
99
|
+
#run outside time_range; it only _stopped_ outside it.
|
|
100
|
+
where('? <= jobs.start_time AND jobs.end_time <= ?', time_range.begin, time_range.end)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
##
|
|
105
|
+
#Finds all jobs that overlap the edges of the given time range
|
|
106
|
+
def self.overlapping_edges(time_range)
|
|
107
|
+
if time_range.empty?
|
|
108
|
+
self.none
|
|
109
|
+
else
|
|
110
|
+
time_range = time_range.exclusive
|
|
111
|
+
query_str = ['begin', 'end'].map{ |edge| "(jobs.start_time < :#{edge} AND jobs.end_time > :#{edge})" }.join(" OR ")
|
|
112
|
+
where(query_str, {:begin => time_range.begin, :end => time_range.end})
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
#Produces a summary of the jobs in the given time interval
|
|
118
|
+
#
|
|
119
|
+
#Returns a hash with the following fields:
|
|
120
|
+
#- <tt>:num_jobs</tt>: the number of jobs in the interval
|
|
121
|
+
#- <tt>:successful</tt>: the number of jobs that have completed successfully
|
|
122
|
+
#- <tt>:cpu_time</tt>: the total CPU time used
|
|
123
|
+
#- <tt>:memory_time</tt>: the sum of memory * wall_time for all jobs in the interval
|
|
124
|
+
#
|
|
125
|
+
#This method should probably not be chained with other queries that filter by start/end time.
|
|
126
|
+
#It also doesn't work with the limit() method.
|
|
127
|
+
#
|
|
128
|
+
#The time_range parameter is always treated as if time_range.exclude_end? is true.
|
|
129
|
+
def self.summary(time_range = nil)
|
|
130
|
+
jobs = self
|
|
131
|
+
|
|
132
|
+
num_jobs = 0
|
|
133
|
+
successful = 0
|
|
134
|
+
cpu_time = 0.0
|
|
135
|
+
memory_time = 0
|
|
136
|
+
|
|
137
|
+
if time_range
|
|
138
|
+
unless time_range.empty?
|
|
139
|
+
time_range = time_range.exclusive
|
|
140
|
+
|
|
141
|
+
#Any jobs that are completely within the time range can
|
|
142
|
+
#be summarized as-is.
|
|
143
|
+
jobs_within = jobs.within_time_range(time_range)
|
|
144
|
+
#TODO: optimize into one query?
|
|
145
|
+
num_jobs += jobs_within.count
|
|
146
|
+
successful += jobs_within.where(:exit_code => 0).count
|
|
147
|
+
cpu_time += jobs_within.sum(:cpu_time)
|
|
148
|
+
memory_time += jobs_within.sum('jobs.memory * jobs.wall_time')
|
|
149
|
+
|
|
150
|
+
#Any jobs that overlap an edge of the time range
|
|
151
|
+
#must be clipped.
|
|
152
|
+
jobs_overlapped = jobs.overlapping_edges(time_range)
|
|
153
|
+
jobs_overlapped.find_each do |job|
|
|
154
|
+
start_time = [job.start_time, time_range.begin].max
|
|
155
|
+
end_time = [job.end_time, time_range.end].min
|
|
156
|
+
clipped_wall_time = end_time.to_i - start_time.to_i
|
|
157
|
+
if job.wall_time != 0
|
|
158
|
+
cpu_time += Float(job.cpu_time * clipped_wall_time) / job.wall_time
|
|
159
|
+
memory_time += job.memory * clipped_wall_time
|
|
160
|
+
end
|
|
161
|
+
num_jobs += 1
|
|
162
|
+
successful += 1 if job.exit_code == 0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
#There's no time_range constraint; just summarize everything.
|
|
167
|
+
num_jobs = jobs.count
|
|
168
|
+
successful = jobs.where(:exit_code => 0).count
|
|
169
|
+
cpu_time = jobs.sum(:cpu_time)
|
|
170
|
+
memory_time = jobs.sum('jobs.memory * jobs.wall_time')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
:num_jobs => num_jobs,
|
|
175
|
+
:successful => successful,
|
|
176
|
+
:cpu_time => cpu_time.round,
|
|
177
|
+
:memory_time => memory_time,
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
before_save do
|
|
182
|
+
write_attribute(:end_time, end_time)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
before_update do
|
|
186
|
+
write_attribute(:end_time, end_time)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
validates_presence_of :user, :system, :cpu_time,
|
|
190
|
+
:start_time, :wall_time, :memory, :exit_code
|
|
191
|
+
|
|
192
|
+
validates_each :command_name do |record, attr, value|
|
|
193
|
+
record.errors.add(attr, 'must not be nil') if value == nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
validates_each :cpu_time, :wall_time, :memory do |record, attr, value|
|
|
197
|
+
record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|