foreman-tasks 6.0.0 → 6.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a31c8423a352c5eeb6ff93b23ce8ed3d868ee4bdb16d456233f2c3df475342cd
4
- data.tar.gz: c181fdc4c13d8c3cfb7e8d8469c9188a8972b97ca223e68cb50d9a901e23d552
3
+ metadata.gz: a5c1faf969670adf42567f2dd46b7b78dc2199561bb96b483516fe4169b7558d
4
+ data.tar.gz: 9aad850fd8decb284e1ccfc1869e5220da554e53286d31ccb3abb6da492a3ca3
5
5
  SHA512:
6
- metadata.gz: c7f2b880bb70b4fc2781ab9c5bff2c7bcd0434bc067669fae601f1815f6720e7dd771a170cdde8a4922565f1bd87c61d5b3dc1c0b10192bd895508d123aa84da
7
- data.tar.gz: a4652958efa52fec83b6c0a8093914b1d22ad574bc4763cb66c3bc41e82bd67e5b1448be0fd79ae3ce64225cb44d6c25faeb324cd10a846dfbe77abee5d55b02
6
+ metadata.gz: 9f76c37ad5faccedfee63f80b35b9bb1a3b65974fbef664c49baac53f6991846ed215c76cef6452bd966ea9da219d1adbae82edba3b3e783c7619ce832d51e96
7
+ data.tar.gz: d32b7d573c6809bba7bc28a4568513544752657461a9877c4f3824e8f5b0eca0b674fa9161f97cd8c72204b6edcc52f3e86480e19636538c53c8a4f473304e4f
@@ -4,6 +4,7 @@ env:
4
4
  RAILS_ENV: test
5
5
  DATABASE_URL: postgresql://postgres:@localhost/test
6
6
  DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true
7
+ BUNDLE_WITHOUT: "journald:development:console:mysql2:sqlite:libvirt"
7
8
  jobs:
8
9
  rubocop:
9
10
  runs-on: ubuntu-latest
@@ -13,10 +14,7 @@ jobs:
13
14
  uses: ruby/setup-ruby@v1
14
15
  with:
15
16
  ruby-version: 2.7
16
- - name: Setup
17
- run: |
18
- gem install bundler
19
- bundle install --jobs=3 --retry=3
17
+ bundler-cache: true
20
18
  - name: Run rubocop
21
19
  run: bundle exec rubocop
22
20
  test_ruby:
@@ -43,30 +41,21 @@ jobs:
43
41
  - uses: actions/checkout@v2
44
42
  with:
45
43
  path: foreman-tasks
44
+ - name: Setup Plugin in Foreman
45
+ run: |
46
+ echo "gem 'foreman-tasks', path: './foreman-tasks'" > bundler.d/foreman-tasks.local.rb
47
+ echo "gem 'sqlite3'" >> bundler.d/foreman-tasks.local.rb
46
48
  - name: Setup Ruby
47
49
  uses: ruby/setup-ruby@v1
48
50
  with:
49
51
  ruby-version: ${{ matrix.ruby-version }}
52
+ bundler-cache: true
50
53
  - name: Setup Node
51
54
  uses: actions/setup-node@v1
52
55
  with:
53
56
  node-version: ${{ matrix.node-version }}
54
- - uses: actions/cache@v2
55
- with:
56
- path: vendor/bundle
57
- key: ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
58
- restore-keys: |
59
- ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-
60
- - name: Setup Bundler
61
- run: |
62
- echo "gem 'foreman-tasks', path: './foreman-tasks'" > bundler.d/foreman-tasks.local.rb
63
- echo "gem 'sqlite3'" >> bundler.d/foreman-tasks.local.rb
64
- gem install bundler
65
- bundle config set without journald development console libvirt
66
- bundle config set path vendor/bundle
67
57
  - name: Prepare test env
68
58
  run: |
69
- bundle install --jobs=3 --retry=3
70
59
  bundle exec rake db:create
71
60
  bundle exec rake db:migrate
72
61
  - name: Run plugin tests
data/.rubocop.yml CHANGED
@@ -9,6 +9,7 @@ AllCops:
9
9
  Exclude:
10
10
  - 'node_modules/**/*'
