whenever_systemd 0.0.2

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.
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: []