bookie_accounting 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. data/.gitignore +19 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +22 -0
  4. data/README.md +29 -0
  5. data/Rakefile +15 -0
  6. data/bin/bookie-create-tables +52 -0
  7. data/bin/bookie-data +102 -0
  8. data/bin/bookie-send +110 -0
  9. data/bookie_accounting.gemspec +28 -0
  10. data/lib/bookie.rb +11 -0
  11. data/lib/bookie/config.rb +101 -0
  12. data/lib/bookie/database.rb +656 -0
  13. data/lib/bookie/formatter.rb +149 -0
  14. data/lib/bookie/formatters/comma_dump.rb +24 -0
  15. data/lib/bookie/formatters/spreadsheet.rb +45 -0
  16. data/lib/bookie/formatters/stdout.rb +32 -0
  17. data/lib/bookie/sender.rb +108 -0
  18. data/lib/bookie/senders/standalone.rb +37 -0
  19. data/lib/bookie/senders/torque_cluster.rb +166 -0
  20. data/lib/bookie/version.rb +4 -0
  21. data/snapshot/config.json +12 -0
  22. data/snapshot/default.json +11 -0
  23. data/snapshot/pacct +0 -0
  24. data/snapshot/pacct_large +0 -0
  25. data/snapshot/pacct_test_config.json +14 -0
  26. data/snapshot/test_config.json +13 -0
  27. data/snapshot/torque +3 -0
  28. data/snapshot/torque_invalid_lines +5 -0
  29. data/snapshot/torque_invalid_lines_2 +4 -0
  30. data/snapshot/torque_invalid_lines_3 +3 -0
  31. data/snapshot/torque_large +100 -0
  32. data/spec/comma_dump_formatter_spec.rb +56 -0
  33. data/spec/config_spec.rb +55 -0
  34. data/spec/database_spec.rb +625 -0
  35. data/spec/formatter_spec.rb +93 -0
  36. data/spec/sender_spec.rb +104 -0
  37. data/spec/spec_helper.rb +121 -0
  38. data/spec/spreadsheet_formatter_spec.rb +112 -0
  39. data/spec/standalone_sender_spec.rb +40 -0
  40. data/spec/stdout_formatter_spec.rb +66 -0
  41. data/spec/torque_cluster_sender_spec.rb +111 -0
  42. metadata +227 -0