11
11
  - 'locale/*'
12
+ - 'vendor/**/*'
12
13
  TargetRubyVersion: 2.5
13
14
 
14
15
  Lint/ShadowingOuterLocalVariable:
@@ -4,6 +4,8 @@ module ForemanTasks
4
4
  include Foreman::Controller::CsvResponder
5
5
  include ForemanTasks::FindTasksCommon
6
6
 
7
+ before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :cancel_step, :resume]
8
+
7
9
  def show
8
10
  @task = resource_base.find(params[:id])
9
11
  render :layout => !request.xhr?
@@ -31,8 +33,7 @@ module ForemanTasks
31
33
  end
32
34
 
33
35
  def cancel_step
34
- task = find_dynflow_task
35
- result = ForemanTasks.dynflow.world.event(task.external_id, params[:step_id].to_i, ::Dynflow::Action::Cancellable::Cancel).wait
36
+ result = ForemanTasks.dynflow.world.event(@dynflow_task.external_id, params[:step_id].to_i, ::Dynflow::Action::Cancellable::Cancel).wait
36
37
  if result.rejected?
37
38
  render json: { error: result.reason }, status: :bad_request
38
39
  else
@@ -41,8 +42,7 @@ module ForemanTasks
41
42
  end
42
43
 
43
44
  def cancel
44
- task = find_dynflow_task
45
- if task.cancel
45
+ if @dynflow_task.cancel
46
46
  render json: { statusText: 'OK' }
47
47
  else
48
48
  render json: {}, status: :bad_request
@@ -50,19 +50,17 @@ module ForemanTasks
50
50
  end
51
51
 
52
52
  def abort
53
- task = find_dynflow_task
54
- if task.abort
53
+ if @dynflow_task.abort
55
54
  flash[:info] = _('Trying to abort the task')
56
55
  else
57
56
  flash[:warning] = _('The task cannot be aborted at the moment.')
58
57
  end
59
- redirect_back(:fallback_location => foreman_tasks_task_path(task))
58
+ redirect_back(:fallback_location => foreman_tasks_task_path(@dynflow_task))
60
59
  end
61
60
 
62
61
  def resume
63
- task = find_dynflow_task
64
- if task.resumable?
65
- ForemanTasks.dynflow.world.execute(task.execution_plan.id)
62
+ if @dynflow_task.resumable?
63
+ ForemanTasks.dynflow.world.execute(@dynflow_task.execution_plan.id)
66
64
  render json: { statusText: 'OK' }
67
65
  else
68
66
  render json: {}, status: :bad_request
@@ -70,10 +68,8 @@ module ForemanTasks
70
68
  end
71
69
 
72
70
  def unlock
73
- task = find_dynflow_task
74
- if task.paused?
75
- task.state = :stopped
76
- task.save!
71
+ if @dynflow_task.paused?
72
+ unlock_task(@dynflow_task)
77
73
  render json: { statusText: 'OK' }
78
74
  else
79
75
  render json: {}, status: :bad_request
@@ -81,9 +77,7 @@ module ForemanTasks
81
77
  end
82
78
 
83
79
  def force_unlock
84
- task = find_dynflow_task
85
- task.state = :stopped
86
- task.save!
80
+ unlock_task(@dynflow_task)
87
81
  render json: { statusText: 'OK' }
88
82
  end
89
83
 
@@ -98,6 +92,12 @@ module ForemanTasks
98
92
 
99
93
  private
100
94
 
95
+ def unlock_task(task)
96
+ task.state = :stopped
97
+ task.locks.destroy_all
98
+ task.save!
99
+ end
100
+
101
101
  def respond_with_tasks(scope)
102
102
  @tasks = filter(scope, paginate: false).with_duration
103
103
  csv_response(@tasks, [:id, :action, :state, :result, 'started_at.in_time_zone', 'ended_at.in_time_zone', :duration, :username], ['Id', 'Action', 'State', 'Result', 'Started At', 'Ended At', 'Duration', 'User'])
@@ -123,7 +123,7 @@ module ForemanTasks
123
123
  end
124
124
 
125
125
  def find_dynflow_task
