Class: ForemanTasks::Cleaner

Inherits:
Object
  • Object
show all
Defined in:
lib/foreman_tasks/cleaner.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Cleaner

seconds ago. If not specified, no implicit filtering on the date.

Parameters:

  • filter (String)

    scoped search matching the tasks to be deleted

  • after (String|nil)

    delete the tasks after they are older than the value: the number in string is expected to be followed by time unit specification one of s,h,d,y for

Raises:

  • (ArgumentError)
[View source]

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/foreman_tasks/cleaner.rb', line 110

def initialize(options = {})
  default_options = { :after        => '0s',
                      :verbose      => false,
                      :batch_size   => 1000,
                      :noop         => false,
                      :states       => ['stopped'],
                      :backup_dir   => ForemanTasks.dynflow.world.persistence.current_backup_dir }
  options         = default_options.merge(options)
  Foreman::Logging.logger('foreman-tasks').info("Running foreman-tasks cleaner with options #{options.inspect}")

  @filter         = options[:filter]
  @after          = parse_time_interval(options[:after])
  @states         = options[:states]
  @verbose        = options[:verbose]
  @batch_size     = options[:batch_size]
  @noop           = options[:noop]
  @backup_dir     = options[:backup_dir]

  raise ArgumentError, 'filter not speficied' if @filter.nil?

  @full_filter = prepare_filter
end

Instance Attribute Details

#afterObject (readonly)

Returns the value of attribute after.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def after
  @after
end

#batch_sizeObject (readonly)

Returns the value of attribute batch_size.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def batch_size
  @batch_size
end

#filterObject (readonly)

Returns the value of attribute filter.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def filter
  @filter
end

#full_filterObject (readonly)

Returns the value of attribute full_filter.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def full_filter
  @full_filter
end

#noopObject (readonly)

Returns the value of attribute noop.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def noop
  @noop
end

#statesObject (readonly)

Returns the value of attribute states.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def states
  @states
end

#verboseObject (readonly)

Returns the value of attribute verbose.


103
104
105
# File 'lib/foreman_tasks/cleaner.rb', line 103

def verbose
  @verbose
end

Class Method Details

.actions_by_rules(action_rules) ⇒ Object

[View source]

90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/foreman_tasks/cleaner.rb', line 90

def self.actions_by_rules(action_rules)
  disable_actions_with_periods = action_rules.exclude_search
  cleanup_settings.fetch(:rules, []).map do |hash|
    next if hash[:after].nil?
    conditions = []
    conditions << disable_actions_with_periods unless hash[:override_actions]
    conditions << hash[:filter] if hash[:filter]
    hash[:states] = [] if hash[:states] == 'all'
    hash[:filter] = conditions.map { |condition| "(#{condition})" }.join(' AND ')
    hash
  end.compact
end

.actions_with_default_cleanupObject

[View source]

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/foreman_tasks/cleaner.rb', line 69

def self.actions_with_default_cleanup
  actions = cleanup_settings.fetch(:actions, [])
                            .flat_map do |action|
    Array(action[:name]).map do |klass|
      ActionRule.new(klass.safe_constantize || klass, action[:after], action[:filter])
    end
            rescue => e
              Foreman::Logging.exception("Error handling #{action} cleanup settings", e)
              nil
  end.compact
  hardcoded = (ForemanTasks.dynflow.world.action_classes - actions.map(&:klass))
              .select { |klass| klass.respond_to?(:cleanup_after) || klass.respond_to?(:cleanup_rules) }
              .flat_map { |klass| klass.respond_to?(:cleanup_rules) ? klass.cleanup_rules : ActionRule.new(klass, klass.cleanup_after) }
  actions + hardcoded
end

.cleanup_settingsObject

[View source]

85
86
87
88
# File 'lib/foreman_tasks/cleaner.rb', line 85

def self.cleanup_settings
  return @cleanup_settings if @cleanup_settings
  @cleanup_settings = SETTINGS.dig(:'foreman-tasks', :cleanup) || {}
end

.run(options) ⇒ Object

[View source]

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/foreman_tasks/cleaner.rb', line 50

def self.run(options)
  if options.key?(:filter)
    new(options).delete
  else
    [:after, :states].each do |invalid_option|
      if options.key?(invalid_option)
        raise "The option #{invalid_option} is not valid unless the filter specified"
      end
    end
    with_periods = actions_with_default_cleanup
    ActionRule.compose_include_rules(with_periods).each do |rule|
      new(options.merge(:filter => rule.include_search, :after => rule.after)).delete
    end
    actions_by_rules(CompositeActionRule.new(*with_periods)).each do |hash|
      new(options.merge(hash)).delete
    end
  end
