Browse Source

v0.2.0 - initial release - fully operational

themage
theMage 2 years ago
parent
commit
35eab7e724

+ 2 - 0
lib/Hercules/Admin.pm

@ -29,6 +29,8 @@ sub startup {
29 29
  # jobs
30 30
  $r->route('/jobs/')->to('jobs#list');
31 31
  $r->route('/jobs/:group')->to('jobs#list');
32
  $r->route('/job/new')->to('jobs#new_job');
33
  $r->route('/job/add')->to('jobs#save');
32 34
  $r->route('/job/:job')->to('jobs#view');
33 35
  $r->route('/job/:job/start')->to('jobs#start');
34 36
  $r->route('/job/:job/stop')->to('jobs#stop');

+ 2 - 2
lib/Hercules/Admin/Dashboard.pm

@ -115,8 +115,8 @@ sub index {
115 115
  }
116 116
117 117
  @ginfo = sort {
118
          $b->{runnable_jobs}   <=> $a->{runnable_jobs}
119
      ||  $b->{next_job_start}  <=> $a->{next_job_start}
118
          $b->{runnable_jobs}//0      <=> $a->{runnable_jobs} // 0
119
      ||  $b->{next_job_start}//time  <=> $a->{next_job_start} // time
120 120
    } @ginfo;
121 121
122 122
  my ($jobs_behind, $jobs_ok, $jobs_failed, $max_late) = (0,0,0,0);

+ 114 - 7
lib/Hercules/Admin/Jobs.pm

@ -11,6 +11,8 @@ use Hercules::Utils qw(
11 11
  stash_core_job_classes
12 12
);
13 13
14
use JSON qw(from_json);
15
14 16
my %status2icon   = ( 
15 17
  failing => 'times',
16 18
  running => 'rocket',
@ -68,9 +70,6 @@ sub list {
68 70
  $stash->{ param_search } = $search;
69 71
  $stash->{ param_status } = $status;
70 72
71
  use Data::Dumper;
72
  $stash->{debug_dump} = Dumper(\@jobs);
73
74 73
  $c->render( template => 'jobs/list' );
75 74
}
76 75
@ -142,9 +141,9 @@ sub view {
142 141
  $stash->{job} = $job;
143 142
144 143
  my $output = $job->get_output();
145
  my $out = $stash->{job_output} = $output 
144
  my $out = $stash->{job_output} = ($output 
146 145
    ? {%{ $output }}
147
    : 0;
146
    : 0);
148 147
149 148
  if ($out and $out->{run_epoch}) {
150 149
    $out->{run_epoch_tu} = seconds_to_timeunits( $out->{run_epoch}, 3 );
@ -173,12 +172,120 @@ sub edit {
173 172
  _stash_groups( $c );
174 173
  stash_core_job_classes( $c );
175 174
176
  use Data::Dumper;
177
  $stash->{debug_dump} = Dumper( $c->stash );
175
  $c->render(template => 'jobs/edit');
176
}
177
178
sub new_job {
179
  my ($c) = @_;
180
  $c->stash->{job} = {};
181
  _stash_groups( $c );
182
  stash_core_job_classes( $c );
178 183
179 184
  $c->render(template => 'jobs/edit');
180 185
}
181 186
187
sub save {
188
  my ($c) = @_;
189
  my $stash = $c->stash;
190
191
  my %data;
192
  my ($name) = $c->param('name');
193
  return $c->reply->exception('Invalid name')
194
    if $name !~ m{\A\w[\w_\-]*\w\z};
195
  $data{name} = $name;
196
197
  my $jobname = $stash->{job};
198
  my $job;
199
  if ($jobname) {
200
    ($job) = Hercules::Db::Schedule->retrieve(name => $jobname);
201
    return $c->render->not_found
202
      unless $job;
203
  } else {
204
    return $c->render->exception
205
      unless $name;
206
207
    ($job) = Hercules::Db::Schedule->retrieve(name => $name);
208
209
    my $replace = $c->param('replace');
210
211
    if ($job and !$replace) {
212
      return $c->render(
213
          text => 'Another job with same name exists',
214
          status => 406
215
        );
216
    }
217
  }
218
219
  $data{cron_group} = $c->param('cron_group') // 0;
220
  if ($data{cron_group}) {
221
    my ($grp) = Hercules::Db::Group->retrieve($data{cron_group});
222
    return $c->reply->exception('Invalid cron group')
223
      unless $grp;
224
  } #no group? no problem
225
226
  $data{class} = $c->param('jobclass');
227
  if (!$data{class} or $data{class} !~ m{\A\w+(::\w+)*(::)?\z}) {
228
    return $c->reply->exception('Invalid job class');
229
  }
230
231
  $data{params} = $c->param('params');
232
  if ($data{params}) {
233
    eval {
234
      my $obj = from_json('{'.$data{params}.'}', {utf8=>1});
235
      $data{params} = $obj // {};
236
      1;
237
    } or do {
238
      return $c->reply->exception('Invalid params');
239
    };
240
  } else {
241
    $data{params} = {};
242
  }
243
244
  if ($data{run_every} = $c->param('every')) {
245
    if ($data{run_every} !~ m{\A\d+[smhdwMy]?\z}) {
246
      return $c->reply->exception('Invalid run_every parameter');
247
    }
248
  } elsif ($data{run_schedule} = $c->param('cron')) {
249
    my @parts = split /\s/, $data{run_schedule};
250
    return $c->reply->exception('Invalid run_schedule')
251
      if @parts != 5;
252
    for my $part (@parts) {
253
      return $c->reply->exception('Invalid run_schedule')
254
        if $part !~ m{\A(\*(/\d+)?|\d+(,\d+)*)\z};
255
    }
256
  } else {
257
    ## do nothing - this will be expanded to allow
258
    #  * run_after - a job is scheduled when another job
259
    #                ends successfully.
260
    #  * run_on_demand - a rest call to request a job to be run
261
    #
262
    # but for now, we just keep this empty.
263
  }
264
265
  $data{run_every} ||= '';
266
  $data{run_schedule} ||= '';
267
268
  # if we got here, everything should be ok
269
  # params is a object, because of serialization
270
  unless ($job) {
271
    my $_params = delete $data{params} || {};
272
    $data{flags} = 'active';
273
    ($job) = Hercules::Db::Schedule->create(\%data);
274
    %data = ( params => $_params );
275
  }
276
 
277
  for my $k (keys %data) {
278
    if ($k eq 'params') {
279
      $job->set_params( $data{$k} );
280
    } else {
281
      $job->$k($data{$k});
282
    }
283
  }
284
  $job->update;
285
286
  return $c->render(text => 'ok, maybe');
287
}
288
182 289
sub _stash_groups {
183 290
  my ($c) = @_;
184 291

+ 1 - 1
lib/Hercules/Config.pm

@ -1,6 +1,6 @@
1 1
package Hercules::Config;
2 2
3
our $VERSION = '0.1.0';
3
our $VERSION = '0.2.0';
4 4
5 5
use Config::RecurseINI 'hercules' => qw(config);
6 6

+ 7 - 0
lib/Hercules/Db/Schedule.pm

@ -110,6 +110,13 @@ __PACKAGE__->has_a(params => 'Hercules::Params',
110 110
      },
111 111
  );
112 112
113
sub set_params {
114
  my ($self, $params) = @_;
115
116
  $params = {} unless $params;
117
  return $self->params( bless { %$params }, 'Hercules::Params' );
118
}
119
113 120
sub params_as_text {
114 121
  my ($self) = @_;
115 122
  my $params = $self->params;

+ 4 - 0
lib/Hercules/Utils.pm

@ -72,6 +72,10 @@ sub seconds_to_timeunits {
72 72
    last unless --$units;
73 73
  }
74 74
75
  if ($seconds > 5*60) {
76
    $res .= '+';
77
  }
78
75 79
  return $res;
76 80
}
77 81

+ 134 - 0
public/js/hercules.js

@ -200,4 +200,138 @@ jQuery(document).ready(function($) {
200 200
  $('.btn-go-back').click(function() {
201 201
    history.back();
202 202
  });
203
204
  $('#edit-job-name').keyup(function() {
205
    var newname = $( this ).val();
206
    if (newname.match(/[^\w\-_]/)) {
207
      formgroup = $(this).closest('.form-group');
208
      $( formgroup ).addClass('has-error');
209
      setTimeout(function(){
210
          $(formgroup).removeClass('has-error');
211
        }, 800);
212
      newname = newname.replace(/[^\w\-_]/g,'');
213
      $( this ).val( newname );
214
    }
215
  });
216
217
  $('#add-new-job').click(function() {
218
    url = '/job/new';
219
    location.href = url;
220
  });
221
222
  // Save job edit
223
  $('.btn-save-job-changes').click(function() {
224
    var data = {};
225
    data.name = $('#edit-job-name').val();
226
    var errors = 0;
227
    formgroup = $('#edit-job-name').closest('.form-group');
228
    $(formgroup).removeClass('has-error');
229
    if (data.name == '' || data.name.match(/[^\w\-_]/)) {
230
      $( formgroup ).addClass('has-error');
231
      errors++;
232
    }
233
234
    // cron group
235
    data.cron_group = $('#edit-job-crongroup option:selected').val();
236
237
    // class
238
    var useclass=$('[name=useclass]:checked').val();
239
    console.log('useclass: '+useclass);
240
    var usegroup = $('#edit-group-core_class').closest('.form-group');
241
    $(usegroup).removeClass('has-error');
242
    if (useclass == '' ) {
243
      $(usegroup).addClass('has-error');
244
      errors++; 
245
    } else if (useclass == 'core') {
246
      data.jobclass = $('#edit-group-core_class').val();
247
      if (data.jobclass == '') {
248
        $(usegroup).addClass('has-error');
249
        errors++;
250
      }
251
    } else if (useclass == 'nocore') {
252
      data.jobclass = $('#edit-job-class').val();
253
      if (data.jobclass == '') {
254
        $(usegroup).addClass('has-error');
255
        errors++;
256
      }
257
    }
258
    if (data.jobclass != '') {
259
      if (!data.jobclass.match(/^\w+(::\w+)*(::)?$/)) {
260
        $(usegroup).addClass('has-error');
261
        errors++;
262
      }
263
    }
264
265
    // params
266
    var params = $('#edit-job-params').val();
267
    $('#edit-job-params').closest('.form-group').removeClass('has-error');
268
    if (params != '') {
269
      var paramjson = '{'+params+'}';
270
      try {
271
        paramobj = JSON.parse( paramjson );
272
        data.params = params;
273
      } catch(e) {
274
        $('#edit-job-params').closest('.form-group').addClass('has-error'); 
275
        errors++;
276
      }
277
    }
278
279
    //run schedule
280
    var usesched=$('[name=runtype]:checked').val();
281
    var schdgroup=$('#edit-job-run-every').closest('.form-group');
282
    $(schdgroup).removeClass('has-error');
283
    if (usesched == '') {
284
      $(schdgroup).addClass('has-error');
285
      errors++;
286
    } else if ( usesched == 'every') {
287
      data.every = $('#edit-job-run-every').val();
288
      if (data.every == '') {
289
        $(schdgroup).addClass('has-error');
290
        errors++;
291
292
      } else if (!data.every.match(/^\d+[smhdwMy]?$/)) {
293
        $(schdgroup).addClass('has-error');
294
        errors++;
295
      }
296
    } else if ( usesched == 'cron' ) {
297
      data.cron = $('#edit-job-run-cron').val();
298
      if ( data.cron == '' ) {
299
        $(schdgroup).addClass('has-error');
300
        errors++;
301
        
302
      } else {
303
        var parts = data.cron.split(' ');
304
        if ( parts.length != 5 ) {
305
          $(schdgroup).addClass('has-error');
306
          errors++;
307
        } else {
308
          for (i=0; i < parts.length; i++) {
309
            if (!parts[i].match(/^(\*(\/\d+)?|\d+(,\d+)*)$/)) {
310
              $(schdgroup).addClass('has-error');
311
              errors++;
312
            }
313
          }
314
        }
315
      }
316
    }
317
    
318
    if ( errors === 0 ) {
319
      var url;
320
      var oldname = $('#edit-job-old-name').val();
321
      if ( oldname ) {
322
        url = '/job/'+oldname+'/save/';
323
      } else {
324
        url = '/job/add';
325
      }
326
      $('#spinner-modal').modal('show');
327
      $.post(url, data, function( res ) {
328
          $('#spinner-modal').modal('hide');
329
          url = '/job/'+data.name;
330
          location.href = url;
331
        }).fail(function() {
332
          $('#spinner-modal').modal('hide');
333
          alert('Operation failed');
334
        });
335
    }
336
  });
203 337
});

+ 10 - 4
templates/jobs/edit.html.ep

@ -2,7 +2,11 @@
2 2
3 3
<div class="row">
4 4
  <div class="col-lg-12">
5
    <h1 class='page-header'> Job - view '<%= $job->{name} %>'</h1>
5
% if ($job->{name} ) {
6
    <h1 class='page-header'> Job - edit '<%= $job->{name} %>'</h1>
7
% } else {
8
    <h1 class='page-header'> edit new job</h1>
9
% }
6 10
  </div>
7 11
</div>
8 12
@ -18,6 +22,8 @@
18 22
      </div>
19 23
      <div class="panel-body">
20 24
        <div class="form-group">
25
          <input type="hidden" id="edit-job-old-name"
26
              value="<%= $job->{name} %>" />
21 27
          <label>Job Name</label>
22 28
          <input class="form-control" id="edit-job-name"
23 29
            value="<%= $job->{name} %>" />
@ -103,7 +109,7 @@
103 109
                      ?'checked=1':'' %>
104 110
                /> Run schedule
105 111
            </span>
106
            <input class="form-control" id="edit-job-run-every"
112
            <input class="form-control" id="edit-job-run-cron"
107 113
              value="<%= $job->{run_schedule} %>" />
108 114
          </div>
109 115
          <p class="help-block">Use a cron schedule definition
@ -115,8 +121,8 @@
115 121
        </div>
116 122
      </div>
117 123
      <div class="panel-footer hercules-table-actions">
118
        <button type="button" class="btn btn-secondary">Cancel</button>
119
        <button type="button" class="btn btn-primary">Update</button>
124
        <button type="button" class="btn btn-secondary btn-go-back">Cancel</button>
125
        <button type="button" class="btn btn-primary btn-save-job-changes">Update</button>
120 126
      </table>
121 127
    </div>
122 128
  </div>