126
- resource_scope.where(:type => 'ForemanTasks::Task::DynflowTask').find(params[:id])
126
+ @dynflow_task = resource_scope.where(:type => 'ForemanTasks::Task::DynflowTask').find(params[:id])
127
127
  end
128
128
 
129
129
  def filter(scope, paginate: true)
@@ -22,7 +22,15 @@ module Actions
22
22
  end
23
23
  end
24
24
 
25
- class ProxyActionStopped; end
25
+ class ProxyActionStopped < RuntimeError
26
+ def backtrace
27
+ []
28
+ end
29
+ end
30
+
31
+ ProxyActionStoppedEvent = ::Algebrick.type do
32
+ fields! exception: type { variants NilClass, Exception }
33
+ end
26
34
 
27
35
  def plan(proxy, klass, options)
28
36
  options[:connection_options] ||= {}
@@ -52,8 +60,8 @@ module Actions
52
60
  on_data(event.data, event.meta)
53
61
  when ProxyActionMissing
54
62
  on_proxy_action_missing
55
- when ProxyActionStopped
56
- on_proxy_action_stopped
63
+ when ProxyActionStoppedEvent
64
+ on_proxy_action_stopped(event)
57
65
  else
58
66
  raise "Unexpected event #{event.inspect}"
59
67
  end
@@ -94,6 +102,8 @@ module Actions
94
102
  else
95
103
  suspend
96
104
  end
105
+ rescue RestClient::NotFound
106
+ on_proxy_action_missing
97
107
  end
98
108
 
99
109
  def cancel_proxy_task
@@ -133,8 +143,12 @@ module Actions
133
143
  error! ProxyActionMissing.new(_('Proxy task gone missing from the smart proxy'))
134
144
  end
135
145
 
136
- def on_proxy_action_stopped
137
- check_task_status
146
+ def on_proxy_action_stopped(event)
147
+ if event.exception
148
+ error! ProxyActionStopped.new(_('Failed to trigger task on the smart proxy: ') + event.exception.message)
149
+ else
150
+ check_task_status
151
+ end
138
152
  end
139
153
 
140
154
  # @override String name of an action to be triggered on server
@@ -40,8 +40,12 @@ module Actions
40
40
  output[:planned_count] += group.size
41
41
  end
42
42
  rescue => e
43
- action_logger.warn "Could not trigger task on the smart proxy: #{e.message}"
44
- batch.each { |remote_task| remote_task.update_from_batch_trigger({}) }
43
+ action_logger.warn "Could not trigger task on the smart proxy"
44
+ action_logger.warn e
45
+ # The response contains non-serializable objects
46
+ # TypeError: no _dump_data is defined for class Monitor
47
+ e.response = nil
48
+ batch.each { |remote_task| remote_task.update_from_batch_trigger({ 'exception' => e }) }
45
49
  output[:failed_count] += batch.size
46
50
  end
47
51
 
@@ -90,7 +90,7 @@ module ForemanTasks
90
90
  def next_occurrence_time(time = Time.zone.now)
91
91
  @parser ||= CronParser.new(cron_line, Time.zone)
92
92
  # @parser.next(start_time) is not inclusive of the start_time hence stepping back one run to include checking start_time for the first run.
93
- before_next = @parser.next(@parser.last(time))
93
+ before_next = @parser.next(@parser.last(time.in_time_zone))
94
94
  return before_next if before_next >= time && tasks.count == 0
95
95
  @parser.next(time)
96
96
  end
@@ -18,7 +18,8 @@ module ForemanTasks
18
18
  response = begin
19
19
  proxy.launch_tasks('single', :action_class => proxy_action_name, :action_input => input)
20
20
  rescue RestClient::Exception => e
21
- logger.warn "Could not trigger task on the smart proxy: #{e.message}"
21
+ logger.warn "Could not trigger task on the smart proxy"
22
+ logger.warn e
22
23
  {}
23
24
  end
24
25
  update_from_batch_trigger(response)
@@ -26,13 +27,13 @@ module ForemanTasks
26
27
  end
27
28
 
28
29
  def self.batch_trigger(operation, remote_tasks)
29
- remote_tasks.group_by(&:proxy_url).values.map do |group|
30
+ remote_tasks.group_by(&:proxy_url).each_value do |group|
30
31
  input_hash = group.reduce({}) do |acc, remote_task|