@@ -0,0 +1,656 @@
1
+ require 'bookie/config'
2
+
3
+ require 'active_record'
4
+
5
+ module Bookie
6
+ #Contains ActiveRecord structures for the central database
7
+ #
8
+ #For a list of fields in the various models, see {Database Tables}[link:rdoc/database_rdoc.html]
9
+ module Database
10
+
11
+ ##
12
+ #Represents a lock on a table
13
+ #
14
+ #Based on http://kseebaldt.blogspot.com/2007/11/synchronizing-using-active-record.html
15
+ #
16
+ #This should probably not be called within a transaction block; the lock might not be released
17
+ #until the outer transaction completes, and even if it is released before then, there might be
18
+ #concurrency issues.
19
+ class Lock < ActiveRecord::Base
20
+ ##
21
+ #Acquires the lock, runs the given block, and releases the lock when finished
22
+ def synchronize
23
+ transaction do
24
+ #Lock this record to be inaccessible to others until this transaction is completed.
25
+ self.class.lock.find(id)
26
+ yield
27
+ end
28
+ end
29
+
30
+ @locks = {}
31
+
32
+ ##
33
+ #Returns a lock by name
34
+ def self.[](name)
35
+ @locks[name.to_sym] ||= find_by_name(name.to_s) or raise "Unable to find lock '#{name}'"
36
+ end
37
+
38
+ validates_presence_of :name
39
+ end
40
+
41
+ ##
42
+ #A reported job
43
+ #
44
+ #The various filter methods can be chained to produce more complex queries.
45
+ #
46
+ #===Examples
47
+ # Bookie::Database::Job.by_user_name('root').by_system_name('localhost').find_each do |job|
48
+ # puts job.inspect
49
+ # end
50
+ #
51
+ class Job < ActiveRecord::Base
52
+ belongs_to :user
53
+ belongs_to :system
54
+
55
+ ##
56
+ #The time at which the job ended
57
+ def end_time
58
+ return start_time + wall_time
59
+ end
60
+
61
+ ##
62
+ #Filters by user name
63
+ def self.by_user_name(user_name)
64
+ joins(:user).where('users.name = ?', user_name)
65
+ end
66
+
67
+ ##
68
+ #Filters by system name
69
+ def self.by_system_name(system_name)
70
+ joins(:system).where('systems.name = ?', system_name)
71
+ end
72
+
73
+ ##
74
+ #Filters by group name
75
+ def self.by_group_name(group_name)
76
+ group = Group.find_by_name(group_name)
77
+ return joins(:user).where('group_id = ?', group.id) if group
78
+ limit(0)
79
+ end
80
+
81
+ ##
82
+ #Filters by system type
83
+ def self.by_system_type(system_type)
84
+ joins(:system).where('system_type_id = ?', system_type.id)
85
+ end
86
+
87
+ ##
88
+ #Filters by command name
89
+ def self.by_command_name(c_name)
90
+ where('command_name = ?', c_name)
91
+ end
92
+
93
+ ##
94
+ #Filters by a range of start times
95
+ def self.by_start_time_range(start_min, start_max)
96
+ where('? <= start_time AND start_time < ?', start_min, start_max)
97
+ end
98
+
99
+ ##
100
+ #Filters by a range of end times
101
+ def self.by_end_time_range(end_min, end_max)
102
+ where('? <= end_time AND end_time < ?', end_min, end_max)
103
+ end
104
+
105
+ ##
106
+ #Finds all jobs whose running intervals overlap the given time range
107
+ def self.by_time_range_inclusive(min_time, max_time)
108
+ raise ArgumentError.new('Max time must be greater than or equal to min time') if max_time < min_time
109
+ where('start_time < ? AND end_time > ?', max_time, min_time)
110
+ end
111
+
112
+ ##
113
+ #Produces a summary of the jobs in the given time interval
114
+ #
115
+ #Returns a hash with the following fields:
116
+ #- <tt>:jobs</tt>: an array of all jobs in the interval
117
+ #- <tt>:wall_time</tt>: the sum of all the jobs' wall times
118
+ #- <tt>:cpu_time</tt>: the total CPU time used
119
+ #- <tt>:memory_time</tt>: the sum of memory * wall_time for all jobs in the interval
120
+ #- <tt>:successful</tt>: the proportion of jobs that completed successfully
121
+ #
122
+ #This method should probably not be used with other queries that filter by start/end time.
123
+ def self.summary(min_time = nil, max_time = nil)
124
+ jobs = self
125
+ if min_time
126
+ raise ArgumentError.new('Max time must be specified with min time') unless max_time
127
+ jobs = jobs.by_time_range_inclusive(min_time, max_time)
128
+ end
129
+ jobs = jobs.where('cpu_time > 0').all_with_relations
130
+ wall_time = 0
131
+ cpu_time = 0
132
+ successful_jobs = 0
133
+ memory_time = 0
134
+ #To consider: job.end_time should be <= Time.now, but it might be good to check for that.
135
+ #Maybe in a database consistency checker tool?
136
+ #What if the system clock is off?
137
+ #Also consider a check for system start times.
138
+ jobs.each do |job|
139
+ job_start_time = job.start_time
140
+ job_end_time = job.end_time
141
+ if min_time
142
+ job_start_time = [job_start_time, min_time].max
143
+ job_end_time = [job_end_time, max_time].min
144
+ end
145
+ clipped_wall_time = job_end_time.to_i - job_start_time.to_i
146
+ wall_time += clipped_wall_time
147
+ if job.wall_time != 0
148
+ cpu_time += job.cpu_time * clipped_wall_time / job.wall_time
149
+ #To consider: what should I do about jobs that only report a max memory value?
150
+ memory_time += job.memory * clipped_wall_time
151
+ end
152
+ successful_jobs += 1 if job.exit_code == 0
153
+ end
154
+
155
+ return {
156
+ :jobs => jobs,
157
+ #To consider: is this field even useful? It's really in job-seconds, not just seconds.
158
+ #What about one in just seconds (that considers gaps in activity)?
159
+ :wall_time => wall_time,
160
+ :cpu_time => cpu_time,
161
+ :memory_time => memory_time,
162
+ :successful => if jobs.length == 0 then 0.0 else Float(successful_jobs) / jobs.length end,
163
+ }
164
+ end
165
+
166
+ ##
167
+ #Returns an array of all jobs, pre-loading relations to reduce the need for extra queries
168
+ #
169
+ #Relations are not cached between calls.
170
+ def self.all_with_relations
171
+ jobs = all
172
+ transaction do
173
+ users = {}
174
+ groups = {}
175
+ systems = {}
176
+ system_types = {}
177
+ jobs.each do |job|
178
+ system = systems[job.system_id]
179
+ if system
180
+ job.system = system
181
+ else
182
+ system = job.system
183
+ systems[system.id] = system
184
+ end
185
+ system_type = system_types[system.system_type_id]
186
+ if system_type
187
+ system.system_type = system_type
188
+ else
189
+ system_type = system.system_type
190
+ system_types[system_type.id] = system_type
191
+ end
192
+ user = users[job.user_id]
193
+ if user
194
+ job.user = user
195
+ else
196
+ user = job.user
197
+ users[user.id] = user
198
+ end
199
+ group = groups[user.group_id]
200
+ if group
201
+ user.group = group
202
+ else
203
+ group = user.group
204
+ groups[group.id] = group
205
+ end
206
+ end
207
+ end
208
+
209
+ jobs
210
+ end
211
+
212
+ before_save do
213
+ write_attribute(:end_time, end_time)
214
+ end
215
+
216
+ before_update do
217
+ write_attribute(:end_time, end_time)
218
+ end
219
+
220
+ validates_presence_of :user, :system, :cpu_time,
221
+ :start_time, :wall_time, :memory, :exit_code
222
+
223
+ validates_each :cpu_time, :wall_time, :memory do |record, attr, value|
224
+ record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
225
+ end
226
+
227
+ validates_each :start_time do |record, attr, value|
228
+ value = value.to_time if value.respond_to?(:to_time)
229
+ record.errors.add(attr, 'must be a time object') unless value.is_a?(Time)
230
+ end
231
+
232
+ end
233
+
234
+ ##
235
+ #A group
236
+ class Group < ActiveRecord::Base
237
+ has_many :users
238
+
239
+ ##
240
+ #Finds a group by name, creating it if it doesn't exist
241
+ #
242
+ #If <tt>known_groups</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
243
+ #
244
+ #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
245
+ def self.find_or_create!(name, known_groups = nil)
246
+ group = known_groups[name] if known_groups
247
+ unless group
248
+ Lock[:groups].synchronize do
249
+ group = find_by_name(name)
250
+ group ||= create!(:name => name)
251
+ end
252
+ known_groups[name] = group if known_groups
253
+ end
254
+ group
255
+ end
256
+
257
+ validates_presence_of :name
258
+ end
259
+
260
+ #ActiveRecord structure for a user
261
+ class User < ActiveRecord::Base
262
+ belongs_to :group
263
+
264
+ ##
265
+ #Finds a user by name and group, creating it if it doesn't exist
266
+ #
267
+ #If <tt>known_users</tt> is provided, it will be used as a cache to reduce the number of database lookups needed.
268
+ #
269
+ #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
270
+ def self.find_or_create!(name, group, known_users = nil)
271
+ #Determine if the user/group pair must be added to/retrieved from the database.
272
+ user = known_users[[name, group]] if known_users
273
+ unless user
274
+ Lock[:users].synchronize do
275
+ #Does the user already exist?
276
+ user = Bookie::Database::User.find_by_name_and_group_id(name, group.id)
277
+ user ||= Bookie::Database::User.create!(
278
+ :name => name,
279
+ :group => group)
280
+ end
281
+ known_users[[name, group]] = user if known_users
282
+ end
283
+ user
284
+ end
285
+
286
+ validates_presence_of :group, :name
287
+ end
288
+
289
+ ##
290
+ #A system on the network
291
+ class System < ActiveRecord::Base
292
+ ##
293
+ #Raised when a system's specifications are different from those of the active system in the database
294
+ SystemConflictError = Class.new(RuntimeError)
295
+
296
+ has_many :jobs
297
+ belongs_to :system_type
298
+
299
+ ##
300
+ #Finds all systems that are active (i.e. all systems with NULL values for end_time)
301
+ def self.active_systems
302
+ where('end_time IS NULL')
303
+ end
304
+
305
+ ##
306
+ #Filters by name
307
+ def self.by_name(name)
308
+ where('name = ?', name)
309
+ end
310
+
311
+ ##
312
+ #Filters by system type
313
+ def self.by_system_type(sys_type)
314
+ where('system_type_id = ?', sys_type.id)
315
+ end
316
+
317
+ ##
318
+ #Finds the active system for a given hostname
319
+ #
320
+ #<tt>values</tt> should contain a list of fields, including the name, in the format that would normally be passed to System.create!.
321
+ #
322
+ #This method also checks that this system's specifications are the same as those in the database and raises an error if they are different.
323
+ #
324
+ #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
325
+ def self.find_active(values)
326
+ system = nil
327
+ name = values[:name]
328
+ Lock[:systems].synchronize do
329
+ system = active_systems.find_by_name(name)
330
+ if system
331
+ [:cores, :memory, :system_type].each do |key|
332
+ #To consider: this also compares the names, which is unnecessary.
333
+ unless system.send(key) == values[key]
334
+ raise SystemConflictError.new("The specifications on record for '#{name}' do not match this system's specifications.
335
+ Please make sure that all previous systems with this hostname have been marked as decommissioned.")
336
+ end
337
+ end
338
+ else
339
+ raise "There is no active system with hostname '#{values[:name]}' in the database."
340
+ end
341
+ end
342
+ system
343
+ end
344
+
345
+ ##
346
+ #Produces a summary of all the systems for the given time interval
347
+ #
348
+ #Returns a hash with the following fields:
349
+ #- <tt>:avail_cpu_time</tt>: the total CPU time available for the interval
350
+ #- <tt>:avail_memory_time</tt>: the total amount of memory-time available (in kilobyte-seconds)
351
+ #- <tt>:avail_memory_avg</tt>: the average amount of memory available (in kilobytes)
352
+ def self.summary(min_time = nil, max_time = nil)
353
+ current_time = Time.now
354
+ #Sums that are actually returned
355
+ avail_cpu_time = 0
356
+ avail_memory_time = 0
357
+ #Find all the systems within the time range.
358
+ systems = System
359
+ if min_time
360
+ raise ArgumentError.new('Max time must be specified with min time') unless max_time
361
+ raise ArgumentError.new('Max time must be greater than or equal to min time') if max_time < min_time
362
+ #To consider: optimize as union of queries?
363
+ systems = systems.where(
364
+ 'start_time < ? AND (end_time IS NULL OR end_time > ?)',
365
+ max_time,
366
+ min_time)
367
+ end
368
+
369
+ systems.all.each do |system|
370
+ system_start_time = system.start_time
371
+ system_end_time = system.end_time
372
+ #Is there a time range constraint?
373
+ if min_time
374
+ system_start_time = [system_start_time, min_time].max
375
+ system_end_time = [system_end_time, max_time].min if system.end_time
376
+ system_end_time ||= max_time
377
+ else
378
+ system_end_time ||= current_time
379
+ end
380
+ system_wall_time = system_end_time.to_i - system_start_time.to_i
381
+ avail_cpu_time += system.cores * system_wall_time
382
+ avail_memory_time += system.memory * system_wall_time
383
+ end
384
+
385
+ wall_time_range = 0
386
+ if min_time
387
+ wall_time_range = max_time - min_time
388
+ else
389
+ first_started_system = systems.order(:start_time).first
390
+ if first_started_system
391
+ #Is there a system still active?
392
+ last_ended_system = systems.where('end_time IS NULL').first
393
+ if last_ended_system
394
+ wall_time_range = current_time.to_i - first_started_system.start_time.to_i
395
+ else
396
+ #No; find the system that was brought down last.
397
+ last_ended_system = systems.order('end_time DESC').first
398
+ wall_time_range = last_ended_system.end_time.to_i - first_started_system.start_time.to_i
399
+ end
400
+ end
401
+ end
402
+
403
+ {
404
+ :avail_cpu_time => avail_cpu_time,
405
+ :avail_memory_time => avail_memory_time,
406
+ :avail_memory_avg => if wall_time_range == 0 then 0.0 else Float(avail_memory_time) / wall_time_range end,
407
+ }
408
+ end
409
+
410
+ ##
411
+ #Decommissions the given system, setting its end time to <tt>end_time</tt>
412
+ #
413
+ #This should be called any time a system is brought down or its specifications are changed.
414
+ def decommission(end_time)
415
+ self.end_time = end_time
416
+ self.save!
417
+ end
418
+
419
+ validates_presence_of :name, :cores, :memory, :system_type, :start_time
420
+
421
+ validates_each :cores, :memory do |record, attr, value|
422
+ record.errors.add(attr, 'must be a non-negative integer') unless value && value >= 0
423
+ end
424
+
425
+ validates_each :start_time do |record, attr, value|
426
+ value = value.to_time if value.respond_to?(:to_time)
427
+ record.errors.add(attr, 'must be a time object') unless value.is_a?(Time)
428
+ end
429
+
430
+ validates_each :end_time do |record, attr, value|
431
+ record.errors.add(attr, 'must be at or after start time') if value && value < record.start_time
432
+ end
433
+ end
434
+
435
+ ##
436
+ #A hash mapping memory stat type names to their database codes
437
+ #
438
+ #- <tt>:unknown => 0</tt>
439
+ #- <tt>:avg => 1</tt>
440
+ #- <tt>:max => 2</tt>
441
+ #
442
+ MEMORY_STAT_TYPE = {:unknown => 0, :avg => 1, :max => 2}
443
+
444
+ ##
445
+ #The inverse of MEMORY_STAT_TYPE
446
+ MEMORY_STAT_TYPE_INVERSE = MEMORY_STAT_TYPE.invert
447
+
448
+ #A system type
449
+ class SystemType < ActiveRecord::Base
450
+ has_many :systems
451
+
452
+ validates_presence_of :name, :memory_stat_type
453
+
454
+ ##
455
+ #Finds a system type by name and memory stat type, creating it if it doesn't exist
456
+ #
457
+ #It is an error to attempt to create two types with the same name but different memory stat types.
458
+ #
459
+ #This uses Lock#synchronize internally, so it probably should not be called within a transaction block.
460
+ def self.find_or_create!(name, memory_stat_type)
461
+ sys_type = nil
462
+ Lock[:system_types].synchronize do
463
+ sys_type = SystemType.find_by_name(name)
464
+ if sys_type
465
+ unless sys_type.memory_stat_type == memory_stat_type
466
+ type_code = MEMORY_STAT_TYPE[memory_stat_type]
467
+ if type_code == nil
468
+ raise "Unrecognized memory stat type '#{memory_stat_type}'"
469
+ else
470
+ raise "The recorded memory stat type for system type '#{name}' does not match the required type of #{type_code}"
471
+ end
472
+ end
473
+ else
474
+ sys_type = create!(
475
+ :name => name,
476
+ :memory_stat_type => memory_stat_type
477
+ )
478
+ end
479
+ end
480
+ sys_type
481
+ end
482
+
483
+ ##
484
+ #Returns the memory stat type as a symbol
485
+ #
486
+ #See Bookie::Database::MEMORY_STAT_TYPE for possible values.
487
+ #
488
+ #Based on http://www.kensodev.com/2012/05/08/the-simplest-enum-you-will-ever-find-for-your-activerecord-models/
489
+ def memory_stat_type
490
+ type_code = read_attribute(:memory_stat_type)
491
+ raise 'Memory stat type must not be nil' if type_code == nil
492
+ type = MEMORY_STAT_TYPE_INVERSE[type_code]
493
+ raise "Unrecognized memory stat type code #{type_code}" unless type
494
+ type
495
+ end
496
+
497
+ ##
498
+ #Sets the memory stat type
499
+ #
500
+ #<tt>type</tt> should be a symbol.
501
+ def memory_stat_type=(type)
502
+ raise 'Memory stat type must not be nil' if type == nil
503
+ type_code = MEMORY_STAT_TYPE[type]
504
+ raise "Unrecognized memory stat type '#{type}'" unless type_code
505
+ write_attribute(:memory_stat_type, type_code)
506
+ end
507
+ end
508
+
509
+ ##
510
+ #Database migrations
511
+ module Migration
512
+ class CreateUsers < ActiveRecord::Migration
513
+ def up
514
+ create_table :users do |t|
515
+ t.string :name, :null => false
516
+ t.references :group, :null => false
517
+ end
518
+ change_table :users do |t|
519
+ t.index [:name, :group_id], :unique => true
520
+ end
521
+ end
522
+
523
+ def down
524
+ drop_table :users
525
+ end
526
+ end
527
+
528
+ class CreateGroups < ActiveRecord::Migration
529
+ def up
530
+ create_table :groups do |t|
531
+ t.string :name, :null => false
532
+ end
533
+ change_table :groups do |t|
534
+ t.index :name, :unique => true
535
+ end
536
+ end
537
+
538
+ def down
539
+ drop_table :groups
540
+ end
541
+ end
542
+
543
+ class CreateSystems < ActiveRecord::Migration
544
+ def up
545
+ create_table :systems do |t|
546
+ t.string :name, :null => false
547
+ t.references :system_type, :null => false
548
+ t.datetime :start_time, :null => false
549
+ t.datetime :end_time
550
+ t.integer :cores, :null => false
551
+ #To consider: replace with a float? (more compact)
552
+ t.integer :memory, :null => false, :limit => 8
553
+ end
554
+ change_table :systems do |t|
555
+ t.index [:name, :end_time], :unique => true
556
+ t.index :start_time
557
+ t.index :end_time
558
+ t.index :system_type_id
559
+ end
560
+ end
561
+
562
+ def down
563
+ drop_table :systems
564
+ end
565
+ end
566
+
567
+ class CreateSystemTypes < ActiveRecord::Migration
568
+ def up
569
+ create_table :system_types do |t|
570
+ t.string :name, :null => false
571
+ t.integer :memory_stat_type, :limit => 1, :null => false
572
+ end
573
+ change_table :system_types do |t|
574
+ t.index :name, :unique => true
575
+ end
576
+ end
577
+
578
+ def down
579
+ drop_table :system_types
580
+ end
581
+ end
582
+
583
+ class CreateJobs < ActiveRecord::Migration
584
+ def up
585
+ create_table :jobs do |t|
586
+ t.references :user, :null => false
587
+ t.references :system, :null => false
588
+ t.string :command_name, :limit => 24
589
+ t.datetime :start_time, :null => false
590
+ t.datetime :end_time, :null => false
591
+ t.integer :wall_time, :null => false
592
+ t.integer :cpu_time, :null => false
593
+ t.integer :memory, :null => false
594
+ t.integer :exit_code, :null => false
595
+ end
596
+ change_table :jobs do |t|
597
+ t.index :user_id
598
+ t.index :system_id
599
+ t.index :command_name
600
+ t.index :start_time
601
+ t.index :end_time
602
+ end
603
+ end
604
+
605
+ def down
606
+ drop_table :jobs
607
+ end
608
+ end
609
+
610
+ class CreateLocks < ActiveRecord::Migration
611
+ def up
612
+ create_table :locks do |t|
613
+ t.string :name
614
+ end
615
+ change_table :locks do |t|
616
+ t.index :name, :unique => true
617
+ end
618
+
619
+ ['users', 'groups', 'systems', 'system_types'].each do |name|
620
+ Lock.create!(:name => name)
621
+ end
622
+ end
623
+
624
+ def down
625
+ drop_table :locks
626
+ end
627
+ end
628
+
629
+ class << self;
630
+ ##
631
+ #Brings up all migrations
632
+ def up
633
+ CreateUsers.new.up
634
+ CreateGroups.new.up
635
+ CreateSystems.new.up
636
+ CreateSystemTypes.new.up
637
+ CreateJobs.new.up
638
+ CreateLocks.new.up
639
+ end
640
+
641
+ ##
642
+ #Brings down all migrations
643
+ #
644
+ #Warning: this will destroy all data!
645
+ def down
646
+ CreateUsers.new.down
647
+ CreateGroups.new.down
648
+ CreateSystems.new.down
649
+ CreateSystemTypes.new.down
650
+ CreateJobs.new.down
651
+ CreateLocks.new.down
652
+ end
653
+ end
654
+ end
655
+ end
656
+ end