whenever_systemd 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7074b14d9e4bafc762973076ad3a224ab09243736b19d0742658bbb29c78c142
4
+ data.tar.gz: b1ddc106d768eed86e517ea59a011cb8b188f6420e5e4f55a1052b31618d3e44
5
+ SHA512:
6
+ metadata.gz: 3ba3be14419e7c72cc0f151a577105173072de22bb11b4053e896cfbb897164739d597606b3b7cf1734f07877a3de8968aba4179f12490082753e17189307e08
7
+ data.tar.gz: e61467600d7cb1d954c1efdfc71749a8c514b7e52b18cf35cc75573bf10a037e96dda5a6a69f6999381b6ac6b1d9c618f2a460fbf431f03f8d39f0f597a3cff7
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ pkg
3
+ doc
4
+ .*.sw[a-z]
5
+ Gemfile.lock
6
+ gemfiles/*.lock
7
+ .ruby-version
data/.travis.yml ADDED
@@ -0,0 +1,21 @@
1
+ language: ruby
2
+
3
+ before_install:
4
+ - gem install bundler
5
+ - unset _JAVA_OPTIONS
6
+ rvm:
7
+ - 2.4.6
8
+ - 2.5.5
9
+ - 2.6.3
10
+ - jruby-9.2.6.0
11
+
12
+ gemfile:
13
+ - gemfiles/activesupport4.1.gemfile
14
+ - gemfiles/activesupport4.2.gemfile
15
+ - gemfiles/activesupport5.0.gemfile
16
+ - gemfiles/activesupport5.1.gemfile
17
+ - gemfiles/activesupport5.2.gemfile
18
+
19
+ env:
20
+ global:
21
+ - JRUBY_OPTS=--debug
data/Appraisals ADDED
@@ -0,0 +1,19 @@
1
+ appraise 'activesupport4.1' do
2
+ gem "activesupport", "~> 4.1.0"
3
+ end
4
+
5
+ appraise 'activesupport4.2' do
6
+ gem "activesupport", "~> 4.2.0"
7
+ end
8
+
9
+ appraise 'activesupport5.0' do
10
+ gem "activesupport", "~> 5.0.0"
11
+ end
12
+
13
+ appraise 'activesupport5.1' do
14
+ gem "activesupport", "~> 5.1.0"
15
+ end
16
+
17
+ appraise 'activesupport5.2' do
18
+ gem "activesupport", "~> 5.2.0beta2"
19
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ### 0.0.1 / October 27th, 2020
2
+
3
+ * Forked from whenever [Anton Semenov]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in whenever.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2017 Javan Makhmali
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+
2
+ WheneverSystemd is a fork of the gem [Whenever](https://github.com/javan/whenever), which generates & installs
3
+ [systemd timers](https://www.freedesktop.org/software/systemd/man/systemd.timer.html#) from a similar `schedule.rb` file.
4
+
5
+ Note: By some reasons there is no tests yet, if you want to add them, you are welcome.
6
+
7
+ ### Installation
8
+
9
+ ```sh
10
+ $ gem install whenever_systemd
11
+ ```
12
+
13
+ Or with Bundler in your Gemfile.
14
+
15
+ ```ruby
16
+ gem 'whenever_systemd', require: false
17
+ ```
18
+
19
+ ### Getting started
20
+
21
+ ```sh
22
+ $ cd /apps/my-great-project
23
+ $ bundle exec wheneverize .
24
+ ```
25
+
26
+ This will create an initial `config/schedule.rb` file for you (as long as the config folder is already present in your project).
27
+
28
+ ### The `whenever_systemd` command
29
+
30
+ The `whenever_systemd` command will simply show you your `schedule.rb` file converted to cron syntax. It does not read or write your systemd units.
31
+
32
+ ```sh
33
+ $ cd /apps/my-great-project
34
+ $ bundle exec whenever_systemd
35
+ ```
36
+
37
+ To write unit files for your jobs, execute this command:
38
+
39
+ ```sh
40
+ $ whenever_systemd --update-units
41
+ ```
42
+
43
+ Other commonly used options include:
44
+ ```sh
45
+ $ whenever_systemd --load-file config/my_schedule.rb # set the schedule file
46
+ $ whenever_systemd --install-path '/usr/lib/systemd/system/' # install units to specific dir
47
+ ```
48
+
49
+ ### Example schedule.rb file
50
+
51
+ **Note the difference with whenever schedule.rb:**
52
+
53
+ You should provide a name to your job in the first argument, i.e.:
54
+
55
+ ```ruby
56
+ # instead of:
57
+ runner "MyModel.some_process"
58
+
59
+ # With whenever_systemd:
60
+ runner "mymodel-some_process", "MyModel.some_process"
61
+ ```
62
+
63
+ So, here is an example:
64
+
65
+ ```ruby
66
+ set :prefix, "myproject"
67
+ set :timer, { accuracy_sec: "1m" } # timer options
68
+ set :install, { wanted_by: "timers.target" } # project timers target
69
+
70
+ every 3.hours do # 1.minute 1.day 1.week 1.month 1.year is also supported
71
+ runner "mymodel-some_process", "MyModel.some_process"
72
+ rake "myrake-task", "my:rake:task"
73
+ command "my_great_command", "/usr/bin/my_great_command"
74
+ end
75
+
76
+ # Helpers: minutely, hourly, daily, monthly, yearly, quarterly, semiannually
77
+ # See: https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events
78
+ minutely do
79
+ runner "SomeModel.ladeeda"
80
+ end
81
+
82
+ # +every+ helper eats any calendar syntax described in the link above:
83
+ every '*:1/15' do # Every 15 minutes, starting from 01, i.e.: 01,16,31,46
84
+ runner "mymode-task_to_run_in_15m", "Mymodel.task_to_run_in_15m"
85
+ end
86
+
87
+ # Folded blocks:
88
+ daily do
89
+ at '00:00' do # run every day at 00:00
90
+ runner "task-do_something_great", "Task.do_something_great"
91
+ rake "app_server-task", "app_server:task"
92
+ end
93
+ end
94
+
95
+ weekly 'Sun' do
96
+ at '4:30' do # Run every Sunday at 04:30
97
+ runner "mymodel-sunday_task", "Mymodel.sunday_task"
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### Define your own job types
103
+
104
+ Whenever ships with three pre-defined job types: command, runner, and rake. You can define your own with `job_type`.
105
+
106
+ For example:
107
+
108
+ ```ruby
109
+ job_type :awesome, '/usr/local/bin/awesome :task :fun_level'
110
+
111
+ every 2.hours do
112
+ awesome "awesome-party", "party", fun_level: "extreme"
113
+ end
114
+ ```
115
+
116
+ Would run `/usr/local/bin/awesome party extreme` every two hours. `:task` is always replaced with the first argument, and any additional `:whatevers` are replaced with the options passed in or by variables that have been defined with `set`.
117
+
118
+ The default job types that ship with Whenever are defined like so:
119
+
120
+ ```ruby
121
+ job_type :command, ":task :output"
122
+ job_type :rake, "cd :path && :environment_variable=:environment bundle exec rake :task --silent :output"
123
+ job_type :runner, "cd :path && bin/rails runner -e :environment ':task' :output"
124
+ job_type :script, "cd :path && :environment_variable=:environment bundle exec script/:task :output"
125
+ ```
126
+
127
+ If a `:path` is not set it will default to the directory in which `whenever` was executed. `:environment_variable` will default to 'RAILS_ENV'. `:environment` will default to 'production'. `:output` will be replaced with your output redirection settings which you can read more about here: <http://github.com/javan/whenever/wiki/Output-redirection-aka-logging-your-cron-jobs>
128
+
129
+ All jobs are by default run with `bash -l -c 'command...'`. Among other things, this allows your cron jobs to play nice with RVM by loading the entire environment instead of cron's somewhat limited environment. Read more: <http://blog.scoutapp.com/articles/2010/09/07/rvm-and-cron-in-production>
130
+
131
+ You can change this by setting your own `:job_template`.
132
+
133
+ ```ruby
134
+ set :job_template, "bash -l -c ':job'"
135
+ ```
136
+
137
+ Or set the job_template to nil to have your jobs execute normally.
138
+
139
+ ```ruby
140
+ set :job_template, nil
141
+ ```
142
+
143
+ ### Credit
144
+
145
+ WheneverSystemd is forked from Whenever not by a glory seeker, so I just copy the original credits:
146
+
147
+ Whenever was created for use at Inkling (<http://inklingmarkets.com>). Their take on it: <http://blog.inklingmarkets.com/2009/02/whenever-easy-way-to-do-cron-jobs-from.html>
148
+
149
+ Thanks to all the contributors who have made it even better: <http://github.com/javan/whenever/contributors>
150
+
151
+ Copyright &copy; 2017 Javan Makhmali
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ # require 'rake/testtask'
3
+ #
4
+ # Rake::TestTask.new(:test) do |test|
5
+ # test.libs << 'lib' << 'test'
6
+ # test.pattern = 'test/{functional,unit}/**/*_test.rb'
7
+ # test.verbose = true
8
+ # end
9
+ #
10
+ # task :default => :test
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'whenever_systemd'
5
+ require 'whenever_systemd/version'
6
+
7
+ options = {}
8
+
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: whenever [options]"
11
+
12
+ opts.on('-p', '--prefix [unit prefix]', 'Unit prefix') do |prefix|
13
+ options[:prefix] = prefix
14
+ end
15
+
16
+ opts.on('-d', '--dir [install path]',
17
+ 'Unit Search Path',
18
+ " Default: #{WheneverSystemd::DEFAULT_INSTALL_PATH}") do |path|
19
+ options[:install_path] = path
20
+ end
21
+
22
+ opts.on('-i', '--update-units [schedule]',
23
+ 'Install the schedule units.',
24
+ ' Default: full path to schedule.rb file') do |identifier|
25
+ options[:update] = true
26
+ options[:identifier] = identifier if identifier
27
+ end
28
+
29
+ opts.on('-c', '--clear-units [schedule]',
30
+ 'Uninstall units',
31
+ ' Default: full path to schedule.rb file') do |identifier|
32
+ options[:clear] = true
33
+ options[:identifier] = identifier if identifier
34
+ end
35
+
36
+ opts.on('-s', '--set [variables]', 'Example: --set \'environment=staging&path=/my/sweet/path\'') do |set|
37
+ options[:set] = set if set
38
+ end
39
+
40
+ opts.on('-f', '--load-file [schedule file]', 'Default: config/schedule.rb') do |file|
41
+ options[:file] = file if file
42
+ end
43
+
44
+ opts.on('-u', '--user [user]', 'Default: current user') do |user|
45
+ options[:user] = user if user
46
+ end
47
+
48
+ opts.on('-r', '--roles [role1,role2]', 'Comma-separated list of server roles to generate cron jobs for') do |roles|
49
+ options[:roles] = roles.split(',').map(&:to_sym) if roles
50
+ end
51
+
52
+ opts.on('-S', '--dry-run', 'Only print script which will be executed') do |c|
53
+ options[:dry] = true
54
+ end
55
+
56
+ opts.on("--sudo", "Use sudo with operations") do
57
+ options[:sudo] = true
58
+ end
59
+
60
+ opts.on("--script [script]", "Use custom script to perform action") do |v|
61
+ options[:custom_script] = File.expand_path(v)
62
+ end
63
+
64
+ opts.on('-v', '--version') { puts "WheneverSystemd v#{WheneverSystemd::VERSION}"; exit(0) }
65
+ end.parse!
66
+
67
+ WheneverSystemd::CommandLine.execute(options)
data/bin/wheneverize ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file is based heavily on Capistrano's `capify` command
4
+
5
+ require 'optparse'
6
+ require 'fileutils'
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: #{File.basename($0)} [path]"
10
+
11
+ begin
12
+ opts.parse!(ARGV)
13
+ rescue OptionParser::ParseError => e
14
+ warn e.message
15
+ puts opts
16
+ exit 1
17
+ end
18
+ end
19
+
20
+ unless ARGV.empty?
21
+ if !File.exist?(ARGV.first)
22
+ abort "`#{ARGV.first}' does not exist."
23
+ elsif !File.directory?(ARGV.first)
24
+ abort "`#{ARGV.first}' is not a directory."
25
+ elsif ARGV.length > 1
26
+ abort "Too many arguments; please specify only the directory to wheneverize."
27
+ end
28
+ end
29
+
30
+ content = <<-FILE
31
+ # Use this file to easily define all of your cron jobs.
32
+ #
33
+ # It's helpful, but not entirely necessary to understand cron before proceeding.
34
+ # http://en.wikipedia.org/wiki/Cron
35
+
36
+ # Example:
37
+ #
38
+ # set :output, "/path/to/my/cron_log.log"
39
+ #
40
+ # every 2.hours do
41
+ # command "/usr/bin/some_great_command"
42
+ # runner "MyModel.some_method"
43
+ # rake "some:great:rake:task"
44
+ # end
45
+ #
46
+ # every 4.days do
47
+ # runner "AnotherModel.prune_old_records"
48
+ # end
49
+
50
+ # Learn more: http://github.com/javan/whenever
51
+ FILE
52
+
53
+ file = 'config/schedule.rb'
54
+ base = ARGV.empty? ? '.' : ARGV.shift
55
+
56
+ file = File.join(base, file)
57
+ if File.exist?(file)
58
+ warn "[skip] `#{file}' already exists"
59
+ elsif File.exist?(file.downcase)
60
+ warn "[skip] `#{file.downcase}' exists, which could conflict with `#{file}'"
61
+ else
62
+ dir = File.dirname(file)
63
+ if !File.exist?(dir)
64
+ warn "[add] creating `#{dir}'"
65
+ FileUtils.mkdir_p(dir)
66
+ end
67
+ puts "[add] writing `#{file}'"
68
+ File.open(file, "w") { |f| f.write(content) }
69
+ end
70
+
71
+ puts "[done] wheneverized!"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "activesupport", "~> 5.2"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module WheneverSystemd
7
+ class CommandLine
8
+ def self.execute(options={})
9
+ new(options).run
10
+ end
11
+
12
+ def initialize(options={})
13
+ @options = options
14
+
15
+ @options[:install_path] ||= WheneverSystemd::DEFAULT_INSTALL_PATH
16
+ @options[:temp_path] ||= "#{ENV['HOME']}/tmp/whenever-#{Time.now.to_i}"
17
+ @options[:file] ||= "config/schedule.rb"
18
+ @options[:cut] ||= 0
19
+ @options[:identifier] ||= default_identifier
20
+
21
+ if !File.exist?(@options[:file]) && @options[:clear].nil?
22
+ warn("[fail] Can't find file: #{@options[:file]}")
23
+ exit(1)
24
+ end
25
+ end
26
+
27
+ def run
28
+ if @options[:dry]
29
+ dry_run
30
+ elsif @options[:clear]
31
+ clear_units
32
+ elsif @options[:update]
33
+ update_units
34
+ else
35
+ show_units
36
+ exit(0)
37
+ end
38
+ end
39
+
40
+ def show_units
41
+ puts job_list.dry_units(@options[:install_path])
42
+ puts "## [message] Above is your schedule file converted to systemd units."
43
+ puts "## [message] Your active units was not updated."
44
+ puts "## [message] Run `whenever --help' for more options."
45
+ end
46
+
47
+ def dry_run
48
+ if @options[:clear]
49
+ puts job_list.generate_clear_script(@options[:install_path])
50
+ elsif @options[:update]
51
+ puts job_list.generate_update_script(@options[:install_path])
52
+ else
53
+ show_units
54
+ end
55
+ exit(0)
56
+ end
57
+
58
+ def update_units
59
+ make_and_run_script(:update_units)
60
+ end
61
+
62
+ def clear_units
63
+ make_and_run_script(:clear_units)
64
+ end
65
+
66
+ def job_list
67
+ @job_list ||= JobList.new(@options)
68
+ end
69
+
70
+ private
71
+
72
+ def make_and_run_script(name)
73
+ script_name = [@options[:prefix], name].compact.join(?-)
74
+
75
+ script_path = make_script(script_name) do
76
+ job_list.generate_update_script(@options[:install_path])
77
+ end
78
+
79
+ cmd =
80
+ if @options[:custom_script]
81
+ binding.eval(Pathname(@options[:custom_script]).read)
82
+ else
83
+ sudo_if_need(script_path)
84
+ end
85
+
86
+ system(cmd)
87
+ end
88
+
89
+ def sudo_if_need(*cmd)
90
+ Shellwords.join(@options[:sudo] ? ["sudo", "bash", *cmd] : ["bash", *cmd])
91
+ end
92
+
93
+ def make_script(name)
94
+ FileUtils.mkdir_p(@options[:temp_path])
95
+ script_file = Pathname(@options[:temp_path])/"#{name}.sh"
96
+ script_file.write(yield)
97
+ # script_file.chmod(0755)
98
+ script_file.to_path
99
+ end
100
+
101
+ protected
102
+
103
+ def default_identifier
104
+ File.expand_path(@options[:file])
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WheneverSystemd
4
+ module Formatters
5
+ factory = proc { |tpl, args| tpl % args }.curry(2).freeze
6
+
7
+ ParamPair = factory["%s=%s"].freeze
8
+
9
+ capitalize = -> (part) do
10
+ part.match?(/\A[a-z][a-z0-9]+\z/) ? part.capitalize : part
11
+ end
12
+
13
+ ParamKey = -> (key) do
14
+ key.to_s.split('_').map(&capitalize).join
15
+ end
16
+
17
+ hash_join_params = -> (hash) do
18
+ hash.transform_keys(&ParamKey).map(&ParamPair) * "\n"
19
+ end
20
+
21
+ ParamsHash = -> (hash) do
22
+ hash.transform_values(&hash_join_params)
23
+ end
24
+
25
+ IntervalMinutes = factory["*:0/%{minutes}"]
26
+ IntervalHours = factory["0/%{hours}:0/%{minutes}"] >> proc { |v| v.chomp("/0") }
27
+ IntervalSeconds = factory["0:0:0/%d"]
28
+
29
+ NormalizeInterval = {
30
+ "daily" => "*-*-*"
31
+ }
32
+
33
+ Duration = -> (freq) do
34
+ case freq
35
+ when 0...3600; IntervalMinutes[freq.parts] # 1 second to hour
36
+ when 3600...86400; IntervalHours[freq.parts] # 1 hour to day
37
+ else IntervalSeconds[freq.to_i]
38
+ end
39
+ end
40
+
41
+ Freq = -> (freq) do
42
+ ActiveSupport::Duration === freq ? Duration[freq] : freq
43
+ end
44
+
45
+ Service = ParamsHash >> factory[<<~INI]
46
+ [Unit]
47
+ %{unit}
48
+
49
+ [Service]
50
+ %{service}
51
+ INI
52
+
53
+ Timer = ParamsHash >> factory[<<~INI]
54
+ [Unit]
55
+ %{unit}
56
+
57
+ [Timer]
58
+ %{timer}
59
+
60
+ [Install]
61
+ %{install}
62
+ INI
63
+
64
+ Target = ParamsHash >> factory[<<~INI]
65
+ [Unit]
66
+ %{unit}
67
+
68
+ [Install]
69
+ WantedBy=timers.target
70
+ INI
71
+
72
+ MaterializeUnit = factory[<<~BASH]
73
+ cat > %{path}/%{filename} <<'EOF'
74
+ %{content}
75
+ EOF
76
+ BASH
77
+
78
+ MaterializeUnits = proc do |list|
79
+ list.map(&MaterializeUnit) * "\n"
80
+ end
81
+
82
+ DryUnit = factory[<<~EOF]
83
+ # filepath: %{path}/%{filename}
84
+ %{content}
85
+
86
+ EOF
87
+
88
+ DryUnits = proc { |list| list.map(&DryUnit) * "\n" }
89
+ end
90
+ end
@@ -0,0 +1,118 @@
1
+ require 'shellwords'
2
+ require "whenever_systemd/formatters"
3
+
4
+ module WheneverSystemd
5
+ class Job
6
+ attr_reader :at, :roles, :mailto, :name
7
+
8
+ def initialize(name, options = {})
9
+ @name = name
10
+ @options = options
11
+ @at = options.delete(:at)
12
+ @template = options.delete(:template)
13
+ @mailto = options.fetch(:mailto, :default_mailto)
14
+ @job_template = options.delete(:job_template) || ":job"
15
+ @roles = Array(options.delete(:roles))
16
+ @options[:output] = options.has_key?(:output) ? Output::Redirection.new(options[:output]).to_s : ''
17
+ @options[:environment_variable] ||= "RAILS_ENV"
18
+ @options[:environment] ||= :production
19
+ @options[:path] = Shellwords.shellescape(@options[:path] || WheneverSystemd.path)
20
+
21
+ description = options.delete(:description)
22
+ install = options.delete(:install) { { wanted_by: "timers.target" } }
23
+
24
+ timer = options.delete(:timer).to_h
25
+ timer[:on_calendar] ||= compose_on_calendar(options.delete(:interval), @at)
26
+
27
+ unit = options.delete(:unit).to_h
28
+ unit[:description] = description
29
+
30
+ service = options.delete(:service).to_h
31
+ service[:type] ||= "oneshot"
32
+ service[:exec_start] = output
33
+
34
+ @service_options = { unit: unit, service: service }
35
+ @timer_options = { unit: { description: description }, timer: timer, install: install }
36
+ end
37
+
38
+ def compose_on_calendar(*args)
39
+ args.compact!
40
+ if args.size > 1 && Formatters::NormalizeInterval.key?(args[0])
41
+ args[0] = Formatters::NormalizeInterval[args[0]]
42
+ end
43
+ args.join(" ")
44
+ end
45
+
46
+ def output
47
+ job = process_template(@template, @options)
48
+ out = process_template(@job_template, @options.merge(:job => job))
49
+ out.gsub(/%/, '\%')
50
+ end
51
+
52
+ def unprefixed_name(prefix = nil)
53
+ prefix ||= @options[:prefix]
54
+ if @name.rindex(prefix) == 0
55
+ prefix_end = prefix.size.next
56
+ @name[prefix_end, @name.size - prefix_end]
57
+ else
58
+ @name
59
+ end
60
+ end
61
+
62
+ def service_name
63
+ "#{@name}.service"
64
+ end
65
+
66
+ def timer_name
67
+ "#{@name}.timer"
68
+ end
69
+
70
+ def unit_expansion
71
+ "#{@name}.{service,timer}"
72
+ end
73
+
74
+ def systemd_units(path)
75
+ [
76
+ { path: path, filename: service_name, content: systemd_service },
77
+ { path: path, filename: timer_name, content: systemd_timer }
78
+ ]
79
+ end
80
+
81
+ def systemd_service
82
+ Formatters::Service[@service_options]
83
+ end
84
+
85
+ def systemd_timer
86
+ Formatters::Timer[@timer_options]
87
+ end
88
+
89
+ def has_role?(role)
90
+ roles.empty? || roles.include?(role)
91
+ end
92
+
93
+ protected
94
+
95
+ def process_template(template, options)
96
+ template.gsub(/:\w+/) do |key|
97
+ before_and_after = [$`[-1..-1], $'[0..0]]
98
+ option = options[key.sub(':', '').to_sym] || key
99
+
100
+ if before_and_after.all? { |c| c == "'" }
101
+ escape_single_quotes(option)
102
+ elsif before_and_after.all? { |c| c == '"' }
103
+ escape_double_quotes(option)
104
+ else
105
+ option
106
+ end
107
+ end.gsub(/\s+/m, " ").strip
108
+ end
109
+
110
+ def escape_single_quotes(str)
111
+ str.gsub(/'/) { "'\\''" }
112
+ end
113
+
114
+ def escape_double_quotes(str)
115
+ str.gsub(/"/) { '\"' }
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "whenever_systemd/formatters"
4
+ require "active_support/duration"
5
+ require "active_support/core_ext/numeric/time"
6
+ require "active_support/core_ext/object/deep_dup"
7
+
8
+ module WheneverSystemd
9
+ class JobList
10
+ attr_reader :roles
11
+
12
+ def initialize(options)
13
+ @jobs, @env, @set_variables, @pre_set_variables = [], {}, {}, {}
14
+
15
+ if options.is_a? String
16
+ options = { :string => options }
17
+ end
18
+ @temp_path = options[:temp_path]
19
+ pre_set(options[:set])
20
+ @roles = options[:roles] || []
21
+
22
+ setup_file = File.expand_path('../setup.rb', __FILE__)
23
+ setup = File.read(setup_file)
24
+ schedule = if options[:string]
25
+ options[:string]
26
+ elsif options[:file]
27
+ File.read(options[:file])
28
+ end
29
+
30
+ instance_eval(setup, setup_file)
31
+ instance_eval(schedule, options[:file] || '<eval>')
32
+ end
33
+
34
+ def set(variable, value)
35
+ variable = variable.to_sym
36
+ return if @pre_set_variables[variable]
37
+
38
+ instance_variable_set("@#{variable}".to_sym, value)
39
+ @set_variables[variable] = value
40
+ end
41
+
42
+ def method_missing(name, *args, &block)
43
+ @set_variables.has_key?(name) ? @set_variables[name] : super
44
+ end
45
+
46
+ def self.respond_to?(name, include_private = false)
47
+ @set_variables.has_key?(name) || super
48
+ end
49
+
50
+ def env(variable, value)
51
+ @env[variable.to_s] = value
52
+ end
53
+
54
+ def every(frequency, at: nil, **timer)
55
+ @options_was = @options
56
+ @options = @options.to_h.merge(interval: Formatters::Freq[frequency], timer: timer, at: at)
57
+ yield
58
+ ensure
59
+ @options, @options_was = @options_was, nil
60
+ end
61
+
62
+ def daily(at: "00:00:00", **options, &block)
63
+ every("*-*-*", at: at, **options, &block)
64
+ end
65
+
66
+ %w(minutely hourly monthly yearly quarterly semiannually).each do |k|
67
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
68
+ def #{k}(**options, &block)
69
+ every(:#{k}, **options, &block)
70
+ end
71
+ RUBY
72
+ end
73
+
74
+ def weekly(*on_days, **options, &block)
75
+ if on_days.size > 0
76
+ every("#{on_days * ?,} *-*-*", **options, &block)
77
+ else
78
+ every(:weekly, **options, &block)
79
+ end
80
+ end
81
+
82
+ def at(time)
83
+ @options_was, @options = @options, @options.to_h.merge(at: time)
84
+ yield
85
+ ensure
86
+ @options, @options_was = @options_was, nil
87
+ end
88
+
89
+ attr_reader :jobs
90
+
91
+ def job_type(name, template)
92
+ singleton_class.class_eval do
93
+ define_method(name) do |job_name, task, *args|
94
+ options = { :task => task, :template => template }
95
+ options.merge!(args[0]) if args[0].is_a? Hash
96
+ options[:description] ||= options.delete("?") { "WheneverSystemd-generated Job" }
97
+
98
+ @jobs ||= []
99
+ @jobs << Job.new("#{@prefix}-#{job_name}", @set_variables.deep_merge(@options).deep_merge(options).deep_dup)
100
+ end
101
+ end
102
+ end
103
+
104
+ def generate(name, path)
105
+ case name.to_sym
106
+ when :units; generate_units_script(path)
107
+ when :update_units; generate_update_script(path)
108
+ when :clear_units; generate_clear_script(path)
109
+ else raise ArgumentError, %(available names: units, update_units, clear_units, given: #{name})
110
+ end
111
+ end
112
+
113
+ def generate_units_script(path)
114
+ Formatters::MaterializeUnits[systemd_units(path)]
115
+ end
116
+
117
+ def generate_update_script(path)
118
+ [
119
+ make_backup_dir,
120
+ backup_previous_units_from(path, all: true),
121
+ systemctl_timers('disable', '--now', all: true),
122
+ generate_units_script(@temp_path),
123
+ copy_updated_units_to(path),
124
+ Shellwords.join(["/usr/bin/systemctl", "daemon-reload"]),
125
+ systemctl_timers('enable', '--now')
126
+ ].join("\n\n")
127
+ end
128
+
129
+ def generate_clear_script(path)
130
+ [
131
+ make_backup_dir,
132
+ backup_previous_units_from(path, all: true),
133
+ systemctl_timers('disable', '--now', all: true),
134
+ format(%(/usr/bin/rm -rfI %{target}/%{expansion}), target: Shellwords.escape(path), expansion: units_expansion(all: true))
135
+ ].join("\n\n")
136
+ end
137
+
138
+ def backup_previous_units_from(path, **opts)
139
+ format(%(/usr/bin/cp -rvf %{source}/%{expansion} -t %{target}),
140
+ source: Shellwords.escape(path),
141
+ expansion: units_expansion(**opts),
142
+ target: Shellwords.escape("#{@temp_path}/backup")
143
+ )
144
+ end
145
+
146
+ def copy_updated_units_to(path)
147
+ format(%(/usr/bin/cp -rvf %{source}/*.{service,timer} -t %{target}),
148
+ source: Shellwords.escape(@temp_path),
149
+ target: Shellwords.escape(path)
150
+ )
151
+ end
152
+
153
+ def make_backup_dir
154
+ Shellwords.join(["mkdir", "-p", "#{@temp_path}/backup"])
155
+ end
156
+
157
+ def systemctl_timers(*args, **opts)
158
+ case args[0]
159
+ when "enable", "disable"
160
+ format(%(for timer in %s; do %s $timer; done),
161
+ units_expansion('timer', sub: true, **opts),
162
+ Shellwords.join(["/usr/bin/systemctl", *args])
163
+ )
164
+ else
165
+ Shellwords.join(["/usr/bin/systemctl", *args, units_expansion('timer', **opts)])
166
+ end
167
+ end
168
+
169
+ def timers
170
+ @jobs.map(&:timer_name)
171
+ end
172
+
173
+ def unit_files(path)
174
+ Dir.glob("#{path}/#{units_expansion}")
175
+ end
176
+
177
+ def units_expansion(ext = "{service,timer}", all: false, sub: false)
178
+ suffixes = all ? "*" : format("{%s}", @jobs.map { |j| Shellwords.escape(j.unprefixed_name) }.join(?,))
179
+ pattern = "#{@prefix}-#{suffixes}.#{ext}"
180
+ return pattern unless sub
181
+ if all
182
+ format(%($(/usr/bin/systemctl list-unit-files '%s' | /usr/bin/cut -d ' ' -f 1 | /usr/bin/head -n -2 | /usr/bin/tail -n +2)), pattern)
183
+ else
184
+ format(%($(/usr/bin/echo %s)), pattern)
185
+ end
186
+ end
187
+
188
+ def dry_units(path)
189
+ Formatters::DryUnits[systemd_units(path)]
190
+ end
191
+
192
+ def systemd_units(path, include_target = true)
193
+ wanted_by = @set_variables.dig(:install, :wanted_by)
194
+ if include_target && wanted_by != "timers.target"
195
+ [timers_target(path), *systemd_units("#{path}/#{wanted_by}.wants", false)]
196
+ else
197
+ @jobs.flat_map { |job| job.systemd_units(path) }
198
+ end
199
+ end
200
+
201
+ def timers_target(path)
202
+ {
203
+ path: path,
204
+ filename: @set_variables.dig(:install, :wanted_by),
205
+ content: Formatters::Target[unit: { description: "Timers target" }]
206
+ }
207
+ end
208
+
209
+ private
210
+
211
+ #
212
+ # Takes a string like: "variable1=something&variable2=somethingelse"
213
+ # and breaks it into variable/value pairs. Used for setting variables at runtime from the command line.
214
+ # Only works for setting values as strings.
215
+ #
216
+ def pre_set(variable_string = nil)
217
+ return if variable_string.nil? || variable_string == ""
218
+
219
+ pairs = variable_string.split('&')
220
+ pairs.each do |pair|
221
+ next unless pair.index('=')
222
+ variable, value = *pair.split('=')
223
+ unless variable.nil? || variable == "" || value.nil? || value == ""
224
+ variable = variable.strip.to_sym
225
+ set(variable, value.strip)
226
+ @pre_set_variables[variable] = value
227
+ end
228
+ end
229
+ end
230
+
231
+ def environment_variables
232
+ return if @env.empty?
233
+
234
+ output = []
235
+ @env.each do |key, val|
236
+ output << "#{key}=#{val.nil? || val == "" ? '""' : val}\n"
237
+ end
238
+ output << "\n"
239
+
240
+ output.join
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,7 @@
1
+ module WheneverSystemd
2
+ module OS
3
+ def self.solaris?
4
+ (/solaris/ =~ RUBY_PLATFORM)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,57 @@
1
+ module WheneverSystemd
2
+ module Output
3
+ class Redirection
4
+ def initialize(output)
5
+ @output = output
6
+ end
7
+
8
+ def to_s
9
+ return '' unless defined?(@output)
10
+ case @output
11
+ when String then redirect_from_string
12
+ when Hash then redirect_from_hash
13
+ when NilClass then ">> /dev/null 2>&1"
14
+ when Proc then @output.call
15
+ else ''
16
+ end
17
+ end
18
+
19
+ protected
20
+
21
+ def stdout
22
+ return unless @output.has_key?(:standard)
23
+ @output[:standard].nil? ? '/dev/null' : @output[:standard]
24
+ end
25
+
26
+ def stderr
27
+ return unless @output.has_key?(:error)
28
+ @output[:error].nil? ? '/dev/null' : @output[:error]
29
+ end
30
+
31
+ def redirect_from_hash
32
+ case
33
+ when stdout == '/dev/null' && stderr == '/dev/null'
34
+ "> /dev/null 2>&1"
35
+ when stdout && stderr == '/dev/null'
36
+ ">> #{stdout} 2> /dev/null"
37
+ when stdout && stderr
38
+ ">> #{stdout} 2>> #{stderr}"
39
+ when stderr == '/dev/null'
40
+ "2> /dev/null"
41
+ when stderr
42
+ "2>> #{stderr}"
43
+ when stdout == '/dev/null'
44
+ "> /dev/null"
45
+ when stdout
46
+ ">> #{stdout}"
47
+ else
48
+ ''
49
+ end
50
+ end
51
+
52
+ def redirect_from_string
53
+ ">> #{@output} 2>&1"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ # Environment variable defaults to RAILS_ENV
2
+ set :environment_variable, "RAILS_ENV"
3
+ # Environment defaults to production
4
+ set :environment, "production"
5
+ # Path defaults to the directory `whenever` was run from
6
+ set :path, WheneverSystemd.path
7
+
8
+ # All jobs are wrapped in this template.
9
+ # http://blog.scoutapp.com/articles/2010/09/07/rvm-and-cron-in-production
10
+ set :job_template, "/bin/bash -l -c ':job'"
11
+
12
+ set :prefix, "myproject"
13
+ set :timer, { accuracy_sec: "1m" }
14
+ set :install, { wanted_by: "timers.target" }
15
+
16
+ set :runner_command, case
17
+ when WheneverSystemd.bin_rails?
18
+ "bin/rails runner"
19
+ when WheneverSystemd.script_rails?
20
+ "script/rails runner"
21
+ else
22
+ "script/runner"
23
+ end
24
+
25
+ set :bundle_command, WheneverSystemd.bundler? ? "bundle exec" : ""
26
+
27
+ job_type :command, ":task :output"
28
+ job_type :rake, "cd :path && :environment_variable=:environment :bundle_command rake :task --silent :output"
29
+ job_type :script, "cd :path && :environment_variable=:environment :bundle_command script/:task :output"
30
+ job_type :runner, "cd :path && :bundle_command :runner_command -e :environment ':task' :output"
@@ -0,0 +1,3 @@
1
+ module WheneverSystemd
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'whenever_systemd/job_list'
4
+ require 'whenever_systemd/job'
5
+ require 'whenever_systemd/command_line'
6
+ require 'whenever_systemd/output_redirection'
7
+ require 'whenever_systemd/os'
8
+
9
+ module WheneverSystemd
10
+ DEFAULT_INSTALL_PATH = "/etc/systemd/system"
11
+
12
+ def self.cron(options)
13
+ JobList.new(options).dry_units(options[:install_path])
14
+ end
15
+
16
+ def self.path
17
+ Dir.pwd
18
+ end
19
+
20
+ def self.bin_rails?
21
+ File.exist?(File.join(path, 'bin', 'rails'))
22
+ end
23
+
24
+ def self.script_rails?
25
+ File.exist?(File.join(path, 'script', 'rails'))
26
+ end
27
+
28
+ def self.bundler?
29
+ File.exist?(File.join(path, 'Gemfile'))
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "whenever_systemd/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "whenever_systemd"
7
+ s.version = WheneverSystemd::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Javan Makhmali", "Anton Semenov"]
10
+ s.email = ["javan@javan.us", "anton.estum@gmail.com"]
11
+ s.license = "MIT"
12
+ s.homepage = "https://github.com/estum/whenever"
13
+ s.summary = %q{Systemd Timers in ruby.}
14
+ s.description = %q{Clean ruby syntax for writing and deploying systemd timers.}
15
+ s.files = `git ls-files`.split("\n")
16
+ # s.test_files = `git ls-files -- test/{functional,unit}/*`.split("\n")
17
+ s.executables = ["whenever_systemd", "wheneverize"]
18
+ s.require_paths = ["lib"]
19
+ s.required_ruby_version = ">= 2.6"
20
+
21
+ s.add_dependency "activesupport", ">= 5.2"
22
+ s.add_development_dependency "bundler"
23
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: whenever_systemd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Javan Makhmali
8
+ - Anton Semenov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-02-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '5.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '5.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description: Clean ruby syntax for writing and deploying systemd timers.
43
+ email:
44
+ - javan@javan.us
45
+ - anton.estum@gmail.com
46
+ executables:
47
+ - whenever_systemd
48
+ - wheneverize
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - ".gitignore"
53
+ - ".travis.yml"
54
+ - Appraisals
55
+ - CHANGELOG.md
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - bin/whenever_systemd
61
+ - bin/wheneverize
62
+ - gemfiles/activesupport5.2.gemfile
63
+ - lib/whenever_systemd.rb
64
+ - lib/whenever_systemd/command_line.rb
65
+ - lib/whenever_systemd/formatters.rb
66
+ - lib/whenever_systemd/job.rb
67
+ - lib/whenever_systemd/job_list.rb
68
+ - lib/whenever_systemd/os.rb
69
+ - lib/whenever_systemd/output_redirection.rb
70
+ - lib/whenever_systemd/setup.rb
71
+ - lib/whenever_systemd/version.rb
72
+ - whenever_systemd.gemspec
73
+ homepage: https://github.com/estum/whenever
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '2.6'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.0.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Systemd Timers in ruby.
96
+ test_files: []