31
32
  acc.merge(remote_task.execution_plan_id => { :action_input => remote_task.proxy_input,
32
33
  :action_class => remote_task.proxy_action_name })
33
34
  end
34
- results = remote_tasks.first.proxy.launch_tasks(operation, input_hash)
35
- remote_tasks.each do |remote_task|
35
+ results = group.first.proxy.launch_tasks(operation, input_hash)
36
+ group.each do |remote_task|
36
37
  remote_task.update_from_batch_trigger results.fetch(remote_task.execution_plan_id, {}),
37
38
  results.fetch('parent', {})
38
39
  end
@@ -48,10 +49,11 @@ module ForemanTasks
48
49
  self.parent_task_id = parent['task_id']
49
50
  self.state = 'parent-triggered'
50
51
  else
52
+ exception = data['exception']
51
53
  # Tell the action the task on the smart proxy stopped
52
54
  ForemanTasks.dynflow.world.event execution_plan_id,
53
55
  step_id,
54
- ::Actions::ProxyAction::ProxyActionStopped.new,
56
+ ::Actions::ProxyAction::ProxyActionStoppedEvent[exception],
55
57
  optional: true
56
58
  end
57
59
  save!
@@ -42,17 +42,17 @@ module ForemanTasks
42
42
  security_block :foreman_tasks do |_map|
43
43
  permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks, :show],
44
44
  :'foreman_tasks/react' => [:index],
45
- :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary, :summary_sub_tasks, :details, :sub_tasks] }, :resource_type => ForemanTasks::Task.name
45
+ :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary, :summary_sub_tasks, :details, :sub_tasks] }, :resource_type => 'ForemanTasks::Task'
46
46
  permission :edit_foreman_tasks, { :'foreman_tasks/tasks' => [:resume, :unlock, :force_unlock, :cancel_step, :cancel, :abort],
47
- :'foreman_tasks/api/tasks' => [:bulk_resume, :bulk_cancel, :bulk_stop] }, :resource_type => ForemanTasks::Task.name
47
+ :'foreman_tasks/api/tasks' => [:bulk_resume, :bulk_cancel, :bulk_stop] }, :resource_type => 'ForemanTasks::Task'
48
48
 
49
- permission :create_recurring_logics, {}, :resource_type => ForemanTasks::RecurringLogic.name
49
+ permission :create_recurring_logics, {}, :resource_type => 'ForemanTasks::RecurringLogic'
50
50
 
51
51
  permission :view_recurring_logics, { :'foreman_tasks/recurring_logics' => [:auto_complete_search, :index, :show],
52
- :'foreman_tasks/api/recurring_logics' => [:index, :show] }, :resource_type => ForemanTasks::RecurringLogic.name
52
+ :'foreman_tasks/api/recurring_logics' => [:index, :show] }, :resource_type => 'ForemanTasks::RecurringLogic'
53
53
 
54
54
  permission :edit_recurring_logics, { :'foreman_tasks/recurring_logics' => [:cancel, :enable, :disable, :clear_cancelled],
55
- :'foreman_tasks/api/recurring_logics' => [:cancel, :update, :bulk_destroy] }, :resource_type => ForemanTasks::RecurringLogic.name
55
+ :'foreman_tasks/api/recurring_logics' => [:cancel, :update, :bulk_destroy] }, :resource_type => 'ForemanTasks::RecurringLogic'
56
56
  end
57
57
 
58
58
  add_all_permissions_to_default_roles
@@ -116,7 +116,7 @@ module ForemanTasks
116
116
  register_graphql_query_field :recurring_logic, '::Types::RecurringLogic', :record_field
117
117
  register_graphql_query_field :recurring_logics, '::Types::RecurringLogic', :collection_field
118
118
 
119
- register_graphql_mutation_field :cancel_recurring_logic, ::Mutations::RecurringLogics::Cancel
119
+ register_graphql_mutation_field :cancel_recurring_logic, '::Mutations::RecurringLogics::Cancel'
120
120
 
121
121
  logger :dynflow, :enabled => true
122
122
  logger :action, :enabled => true
@@ -241,35 +241,42 @@ namespace :foreman_tasks do
241
241
  end
