colonel 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/.gitignore +18 -0
  2. data/.rvmrc +5 -0
  3. data/.yardopts +7 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +66 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +35 -0
  8. data/Rakefile +4 -0
  9. data/bin/colonel +12 -0
  10. data/colonel.gemspec +26 -0
  11. data/lib/colonel.rb +18 -0
  12. data/lib/colonel/array.rb +9 -0
  13. data/lib/colonel/builder.rb +55 -0
  14. data/lib/colonel/crontab.rb +73 -0
  15. data/lib/colonel/job.rb +31 -0
  16. data/lib/colonel/parser.rb +248 -0
  17. data/lib/colonel/server.rb +74 -0
  18. data/lib/colonel/server/assets/coffeescript/application.coffee +64 -0
  19. data/lib/colonel/server/coffee_engine.rb +7 -0
  20. data/lib/colonel/server/helpers/roots_helper.rb +25 -0
  21. data/lib/colonel/server/helpers/views_helper.rb +32 -0
  22. data/lib/colonel/server/public/images/glyphicons-halflings-white.png +0 -0
  23. data/lib/colonel/server/public/images/glyphicons-halflings.png +0 -0
  24. data/lib/colonel/server/public/javascripts/bootstrap.js +2268 -0
  25. data/lib/colonel/server/public/javascripts/bootstrap.min.js +6 -0
  26. data/lib/colonel/server/public/javascripts/jquery-1.9.1.min.js +5 -0
  27. data/lib/colonel/server/public/stylesheets/application.css +42 -0
  28. data/lib/colonel/server/public/stylesheets/bootstrap-responsive.css +1109 -0
  29. data/lib/colonel/server/public/stylesheets/bootstrap-responsive.min.css +9 -0
  30. data/lib/colonel/server/public/stylesheets/bootstrap.css +6158 -0
  31. data/lib/colonel/server/public/stylesheets/bootstrap.min.css +9 -0
  32. data/lib/colonel/server/views/edit.haml +6 -0
  33. data/lib/colonel/server/views/form.haml +51 -0
  34. data/lib/colonel/server/views/index.haml +32 -0
  35. data/lib/colonel/server/views/layout.haml +12 -0
  36. data/lib/colonel/server/views/new.haml +6 -0
  37. data/lib/colonel/version.rb +3 -0
  38. data/pkg/colonel-0.1.1.gem +0 -0
  39. data/pkg/colonel-0.1.2.gem +0 -0
  40. data/pkg/colonel-0.1.3.gem +0 -0
  41. data/pkg/colonel-0.1.gem +0 -0
  42. data/screenshots/job_deleting.png +0 -0
  43. data/screenshots/job_editing.png +0 -0
  44. data/screenshots/jobs_list.png +0 -0
  45. data/screenshots/new_job_validating.png +0 -0
  46. metadata +194 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.rbc
