1package App::Netdisco::Backend::Job;
2
3use Dancer qw/:moose :syntax !error/;
4use aliased 'App::Netdisco::Worker::Status';
5
6use Moo;
7use namespace::clean;
8
9foreach my $slot (qw/
10      job
11      entered
12      started
13      finished
14      device
15      port
16      action
17      only_namespace
18      subaction
19      status
20      username
21      userip
22      log
23      device_key
24      job_priority
25      is_cancelled
26
27      _current_phase
28      _last_namespace
29      _last_priority
30    /) {
31
32  has $slot => (
33    is => 'rw',
34  );
35}
36
37has '_statuslist' => (
38  is => 'rw',
39  default => sub { [] },
40);
41
42sub BUILD {
43  my ($job, $args) = @_;
44
45  if ($job->action =~ m/^(\w+)::(\w+)$/i) {
46    $job->action($1);
47    $job->only_namespace($2);
48  }
49
50  if (!defined $job->subaction) {
51    $job->subaction('');
52  }
53}
54
55=head1 METHODS
56
57=head2 display_name
58
59An attempt to make a meaningful written statement about the job.
60
61=cut
62
63sub display_name {
64  my $job = shift;
65  return join ' ',
66    $job->action,
67    ($job->device || ''),
68    ($job->port || '');
69}
70
71=head2 cancel
72
73Log a status and prevent other stages from running.
74
75=cut
76
77sub cancel {
78  my ($job, $msg) = @_;
79  $msg ||= 'unknown reason for cancelled job';
80  $job->is_cancelled(true);
81  return Status->error($msg);
82}
83
84=head2 best_status
85
86Find the best status so far. The process is to track back from the last worker
87and find the highest scoring status, skipping the check phase.
88
89=cut
90
91sub best_status {
92  my $job = shift;
93  my $cur_level = 0;
94  my $cur_status = '';
95
96  return Status->error()->status if $job->is_cancelled;
97
98  foreach my $status (reverse @{ $job->_statuslist }) {
99    next if $status->phase
100      and $status->phase !~ m/^(?:early|main|store|late)$/;
101
102    if ($status->level >= $cur_level) {
103      $cur_level = $status->level;
104      $cur_status = $status->status;
105    }
106  }
107
108  return $cur_status;
109}
110
111=head2 finalise_status
112
113Find the best status and log it into the job's C<status> and C<log> slots.
114
115=cut
116
117sub finalise_status {
118  my $job = shift;
119  # use DDP; p $job->_statuslist;
120
121  # fallback
122  $job->status('error');
123  $job->log('failed to report from any worker!');
124
125  my $max_level = Status->error()->level;
126
127  if ($job->is_cancelled and scalar @{ $job->_statuslist }) {
128    $job->status( $job->_statuslist->[-1]->status );
129    $job->log( $job->_statuslist->[-1]->log );
130    return;
131  }
132
133  foreach my $status (reverse @{ $job->_statuslist }) {
134    next if $status->phase
135      and $status->phase !~ m/^(?:check|early|main|store|late)$/;
136
137    # done() from check phase should not be the action's done()
138    next if $status->phase eq 'check' and $status->is_ok;
139
140    if ($status->level >= $max_level) {
141      $job->status( $status->status );
142      $job->log( $status->log );
143      $max_level = $status->level;
144    }
145  }
146}
147
148=head2 check_passed
149
150Returns true if at least one worker during the C<check> phase flagged status
151C<done>.
152
153=cut
154
155sub check_passed {
156  my $job = shift;
157  return true if 0 == scalar @{ $job->_statuslist };
158
159  foreach my $status (@{ $job->_statuslist }) {
160    return true if
161      (($status->phase eq 'check') and $status->is_ok);
162  }
163  return false;
164}
165
166=head2 namespace_passed( \%workerconf )
167
168Returns true when, for the namespace specified in the given configuration, a
169worker of a higher priority level has already succeeded.
170
171=cut
172
173sub namespace_passed {
174  my ($job, $workerconf) = @_;
175
176  if ($job->_last_namespace) {
177    foreach my $status (@{ $job->_statuslist }) {
178      next unless ($status->phase eq $workerconf->{phase})
179              and ($workerconf->{namespace} eq $job->_last_namespace)
180              and ($workerconf->{priority} < $job->_last_priority);
181      return true if $status->is_ok;
182    }
183  }
184
185  $job->_last_namespace( $workerconf->{namespace} );
186  $job->_last_priority( $workerconf->{priority} );
187  return false;
188}
189
190=head2 enter_phase( $phase )
191
192Pass the name of the phase being entered.
193
194=cut
195
196sub enter_phase {
197  my ($job, $phase) = @_;
198
199  $job->_current_phase( $phase );
200  debug "=> running workers for phase: $phase";
201
202  $job->_last_namespace( undef );
203  $job->_last_priority( undef );
204}
205
206=head2 add_status
207
208Passed an L<App::Netdisco::Worker::Status> will add it to this job's internal
209status cache. Phase slot of the Status will be set to the current phase.
210
211=cut
212
213sub add_status {
214  my ($job, $status) = @_;
215  return unless ref $status eq 'App::Netdisco::Worker::Status';
216  $status->phase( $job->_current_phase || '' );
217  push @{ $job->_statuslist }, $status;
218  debug $status->log if $status->log
219    and (($status->phase eq 'check') or $status->not_ok);
220}
221
222=head1 ADDITIONAL COLUMNS
223
224Columns which exist in this class but are not in
225L<App::Netdisco::DB::Result::Admin> class.
226
227
228=head2 id
229
230Alias for the C<job> column.
231
232=cut
233
234sub id { (shift)->job }
235
236=head2 extra
237
238Alias for the C<subaction> column.
239
240=head2 only_namespace
241
242Action command from the user can be an action name or the action name plus one
243child namespace in the form: "C<action::child>". This slot stores the C<child>
244component of the command so that C<action> is backwards compatible with
245Netdisco.
246
247=head2 job_priority
248
249When selecting jobs from the database, some types of job are higher priority -
250usually those submitted in the web interface by a user, and those making
251changes (writing to) the device. This slot stores a number which is the
252priority of the job and is used by L<MCE> when managing its job queue.
253
254=cut
255
256sub extra { (shift)->subaction }
257
258true;
259