242
242
  end
243
243
 
244
- def csv_export(export_filename, tasks)
244
+ def csv_export(export_filename, id_scope, task_scope)
245
245
  CSV.open(export_filename, 'wb') do |csv|
246
246
  csv << %w[id state type label result parent_task_id started_at ended_at duration]
247
- tasks.find_each do |task|
248
- with_error_handling(task) do
249
- csv << [task.id, task.state, task.type, task.label, task.result,
250
- task.parent_task_id, task.started_at, task.ended_at, task.duration]
247
+ id_scope.pluck(:id).each_slice(1000).each do |ids|
248
+ task_scope.where(id: ids).each do |task|
249
+ with_error_handling(task) do
250
+ csv << [task.id, task.state, task.type, task.label, task.result,
251
+ task.parent_task_id, task.started_at, task.ended_at, task.duration]
252
+ end
251
253
  end
252
254
  end
253
255
  end
254
256
  end
255
257
 
256
- def html_export(workdir, tasks)
258
+ def html_export(workdir, id_scope, task_scope)
257
259
  PageHelper.copy_assets(workdir)
258
260
 
261
+ ids = id_scope.pluck(:id)
259
262
  renderer = TaskRender.new
260
- total = tasks.count(:all)
263
+ count = 0
264
+ total = ids.count
261
265
  index = File.open(File.join(workdir, 'index.html'), 'w')
262
266
 
263
267
  File.open(File.join(workdir, 'index.html'), 'w') do |index|
264
268
  PageHelper.pagify(index) do |io|
265
269
  PageHelper.generate_with_index(io) do |index|
266
- tasks.find_each.each_with_index do |task, count|
267
- content = with_error_handling(task) { renderer.render_task(task) }
268
- if content
269
- File.open(File.join(workdir, "#{task.id}.html"), 'w') { |file| PageHelper.pagify(file, content) }
270
- with_error_handling(task, _('task index entry')) { PageHelper.generate_index_entry(index, task) }
270
+ ids.each_slice(1000).each do |ids|
271
+ task_scope.where(id: ids).each do |task|
272
+ content = with_error_handling(task) { renderer.render_task(task) }
273
+ if content
274
+ File.open(File.join(workdir, "#{task.id}.html"), 'w') { |file| PageHelper.pagify(file, content) }
275
+ with_error_handling(task, _('task index entry')) { PageHelper.generate_index_entry(index, task) }
276
+ end
277
+ count += 1
278
+ puts "#{count}/#{total}"
271
279
  end
272
- puts "#{count + 1}/#{total}"
273
280
  end
274
281
  end
275
282
  end
@@ -317,21 +324,22 @@ namespace :foreman_tasks do
317
324
  format = ENV['TASK_FORMAT'] || 'html'
318
325
  export_filename = ENV['TASK_FILE'] || generate_filename(format)
319
326
 
320
- tasks = ForemanTasks::Task.search_for(filter).order(:started_at => :desc).with_duration.distinct
327
+ task_scope = ForemanTasks::Task.search_for(filter).with_duration.order(:started_at => :desc)
328
+ id_scope = task_scope.group(:id, :started_at)
321
329
 
322
330
  puts _("Exporting all tasks matching filter #{filter}")
323
- puts _("Gathering #{tasks.count(:all)} tasks.")
331
+ puts _("Gathering #{id_scope.count(:all).count} tasks.")
324
332
  case format
325
333
  when 'html'
326
334
  Dir.mktmpdir('task-export') do |tmp_dir|
327
- html_export(tmp_dir, tasks)
335
+ html_export(tmp_dir, id_scope, task_scope)
328
336
  system("tar", "czf", export_filename, tmp_dir)
329
337
  end
330
338
  when 'html-dir'
331
339
  FileUtils.mkdir_p(export_filename)
332
- html_export(export_filename, tasks)
340
+ html_export(export_filename, id_scope, task_scope)
333
341
  when 'csv'
334
- csv_export(export_filename, tasks)
342
+ csv_export(export_filename, id_scope, task_scope)
335
343
  else
336
344
  raise "Unkonwn export format '#{format}'"
