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