2
+ *.sassc
3
+ .sass-cache
4
+ capybara-*.html
5
+ .rspec
6
+ /.bundle
7
+ /vendor/bundle
8
+ /log/*
9
+ /tmp/*
10
+ /db/*.sqlite3
11
+ /public/system/*
12
+ /coverage/
13
+ /spec/tmp/*
14
+ **.orig
15
+ rerun.txt
16
+ pickle-email-*.html
17
+ .idea/
18
+ .DS_Store
data/.rvmrc ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+
3
+ rvm_trust_rvmrcs_flag=1
4
+ rvm_gemset_create_on_use_flag=1
5
+ rvm use ruby-1.9.3@colonel
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --markup markdown
2
+ --markup-provider redcarpet
3
+ --charset utf-8
4
+ --readme README.md
5
+ -
6
+ README.md
7
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sinatra'
4
+ gem 'actionpack'
5
+ gem 'shotgun'
6
+ gem 'thin'
7
+ gem 'sqlite3'
8
+ gem 'vegas'
9
+ gem 'rake'
10
+
11
+ # sinatra extensions
12
+ gem 'sinatra-authorization'
13
+
14
+ #front-end
15
+ gem 'haml'
16
+ gem 'coffee-script'
17
+
18
+ # Specify your gem's dependencies in rand.gemspec
19
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,66 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ colonel (0.1.4)
5
+ coffee-script
6
+ haml
7
+ sinatra (>= 0.9.2)
8
+ vegas (~> 0.1.2)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ actionpack (2.3.2)
14
+ activesupport (= 2.3.2)
15
+ activesupport (2.3.2)
16
+ coffee-script (2.2.0)
17
+ coffee-script-source
18
+ execjs
19
+ coffee-script-source (1.4.0)
20
+ daemons (1.1.9)
21
+ eventmachine (1.0.0)
22
+ execjs (1.4.0)
23
+ multi_json (~> 1.0)
24
+ haml (4.0.0)
25
+ tilt
26
+ multi_json (1.6.1)
27
+ rack (1.5.2)
28
+ rack-protection (1.3.2)
29
+ rack
30
+ rake (10.0.3)
31
+ redcarpet (1.17.2)
32
+ shotgun (0.9)
33
+ rack (>= 1.0)
34
+ sinatra (1.3.4)
35
+ rack (~> 1.4)
36
+ rack-protection (~> 1.3)
37
+ tilt (~> 1.3, >= 1.3.3)
38
+ sinatra-authorization (1.0.0)
39
+ sinatra (>= 0.9.1.1)
40
+ sqlite3 (1.3.7)
41
+ thin (1.5.0)
42
+ daemons (>= 1.0.9)
43
+ eventmachine (>= 0.12.6)
44
+ rack (>= 1.0.0)
45
+ tilt (1.3.3)
46
+ vegas (0.1.11)
47
+ rack (>= 1.0.0)
48
+ yard (0.7.5)
49
+
50
+ PLATFORMS
51
+ ruby
52
+
53
+ DEPENDENCIES
54
+ actionpack
55
+ coffee-script
56
+ colonel!
57
+ haml
58
+ rake
59
+ redcarpet (~> 1.17)
60
+ shotgun
61
+ sinatra
62
+ sinatra-authorization
63
+ sqlite3
64
+ thin
65
+ vegas
66
+ yard (~> 0.7.5)
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 santaux
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ Colonel
2
+ =======
3
+
4
+ Gem for cron jobs editing by web GUI. Web GUI is based on Sinatra, Vegas
5
+ and Twitter Bootstrap.
6
+
7
+ ## Usage
8
+
9
+ Add colonel to your Gemfile:
10
+
11
+ gem 'colonel', :git => 'git@github.com:santaux/colonel.git'
12
+
13
+ Install it:
14
+
15
+ bundle
16
+
17
+ Run application into your terminal and use:
18
+
19
+ bundle exec colonel
20
+
21
+ Stop application when you want:
22
+
23
+ bundle exec colonel -K
24
+
25
+ Or use it directly through your irb/rails console:
26
+
27
+ require 'colonel'
28
+ builder = Colonel::Builder.new
29
+ jobs = builder.parse
30
+
31
+ ## Screenshots
32
+ ![Jobs list](https://github.com/santaux/colonel/raw/master/screenshots/jobs_list.png "jobs list")
33
+ ![Job editing](https://github.com/santaux/colonel/raw/master/screenshots/job_editing.png "job editing")
34
+ ![Job deleting](https://github.com/santaux/colonel/raw/master/screenshots/job_deleting.png "job deleting")
35
+ ![New job validating](https://github.com/santaux/colonel/raw/master/screenshots/new_job_validating.png "new job validating")
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'yard'
3
+
4
+ YARD::Rake::YardocTask.new
data/bin/colonel ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ begin
5
+ require 'vegas'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'vegas'
9
+ end
10
+ require 'colonel/server'
11
+
12
+ Vegas::Runner.new(Colonel::Server, 'colonel')
data/colonel.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'colonel/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "colonel"
8
+ gem.version = Colonel::VERSION
9
+ gem.authors = ["santaux"]
10
+ gem.email = ["santaux@gmail.com"]
11
+ gem.description = %q{Gem for managing cron jobs with web GUI}
12
+ gem.summary = %q{Gem for managing cron jobs with web GUI}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "vegas", "~> 0.1.2"
21
+ gem.add_dependency "sinatra", ">= 0.9.2"
22
+ gem.add_dependency "haml"
23
+ gem.add_dependency "coffee-script"
24
+ gem.add_development_dependency "redcarpet", "~> 1.17"
25
+ gem.add_development_dependency "yard", "~> 0.7.5"
26
+ end
data/lib/colonel.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "colonel/version"
2
+ require 'colonel/array'
3
+
4
+ # TODO: Rewrite views with helpers
5
+ # TODO: Add ActiveModel methods to classes
6
+ # TODO: Add ability to import/export crontab files
7
+ # TODO: Move it to gem as rails engine
8
+ # TODO: Make import/output of templates
9
+ module Colonel
10
+ require 'colonel/builder'
11
+ require 'colonel/crontab'
12
+ require 'colonel/job'
13
+ require 'colonel/parser'
14
+ end
15
+
16
+ class Array
17
+ include Colonel::Array
18
+ end
@@ -0,0 +1,9 @@
1
+ module Colonel
2
+ module Array
3
+ %w[second third fourth fifth].each_with_index do |name,index|
4
+ define_method name do
5
+ self[index+1]
6
+ end unless method_defined? name
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,55 @@
1
+ # example:
2
+ # builder = Colonel::Builder.new
3
+ # jobs = builder.parse
4
+ module Colonel
5
+ class Builder
6
+ def initialize(opts={})
7
+ Job.clear_amount
8
+ @all_flag = opts[:all_flag].nil? ? true : opts[:all_flag]
9
+ end
10
+
11
+ def user
12
+ @_user ||= `whoami`.chomp
13
+ end
14
+
15
+ def crontab
16
+ @_crontab ||= Crontab.new(user)
17
+ end
18
+
19
+ def parse
20
+ @jobs ||= []
21
+ crontab.reject_useless.each do |line|
22
+ @jobs << Parser.new(line, :all_flag => @all_flag).execute
23
+ end
24
+ @jobs
25
+ end
26
+
27
+ def update_crontab
28
+ crontab.update(@jobs)
29
+ end
30
+
31
+ def find_job(id)
32
+ @jobs.select { |j| j.id == id.to_i }.first
33
+ end
34
+
35
+ def get_job_index(id)
36
+ @jobs.each_with_index { |j,i| return i if j.id == id.to_i }
37
+ end
38
+
39
+ # TODO: Refactor it! To complicated!
40
+ def update_job(opts={})
41
+ index = get_job_index(opts[:id])
42
+ job = find_job(opts[:id])
43
+ @jobs[index] = job.update(opts)
44
+ end
45
+
46
+ def add_job(opts={})
47
+ @jobs << Job.new( Parser::Schedule.new(opts[:schedule]), Parser::Command.new(opts[:command]))
48
+ end
49
+
50
+ def destroy_job(id)
51
+ index = get_job_index(id)
52
+ @jobs.delete_at index
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ require 'tempfile'
2
+
3
+ module Colonel
4
+ class Crontab
5
+ def initialize(user)
6
+ @user = user
7
+ end
8
+
9
+ def read
10
+ @tab = `crontab -l`
11
+ end
12
+
13
+ def lines
14
+ @_lines ||= read.split(/\n/)
15
+ end
16
+
17
+ def reject_useless
18
+ lines.reject { |line| reject_if_wrong(line) }
19
+ end
20
+
21
+ def reject_if_wrong(line)
22
+ !line.match /^(\d+|\*)/
23
+ end
24
+
25
+ def update(jobs)
26
+ update_tab(jobs)
27
+ write
28
+ end
29
+
30
+ def update_tab(jobs)
31
+ tab_lines = jobs.map do |j|
32
+ [
33
+ TimeCreator.new(
34
+ minutes: j.schedule.minutes,
35
+ hours: j.schedule.hours,
36
+ days: j.schedule.days,
37
+ months: j.schedule.months,
38
+ weekdays: j.schedule.weekdays,
39
+ ).generate,
40
+ j.command.get
41
+ ].join("\t\t\t")
42
+ end
43
+ tab_lines
44
+ @tab = "# --- Updated with Colonel ---\n" + tab_lines.join("\n") + "\n# --- \n"
45
+ end
46
+
47
+ def write_to_temp
48
+ @tempfile = Tempfile.new("#{@user}_crontab_temp")
49
+ @tempfile.write(@tab)
50
+ @tempfile.close
51
+ end
52
+
53
+ def write
54
+ write_to_temp
55
+ `crontab #{@tempfile.path}`
56
+ @tempfile.delete
57
+ end
58
+
59
+ class TimeCreator
60
+ def initialize(opts={})
61
+ @minutes = opts[:minutes]
62
+ @hours = opts[:hours]
63
+ @days = opts[:days]
64
+ @months = opts[:months]
65
+ @weekdays = opts[:weekdays]
66
+ end
67
+
68
+ def generate
69
+ [@minutes, @hours, @days, @months, @weekdays].map { |t| t.is_a?(Array) ? t.join(",") : t }.join(" ")#.join("\t")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ module Colonel
2
+ class Job
3
+
4
+ attr_reader :id, :schedule, :command
5
+
6
+ def initialize(schedule, command)
7
+ @schedule = schedule
8
+ @command = command
9
+
10
+ increase_amount
11
+ @id = @@amount
12
+ end
13
+
14
+ def self.clear_amount
15
+ @@amount = 0
16
+ end
17
+
18
+ def update(opts={})
19
+ @schedule = Parser::Schedule.new(opts[:schedule])
20
+ @command = Parser::Command.new(opts[:command])
21
+
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ def increase_amount
28
+ @@amount = @@amount ? @@amount+1 : 1
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,248 @@
1
+ module Colonel
2
+ class Parser
3
+
4
+ SCHEDULE_SIZE = 5
5
+ PERIOD = /@hourly|@daily|@weekly|@monthly|@yearly/
6
+
7
+ def initialize(text, opts={})
8
+ @all_flag = opts[:all_flag].nil? ? true : opts[:all_flag]
9
+ filtered_text = replace_periods(text)
10
+ @tokens = scan_tokens(filtered_text)
11
+ end
12
+
13
+ def scan_tokens(text)
14
+ text.scan(/[\S]+/)
15
+ end
16
+
17
+ def replace_periods(text)
18
+ period = text.scan(PERIOD).first
19
+ if period
20
+ time_string = case period
21
+ when "@hourly"
22
+ "0 * * * *"
23
+ when "@daily"
24
+ "0 0 * * *"
25
+ when "@weekly"
26
+ "0 0 * * 0"
27
+ when "@monthly"
28
+ "0 0 1 * *"
29
+ when "@yearly"
30
+ "0 0 1 1 *"
31
+ end
32
+ text.gsub(/^(#{PERIOD})/, time_string)
33
+ else
34
+ text
35
+ end
36
+ end
37
+
38
+ def schedule_tokens
39
+ @tokens.first(SCHEDULE_SIZE)
40
+ end
41
+
42
+ def command_tokens
43
+ @tokens.last(@tokens.size - SCHEDULE_SIZE)
44
+ end
45
+
46
+ def execute
47
+ Job.new Schedule.new(schedule_tokens: schedule_tokens, all_flag: @all_flag), Command.new(command_tokens)
48
+ end
49
+
50
+ class Command
51
+ def initialize(data)
52
+ if data.is_a?(Array)
53
+ @command_tokens = data
54
+ else
55
+ @_string = data
56
+ end
57
+ end
58
+
59
+ def get
60
+ @_string ||= @command_tokens.join(' ')
61
+ end
62
+
63
+ def exist?
64
+ @_string.size > 0
65
+ end
66
+ end
67
+
68
+ # Class to store parsed time fields values
69
+ class Schedule
70
+
71
+ def initialize(opts={})
72
+ @schedule_tokens = opts[:schedule_tokens]
73
+ @all_flag = opts[:all_flag].nil? ? true : opts[:all_flag]
74
+
75
+ @_minutes = opts[:minutes]
76
+ @_hours = opts[:hours]
77
+ @_days = opts[:days]
78
+ @_months = opts[:months]
79
+ @_weekdays = opts[:weekdays]
80
+ end
81
+
82
+ def minutes
83
+ @_minutes ||= TimeParser.new(@schedule_tokens.first, :minute, @all_flag).result
84
+ end
85
+
86
+ def hours
87
+ @_hours ||= TimeParser.new(@schedule_tokens.second, :hour, @all_flag).result
88
+ end
89
+
90
+ def days
91
+ @_days ||= TimeParser.new(@schedule_tokens.third, :day, @all_flag).result
92
+ end
93
+
94
+ def months
95
+ @_months ||= TimeParser.new(@schedule_tokens.fourth, :month, @all_flag).result
96
+ end
97
+
98
+ def weekdays
99
+ @_weekdays ||= TimeParser.new(@schedule_tokens.fifth, :weekday, @all_flag).result
100
+ end
101
+
102
+ %w[minutes hours days months weekdays].each do |method|
103
+ define_method "#{method}_string" do
104
+ result = send(method)
105
+ result.is_a?(Array) ? result.join(', ') : result.to_s
106
+ end
107
+ end
108
+ end
109
+
110
+ # Class to parse time fields from crontab line.
111
+ class TimeParser
112
+ DIGIT = /\d{1,2}/
113
+ ALL = /\*/
114
+ COMMA = /\,/
115
+ DASH = /\-/
116
+ RANGE = /#{DIGIT}#{DASH}#{DIGIT}/
117
+ DEVIDE = /#{ALL}\/#{DIGIT}|#{RANGE}\/#{DIGIT}/
118
+ DEVIDER = /\//
119
+
120
+ def tokens
121
+ @tokens
122
+ end
123
+
124
+ # if all_flag is true -> return :all result if all?
125
+ def initialize(time_expression, time_type, all_flag=true)
126
+ @time_type = time_type
127
+ @tokens = time_expression.scan(/#{COMMA}|#{DEVIDE}|#{RANGE}|#{DIGIT}|#{ALL}/)
128
+ @all_flag = all_flag
129
+ end
130
+
131
+ def ast
132
+ @_ast ||= expression
133
+ end
134
+
135
+ def result
136
+ unique = ast.flatten.uniq
137
+ unique.pop if unique.last.nil? #remove nil element
138
+ if all?(unique) && @all_flag
139
+ :all
140
+ else
141
+ unique
142
+ end
143
+ end
144
+
145
+ def min_time_value
146
+ @_min_time_value ||= case @time_type
147
+ when :minute
148
+ 0
149
+ when :hour
150
+ 0
151
+ when :day
152
+ 1
153
+ when :month
154
+ 1
155
+ when :weekday
156
+ 0
157
+ end
158
+ end
159
+
160
+ def max_time_value
161
+ @_max_time_value ||= case @time_type
162
+ when :minute
163
+ 59
164
+ when :hour
165
+ 23
166
+ when :day
167
+ 31
168
+ when :month
169
+ 12
170
+ when :weekday
171
+ 6
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ def next_token
178
+ @tokens.shift
179
+ end
180
+
181
+ def next_token_safe
182
+ @tokens.first
183
+ end
184
+
185
+ def second_token_safe
186
+ @tokens.second
187
+ end
188
+
189
+ def expression
190
+ token = next_token
191
+
192
+ case token
193
+ when nil
194
+ return nil
195
+ when DEVIDE
196
+ return [devide(token), expression]
197
+ when RANGE
198
+ return [range(token), expression]
199
+ when DIGIT
200
+ return [digit(token), expression]
201
+ when ALL
202
+ return [all(token), expression]
203
+ when COMMA
204
+ return expression
205
+ else
206
+ raise "Unknown token: #{token}!"
207
+ end
208
+ end
209
+
210
+ def digit(token)
211
+ return token
212
+ end
213
+
214
+ def all(token)
215
+ return (min_time_value..max_time_value).to_a
216
+ end
217
+
218
+ def devide(token)
219
+ case token
220
+ when /#{RANGE}#{DEVIDER}#{DIGIT}/
221
+ up, bottom = token.split(DEVIDER)
222
+ range(up).select do |x|
223
+ x % bottom.to_i == 0
224
+ end
225
+ when /#{ALL}#{DEVIDER}#{DIGIT}/
226
+ up, bottom = token.split(DEVIDER)
227
+ (min_time_value..max_time_value).select do |x|
228
+ x % bottom.to_i == 0
229
+ end
230
+ end
231
+ end
232
+
233
+ def range(token)
234
+ min, max = token.split(DASH)
235
+ (min..max).to_a
236
+ end
237
+
238
+ def correct?(array)
239
+ array.all? { |el| el <= max_time_value } and array.all? { |el| el >= min_time_value }
240
+ end
241
+
242
+ def all?(array)
243
+ array.count-1 == max_time_value-min_time_value
244
+ end
245
+
246
+ end
247
+ end
248
+ end