end

Instance Method Details

#deleteObject

Delete the filtered tasks, including the dynflow execution plans

[View source]

134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/foreman_tasks/cleaner.rb', line 134

def delete
  message = "deleting all tasks matching filter #{full_filter}"
  with_noop(ForemanTasks::Task.search_for(full_filter), 'tasks matching filter', message) do |source, name|
    with_batches(source, name) do |chunk|
      delete_tasks chunk
      delete_dynflow_plans chunk
      delete_remote_tasks(chunk)
    end
  end
  delete_orphaned_locks
  delete_orphaned_links
  delete_orphaned_dynflow_tasks
end

#delete_dynflow_plans(chunk) ⇒ Object

[View source]

181
182
183
# File 'lib/foreman_tasks/cleaner.rb', line 181

def delete_dynflow_plans(chunk)
  delete_dynflow_plans_by_uuid chunk.find_all { |task| task.is_a? Task::DynflowTask }.map(&:external_id)
end

#delete_dynflow_plans_by_uuid(uuids) ⇒ Object

[View source]

185
186
187
# File 'lib/foreman_tasks/cleaner.rb', line 185

def delete_dynflow_plans_by_uuid(uuids)
  ForemanTasks.dynflow.world.persistence.delete_execution_plans({ 'uuid' => uuids }, batch_size, @backup_dir)
end

#delete_orphaned_dynflow_tasksObject

[View source]

207
208
209
210
211
212
213
# File 'lib/foreman_tasks/cleaner.rb', line 207

def delete_orphaned_dynflow_tasks
  with_noop(orphaned_dynflow_tasks, 'orphaned execution plans') do |source, name|
    with_batches(source, name) do |chunk|
      delete_dynflow_plans_by_uuid chunk.select_map(:uuid)
    end
  end
end
[View source]

198
199
200
201
202
203
204
205
# File 'lib/foreman_tasks/cleaner.rb', line 198

def delete_orphaned_links
  orphaned_links = ForemanTasks::Link.left_outer_joins(:task).where(:'foreman_tasks_tasks.id' => nil)
  with_noop(orphaned_links, 'orphaned task links') do |source, name|
    with_batches(source, name) do |chunk|
      ForemanTasks::Link.where(id: chunk.pluck(:id)).delete_all
    end
  end
end

#delete_orphaned_locksObject

[View source]

189
190
191
192
193
194
195
196
# File 'lib/foreman_tasks/cleaner.rb', line 189

def delete_orphaned_locks
  orphaned_locks = ForemanTasks::Lock.left_outer_joins(:task).where(:'foreman_tasks_tasks.id' => nil)
  with_noop(orphaned_locks, 'orphaned task locks') do |source, name|
    with_batches(source, name) do |chunk|
      ForemanTasks::Lock.where(id: chunk.pluck(:id)).delete_all
    end
  end
end

#delete_remote_tasks(chunk) ⇒ Object

[View source]

158
159
160
# File 'lib/foreman_tasks/cleaner.rb', line 158

def delete_remote_tasks(chunk)
  ForemanTasks::RemoteTask.where(:execution_plan_id => chunk.map(&:external_id)).delete_all
end

#delete_tasks(chunk) ⇒ Object

[View source]

152
153
154
155
156
# File 'lib/foreman_tasks/cleaner.rb', line 152

def delete_tasks(chunk)
  tasks = ForemanTasks::Task.where(:id => chunk.map(&:id))
  tasks_to_csv(tasks, @backup_dir, 'foreman_tasks.csv') if @backup_dir
  tasks.delete_all
end

#orphaned_dynflow_tasksObject

[View source]

240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/foreman_tasks/cleaner.rb', line 240

def orphaned_dynflow_tasks
  dynflow_plan_uuid_attribute = "dynflow_execution_plans.uuid"
  if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
    # typecast the UUID attribute for Postgres
    dynflow_plan_uuid_attribute += "::varchar"
  end

  db = ForemanTasks.dynflow.world.persistence.adapter.db
  db.fetch("select dynflow_execution_plans.uuid from dynflow_execution_plans left join "\
           "foreman_tasks_tasks on (#{dynflow_plan_uuid_attribute} = foreman_tasks_tasks.external_id) "\
           "where foreman_tasks_tasks.id IS NULL")