337
345
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '6.0.0'.freeze
2
+ VERSION = '6.0.3'.freeze
3
3
  end
@@ -1,6 +1,5 @@
1
1
  # Autogenerated!
2
- _("Preupgrade job")
3
- _("Remote action:")
4
2
  _("Import Puppet classes")
5
3
  _("Import facts")
6
- _("Action with sub plans")
4
+ _("Action with sub plans")
5
+ _("Remote action:")
@@ -56,6 +56,9 @@ msgstr ""
56
56
  msgid "A paused task represents a process that has not finished properly. Any task in paused state can lead to potential inconsistency and needs to be resolved."
57
57
  msgstr ""
58
58
 
59
+ msgid "A special label for tracking a recurring job. There can be only one active job with a given purpose at a time."
60
+ msgstr ""
61
+
59
62
  msgid "Action"
60
63
  msgstr ""
61
64
 
@@ -68,6 +71,9 @@ msgstr ""
68
71
  msgid "Active Filters:"
69
72
  msgstr ""
70
73
 
74
+ msgid "Active or disabled recurring logic with purpose %s already exists"
75
+ msgstr ""
76
+
71
77
  msgid "All %s tasks are selected. "
72
78
  msgstr ""
73
79
 
@@ -276,6 +282,9 @@ msgstr ""
276
282
  msgid "Fri"
277
283
  msgstr ""
278
284
 
285
+ msgid "I understand that this may cause harm and have working database backups of all backend services."
286
+ msgstr ""
287
+
279
288
  msgid "ID"
280
289
  msgstr ""
281
290
 
@@ -427,9 +436,6 @@ msgstr ""
427
436
  msgid "Polling multiplier which is used to multiply the default polling intervals. This can be used to prevent polling too frequently for long running tasks."
428
437
  msgstr ""
429
438
 
430
- msgid "Preupgrade job"
431
- msgstr ""
432
-
433
439
  msgid "Proxy action retry count"
434
440
  msgstr ""
435
441
 
@@ -442,6 +448,9 @@ msgstr ""
442
448
  msgid "Proxy tasks batch size"
443
449
  msgstr ""
444
450
 
451
+ msgid "Purpose"
452
+ msgstr ""
453
+
445
454
  msgid "Raw"
446
455
  msgstr ""
447
456
 
@@ -487,6 +496,9 @@ msgstr ""
487
496
  msgid "Resource search_params requires resource_type and resource_id to be specified"
488
497
  msgstr ""
489
498
 
499
+ msgid "Resources for %s task(s) will be unlocked and will not prevent other tasks from being run. As the task(s) might be still running, it should be avoided to use this unless you are really sure the task(s) got stuck."
500
+ msgstr ""
501
+
490
502
  msgid "Result"
491
503
  msgstr ""
492
504
 
@@ -663,6 +675,12 @@ msgstr[1] ""
663
675
  msgid "This action will delete all cancelled recurring logics. Please note that this action can't be reversed."
664
676
  msgstr ""
665
677
 
678
+ msgid "This will %(action)s %(number)s task(s), putting them in the %(state)s state. Are you sure?"
679
+ msgstr ""
680
+
681
+ msgid "This will unlock the resources that the task is running against. Please note that this might lead to inconsistent state and should be used with caution, after making sure that the task can't be resumed."
682
+ msgstr ""
683
+
666
684
  msgid "Thu"
667
685
  msgstr ""
668
686
 
@@ -717,6 +735,9 @@ msgstr ""
717
735
  msgid "Yes"
718
736
  msgstr ""
719
737
 
738
+ msgid "You can find resource locks on this page. Exclusive lock marked with locked icon means that no other task can use locked resource while this task is running. Non-exclusive lock marked with unlocked icon means other tasks can access the resource freely, it is only used to indicate the relation of this task with the resource"
739
+ msgstr ""
740
+
720
741
  msgid "You do not have permission"
721
742
  msgstr ""
722
743
 
@@ -758,6 +779,9 @@ msgstr ""
758
779
  msgid "is month (range: 1-12)"
759
780
  msgstr ""
760
781
 
782
+ msgid "is not a valid format"
783
+ msgstr ""
784
+
761
785
  msgid "last"
762
786
  msgstr ""
763
787