end

#parse_time_interval(string) ⇒ Object

[View source]

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/foreman_tasks/cleaner.rb', line 281

def parse_time_interval(string)
  matched_string = string.delete(' ').match(/\A(\d+)(\w)\Z/)
  unless matched_string
    raise ArgumentError, "String #{string} isn't an expected specification of time in format of \"{number}{time_unit}\""
  end
  number = matched_string[1].to_i
  value = case matched_string[2]
          when 's'
            number.seconds
          when 'h'
            number.hours
          when 'd'
            number.days
          when 'y'
            number.years
          else
            raise ArgumentError, "Unexpected time unit in #{string}, expected one of [s,h,d,y]"
          end
  value
end

#prepare_filterObject

[View source]

253
254
255
256
257
258
# File 'lib/foreman_tasks/cleaner.rb', line 253

def prepare_filter
  filter_parts = [filter]
  filter_parts << %(started_at < "#{after.ago.to_s(:db)}") if after > 0
  filter_parts << "state ^ (#{states.join(',')})" if states.any?
  filter_parts.select(&:present?).map { |segment| "(#{segment})" }.join(' AND ')
end

#report_done(name) ⇒ Object

[View source]

272
273
274
# File 'lib/foreman_tasks/cleaner.rb', line 272

def report_done(name)
  say "Deleted #{@current} #{name}"
end

#report_progress(count) ⇒ Object

[View source]

267
268
269
270
# File 'lib/foreman_tasks/cleaner.rb', line 267

def report_progress(count)
  @current += count
  say "#{@current}/#{@total}", false if verbose
end

#say(message, log = true) ⇒ Object

[View source]

276
277
278
279
# File 'lib/foreman_tasks/cleaner.rb', line 276

def say(message, log = true)
  puts message
  Foreman::Logging.logger('foreman-tasks').info(message) if log
end

#start_tracking_progress(name, total = tasks.size) ⇒ Object

[View source]

260
261
262
263
264
265
# File 'lib/foreman_tasks/cleaner.rb', line 260

def start_tracking_progress(name, total = tasks.size)
  say "About to remove #{total} #{name}"
  @current = 0
  @total = total
  say "#{@current}/#{@total}", false if verbose
end

#tasksObject

[View source]

148
149
150
# File 'lib/foreman_tasks/cleaner.rb', line 148

def tasks
  ForemanTasks::Task.search_for(full_filter).select('DISTINCT foreman_tasks_tasks.id, foreman_tasks_tasks.type, foreman_tasks_tasks.external_id')
end

#tasks_to_csv(dataset, backup_dir, file_name) ⇒ Object

[View source]

162
163
164
165
166
167
168
169
170
# File 'lib/foreman_tasks/cleaner.rb', line 162

def tasks_to_csv(dataset, backup_dir, file_name)
  with_backup_file(backup_dir, file_name) do |csv, appending|
    csv << ForemanTasks::Task.attribute_names.to_csv unless appending
    dataset.each do |row|
      csv << row.attributes.values.to_csv
    end
  end
  dataset
end

#with_backup_file(backup_dir, file_name) ⇒ Object

[View source]

172
173
174
175
176
177
178
179
# File 'lib/foreman_tasks/cleaner.rb', line 172

def with_backup_file(backup_dir, file_name)
  FileUtils.mkdir_p(backup_dir) unless File.directory?(backup_dir)
  csv_file = File.join(backup_dir, file_name)
  appending = File.exist?(csv_file)
  File.open(csv_file, 'a') do |f|
    yield f, appending
  end
end

#with_batches(source, name) ⇒ Object

[View source]

225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/foreman_tasks/cleaner.rb', line 225

def with_batches(source, name)
  count = source.count
  if count.zero?
    say("No #{name} found, skipping.")
    return
  end
  start_tracking_progress(name, count)
  while (chunk = source.limit(batch_size)).any?
    chunk_size = chunk.count
    yield chunk
    report_progress(chunk_size)
  end
  report_done(name)
end

#with_noop(source, name, noop_message = nil) ⇒ Object

source must respond to :count and :limit

[View source]

216
217
218
219
220
221
222
223
# File 'lib/foreman_tasks/cleaner.rb', line 216

def with_noop(source, name, noop_message = nil)
  if noop
    say '[noop] ' + noop_message if noop_message
    say "[noop] #{source.count} #{name} would be deleted"
  else
    yield source, name
  end
end