seijaku 0.2.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63fdce33cce3f7b4496f7ba0968a069554f351f5b1ea37d84c0dc20b7cfb3d49
4
- data.tar.gz: 01fc22c0d064e13bbf514633545ec4f947176a531bf57fe479294d816f60154b
3
+ metadata.gz: 0b351498f055e85f7e20edc44374616118d0162b479104d18f20b271138a9271
4
+ data.tar.gz: '08c117438ac1e83a15fef55c6973e27fd15ef1d2fbc03a7027774deee4904720'
5
5
  SHA512:
6
- metadata.gz: ffc15482a72317f1617f65374e2de9dd86008d7d4faa64dc3a34fe9dc9e5f940f77982f2a009a82c54613b1226135e3b8a58a2a37d126e6b0a6b105d4de7b0f6
7
- data.tar.gz: 3bdfd31aac02d9bdd5be33b5f77f05327ac4eaa05af82654b00d9fb4c30382be07c0879ae1ac91deaaf36468e934a6f0696b7e6223659332de25a2ed22f31fca
6
+ metadata.gz: 23d86d8bad1a17e32a8b52faebe48873369c7eabf8988fbeec15e854d2e4b834aa8d29dab583dbfb121f18de545289bb1f4ab7484cd7bea787c0aba04cfcd33d
7
+ data.tar.gz: '0532610288a45a4e8391f71da9caa73d2ac22f2a02af7a99af4629fdaa9b3efe60a2fdcf7696337030822e2a24d8d1f832855b447f88292e9d92e4774f843134'
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- ## [Unreleased]
1
+ ## Seijaku
2
+
3
+ Please mind Seijaku is still under heavy development.
4
+
5
+ ## [0.3.0] - 2023-12-14
6
+
7
+ - scheduler has been added to run reccurent payloads
8
+ - fixed a bug related to SSHExecutor set as default executor
9
+
10
+ ## [0.2.0] - 2023-12-12
11
+
12
+ - SSH executor added with `ssh` YAML dictionnary and `host` Task support.
13
+ - Documentation added in `./docs` directory
2
14
 
3
15
  ## [0.1.0] - 2023-12-11
4
16
 
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Seijaku
2
2
 
3
+ [![Seijaku version](https://badge.fury.io/rb/seijaku.svg)](https://badge.fury.io/rb/seijaku)
4
+
3
5
  Seijaku is a program that allows you to execute shell commands listed in YAML payload files, ensuring that they are executed correctly.
6
+ It includes a lightweight scheduler that will take care of executing payloads with specific delay between two executions.
4
7
 
5
8
  ## Concepts
6
9
 
@@ -12,6 +15,8 @@ Each task can have "pre" and "post" tasks, for example to create and delete fold
12
15
 
13
16
  A step sometimes needs variables in order to be performed correctly: Seijaku supports the direct definition of variables or from an environment variable of the shell running Seijaku.
14
17
 
18
+ A scheduler is a file listing payloads that must be executed with unique specifications: a name, a delay between two runs and the path to the concerned payload.
19
+
15
20
  ## Example
16
21
 
17
22
  ```yaml
@@ -49,6 +54,21 @@ tasks:
49
54
  - ssh: echo "executed on host"
50
55
  ```
51
56
 
57
+ ### Scheduler
58
+
59
+ ```yaml
60
+ name: my-scheduler
61
+
62
+ payloads:
63
+ - payload: ./test/my-payload.yaml
64
+ name: My test Payload
65
+ every: 3600 # executed every hour
66
+
67
+ - payload: ./test/another-payload.yaml
68
+ name: Another payload
69
+ every: 60 # executed every minute
70
+ ```
71
+
52
72
  ## Installation
53
73
 
54
74
  ### Dependencies
@@ -66,5 +86,6 @@ gem install seijaku
66
86
 
67
87
  ```bash
68
88
  seijaku -h
69
- seijaku -f ./my-payload.yaml
89
+ seijaku -f ./my-payload.yaml # one-time payload execution
90
+ seijaku -s ./my-scheduler.yaml # recurrent payloads execution with delay
70
91
  ```
data/bin/seijaku CHANGED
@@ -19,6 +19,7 @@ module App
19
19
  opts = OptionParser.new
20
20
  opts.banner = "Seijaku: simply runs YAML tasks with shell"
21
21
  opts.on("-f", "--file FILE", "Payload file path") { |o| options[:payload] = o }
22
+ opts.on("-s", "--scheduler FILE", "Scheduler file") { |o| options[:scheduler] = o }
22
23
  opts.on("-h", "--help", "Shows help and exit") do
23
24
  puts opts
24
25
  exit(0)
@@ -26,17 +27,42 @@ module App
26
27
 
27
28
  opts.parse!
28
29
 
29
- if options[:payload].nil?
30
+ if options[:payload].nil? and options[:scheduler].nil?
30
31
  puts opts
31
- exit 0
32
+ puts "Either -f or -s must be set"
33
+ exit 1
32
34
  end
33
35
 
34
- payload_file = YAML.safe_load(
35
- File.read(options[:payload])
36
- )
36
+ if options[:payload] and options[:scheduler]
37
+ puts opts
38
+ puts "Either -f or -s must be set"
39
+ exit 1
40
+ end
37
41
 
38
- logger.info "Starting Seijaku. Payload: #{payload_file["name"]}"
42
+ if options[:payload]
43
+ payload_file = YAML.safe_load(
44
+ File.read(options[:payload]),
45
+ symbolize_names: true
46
+ )
39
47
 
40
- payload = Payload.new(payload_file, logger)
41
- payload.execute!
48
+ logger.info "Starting Seijaku. Payload: #{payload_file["name"]}"
49
+
50
+ payload = Payload.new(payload_file, logger)
51
+ payload.execute!
52
+ end
53
+
54
+ if options[:scheduler]
55
+ scheduler_file = YAML.safe_load(
56
+ File.read(options[:scheduler])
57
+ )
58
+
59
+ logger.info "Starting Seijaku. Scheduler: #{scheduler_file["name"]}"
60
+ begin
61
+ scheduler = Scheduler.new(scheduler_file, logger)
62
+ scheduler.monitor!
63
+ rescue Interrupt
64
+ scheduler.soft_exit!
65
+ puts "All payloads did stop their execution gracefully."
66
+ end
67
+ end
42
68
  end
@@ -0,0 +1,54 @@
1
+ # Schedulers
2
+
3
+ Since version `0.3.0`, Seijaku implements a task scheduler that permits us to execute payloads with interval between two runs.
4
+
5
+ Instead of using the `-f` parameter to run a payload, you can use the `-s (--scheduler)` parameter to pass a `schedulerfile` to Seijaku.
6
+
7
+ ## How it works ?
8
+
9
+ Ruby isn't great at parallelism, but it does integrate `Threads`. The Scheduler will assign a Thread to all the payloads to execute.
10
+
11
+ ## Example
12
+
13
+ Here is a really basic payload for example purpose:
14
+
15
+ ```yaml
16
+ name: my-payload
17
+ tasks:
18
+ - name: say hello world
19
+ steps:
20
+ - sh: echo "hello world"
21
+ ```
22
+
23
+ If you want to run this payload every 10 seconds, you can create a scheduler file (call it whatever you want), describing the awaited behavior.
24
+
25
+ ```yaml
26
+ name: "my-scheduler"
27
+
28
+ payloads:
29
+ - payload: ./my-payload.yaml
30
+ every: 10
31
+ name: say-hello-world-10-s
32
+ ```
33
+
34
+ ## Graceful stop
35
+
36
+ CTRL+C (Interrupt signal) sends an information to all the SchedulerExecutors running, asking them to do not re-schedule the payload execution they are in charge of once the current execution is finished.
37
+
38
+ ```
39
+ ...
40
+ > CTRL+C
41
+ [2023-12-14T18:48:12.509483 #8841] INFO -- : Soft exit asked by user, waiting for payloads to stop naturally...
42
+ I, [2023-12-14T18:48:12.509789 #8841] INFO -- : test payload: still waiting...
43
+ I, [2023-12-14T18:48:12.509856 #8841] INFO -- : another-payload: still waiting...
44
+ I, [2023-12-14T18:48:13.515211 #8841] INFO -- : test payload: still waiting...
45
+ I, [2023-12-14T18:48:13.515496 #8841] INFO -- : another-payload: still waiting...
46
+ I, [2023-12-14T18:48:14.520005 #8841] INFO -- : test payload: still waiting...
47
+ I, [2023-12-14T18:48:14.520262 #8841] INFO -- : another-payload: stopped gracefully.
48
+ I, [2023-12-14T18:48:15.524617 #8841] INFO -- : test payload: stopped gracefully.
49
+ All payloads did stop their execution gracefully.
50
+ ```
51
+
52
+ ## Work in progress
53
+
54
+ * at the moment, only `every` is accepted. Cron formulas could one day be an option but its implementation is more complex.
@@ -6,12 +6,19 @@ require "net/ssh/proxy/jump"
6
6
  module Seijaku
7
7
  # SSHExecutor connects to SSH host and runs command
8
8
  class SSHExecutor
9
- def initialize(raw, variables, task, ssh_hosts)
10
- @command = ["sh", "-c", "'#{raw}'"]
9
+ def initialize(raw, variables, task, ssh_hosts, ssh_settings)
11
10
  @hosts = ssh_hosts
12
- @variables = variables
11
+ @variables = variables.map do |key, value|
12
+ "#{key}='#{value}'"
13
+ end.join(" ")
14
+
15
+ @command = ["#{@variables};", "#{raw}"]
13
16
  @task = task
14
17
  @ssh_hosts = ssh_hosts
18
+ @ssh_settings = ssh_settings.transform_keys(&:to_sym)
19
+ if @ssh_settings[:verify_host_key].eql?("never")
20
+ @ssh_settings[:verify_host_key] = :never
21
+ end
15
22
 
16
23
  raise SSHExecutorError, "no ssh host defined in payload", [] if ssh_hosts.nil?
17
24
  end
@@ -20,12 +27,15 @@ module Seijaku
20
27
  machine = @ssh_hosts.hosts[@task.host]
21
28
  result = { command: @command.join(" "), stdout: nil, stderr: nil }
22
29
  status = {}
23
- options = machine.bastion ? { proxy: bastion_setup } : {}
24
- ssh = Net::SSH.start(machine.host, machine.user, port: machine.port, **options)
30
+ options = machine.bastion ? { proxy: bastion_setup, **@ssh_settings } : @ssh_settings
31
+
32
+ ssh = Net::SSH.start(machine.host, machine.user, {port: machine.port, **@ssh_settings})
33
+
25
34
  ssh.exec!(@command.join(" "), status: status) do |_ch, stream, data|
26
35
  result[:stdout] = data.chomp if stream == :stdout
27
36
  result[:stderr] = data.chomp unless stream == :stdout
28
37
  end
38
+
29
39
  ssh.close
30
40
 
31
41
  result[:exit_status] = status[:exit_code]
@@ -4,12 +4,15 @@ module Seijaku
4
4
  # Payload refers to the payload YAML file submitted by the user.
5
5
  # it includes tasks, steps, variables and name.
6
6
  class Payload
7
+ attr_reader :name
8
+
7
9
  def initialize(payload, logger)
8
- @name = payload.fetch("name")
9
- @variables = get_variables(payload.fetch("variables"))
10
- @ssh_hosts = SSHGroup.new(payload.fetch("ssh", []))
11
- @tasks = payload.fetch("tasks").map do |task|
12
- Task.new(task, @variables, logger, @ssh_hosts)
10
+ @name = payload.fetch(:name)
11
+ @variables = get_variables(payload.fetch(:variables))
12
+ @ssh_hosts = SSHGroup.new(payload.fetch(:ssh, []))
13
+ @ssh_settings = payload.fetch(:ssh_settings, {})
14
+ @tasks = payload.fetch(:tasks).map do |task|
15
+ Task.new(task, @variables, logger, @ssh_hosts, @ssh_settings)
13
16
  end
14
17
  end
15
18
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ class Scheduler
5
+ def initialize(scheduler, logger)
6
+ @name = scheduler.fetch("name", nil)
7
+ @logger = logger
8
+ @scheduler_executors = scheduler.fetch("payloads", []).map do |payload_spec|
9
+ payload = File.read(payload_spec["payload"]).then do |data|
10
+ YAML.safe_load(data).then do |data|
11
+ Payload.new(data, logger)
12
+ end
13
+ end
14
+
15
+ SchedulerExecutor.new(payload, payload_spec, logger)
16
+ end
17
+ end
18
+
19
+ def monitor!
20
+ thd = []
21
+ @scheduler_executors.each do |scheduler_executor|
22
+ @logger.info "scheduling... #{scheduler_executor.name}"
23
+ thd << Thread.new do
24
+ scheduler_executor.execute!
25
+ end
26
+ end
27
+
28
+ thd.each &:join
29
+ end
30
+
31
+ def soft_exit!
32
+ @logger.info "Soft exit asked by user, waiting for payloads to stop naturally..."
33
+ thd = []
34
+ @scheduler_executors.each do |scheduler_executor|
35
+ thd << Thread.new do
36
+ scheduler_executor.status = :exiting
37
+ while scheduler_executor.status != :exited
38
+ @logger.info "#{scheduler_executor.name}: still waiting..."
39
+ sleep 1
40
+ end
41
+
42
+ @logger.info "#{scheduler_executor.name}: stopped gracefully"
43
+ end
44
+ end
45
+ thd.each &:join
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ module Seijaku
2
+ class SchedulerExecutor
3
+ attr_reader :name
4
+ attr_accessor :status
5
+
6
+ def initialize(payload_obj, scheduler_spec, logger)
7
+ @name = scheduler_spec.fetch("name", nil)
8
+ @every = scheduler_spec.fetch("every", nil)
9
+ @payload_file = scheduler_spec.fetch("payload")
10
+
11
+ @logger = logger
12
+ @payload = payload_obj
13
+ @status = :awaiting
14
+ end
15
+
16
+ def execute!
17
+ while @status != :exiting do
18
+ @logger.info "[SCHEDULER] <<- starting execution: #{@name}"
19
+
20
+ @status = :started
21
+ @payload.execute!
22
+ @status = :awaiting
23
+
24
+ @logger.info "[SCHEDULER] <<- finished execution: #{@name} (#{@every}s)"
25
+ sleep @every
26
+ end
27
+
28
+ @status = :exited
29
+ end
30
+ end
31
+ end
@@ -8,7 +8,7 @@ module Seijaku
8
8
  def initialize(ssh_group)
9
9
  @hosts = {}
10
10
  ssh_group.each do |host|
11
- @hosts[host["host"]] = Host.new(host)
11
+ @hosts[host[:host]] = Host.new(host)
12
12
  end
13
13
  end
14
14
  end
@@ -6,10 +6,10 @@ module Seijaku
6
6
  attr_reader :host, :port, :user, :bastion
7
7
 
8
8
  def initialize(host)
9
- @host = host.fetch("host", "localhost")
10
- @port = host.fetch("port", 22)
11
- @bastion = host.fetch("bastion", nil)
12
- @user = host.fetch("user", "root")
9
+ @host = host.fetch(:host, "localhost")
10
+ @port = host.fetch(:port, 22)
11
+ @bastion = host.fetch(:bastion, nil)
12
+ @user = host.fetch(:user, "root")
13
13
  end
14
14
  end
15
15
  end
data/lib/seijaku/step.rb CHANGED
@@ -6,17 +6,18 @@ module Seijaku
6
6
  class Step
7
7
  attr_reader :command, :pipeline
8
8
 
9
- def initialize(step, variables, pipeline, logger, task, ssh_hosts = nil)
10
- @sh = step.fetch("sh", nil)
11
- @bash = step.fetch("bash", nil)
12
- @ssh = step.fetch("ssh", nil)
13
- @soft_fail = step.fetch("soft_fail", false)
14
- @output = step.fetch("output", false)
9
+ def initialize(step, variables, pipeline, logger, task, ssh_hosts = nil, ssh_settings = {})
10
+ @sh = step.fetch(:sh, nil)
11
+ @bash = step.fetch(:bash, nil)
12
+ @ssh = step.fetch(:ssh, nil)
13
+ @soft_fail = step.fetch(:soft_fail, false)
14
+ @output = step.fetch(:output, false)
15
15
  @variables = variables
16
16
  @pipeline = pipeline
17
17
  @logger = logger
18
18
  @task = task
19
19
  @ssh_hosts = ssh_hosts
20
+ @ssh_settings = ssh_settings
20
21
 
21
22
  @command = (@sh || @bash) || @ssh
22
23
  end
@@ -24,7 +25,7 @@ module Seijaku
24
25
  def execute!
25
26
  result = SHExecutor.new(@sh, @variables).run! if @sh
26
27
  result = BashExecutor.new(@bash, @variables).run! if @bash
27
- result = SSHExecutor.new(@ssh, @variables, @task, @ssh_hosts).run!
28
+ result = SSHExecutor.new(@ssh, @variables, @task, @ssh_hosts, @ssh_settings).run!
28
29
 
29
30
  if result[:exit_status] != 0
30
31
  logger_output(result)
data/lib/seijaku/task.rb CHANGED
@@ -5,12 +5,12 @@ module Seijaku
5
5
  class Task
6
6
  attr_reader :host
7
7
 
8
- def initialize(task, variables, logger, ssh_hosts = nil)
9
- @name = task.fetch("name", nil)
10
- @host = task.fetch("host", nil)
11
- @steps = task.fetch("steps", []).map { |step| Step.new(step, variables, :steps, logger, self, ssh_hosts) }
12
- @pre_steps = task.fetch("pre", []).map { |step| Step.new(step, variables, :pre, logger, self, ssh_hosts) }
13
- @post_steps = task.fetch("post", []).map { |step| Step.new(step, variables, :post, logger, self, ssh_hosts) }
8
+ def initialize(task, variables, logger, ssh_hosts = nil, ssh_settings = {})
9
+ @name = task.fetch(:name, nil)
10
+ @host = task.fetch(:host, nil)
11
+ @steps = task.fetch(:steps, []).map { |step| Step.new(step, variables, :steps, logger, self, ssh_hosts, ssh_settings) }
12
+ @pre_steps = task.fetch(:pre, []).map { |step| Step.new(step, variables, :pre, logger, self, ssh_hosts, ssh_settings) }
13
+ @post_steps = task.fetch(:post, []).map { |step| Step.new(step, variables, :post, logger, self, ssh_hosts, ssh_settings) }
14
14
  @logger = logger
15
15
 
16
16
  raise TaskError, "no name set in task", [] if @name.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seijaku
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/seijaku.rb CHANGED
@@ -12,6 +12,9 @@ require_relative "seijaku/executors/sh"
12
12
  require_relative "seijaku/executors/bash"
13
13
  require_relative "seijaku/executors/ssh"
14
14
 
15
+ require_relative "seijaku/scheduler"
16
+ require_relative "seijaku/scheduler_executor"
17
+
15
18
  module Seijaku
16
19
  class Error < StandardError; end
17
20
  class VariableError < Error; end
data/sig/seijaku.rbs CHANGED
@@ -1,4 +1,15 @@
1
1
  module Seijaku
2
2
  VERSION: String
3
3
  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ class Error < StandardError
5
+ end
6
+
7
+ class TaskError < Error
8
+ end
9
+
10
+ class VariableError < Error
11
+ end
12
+
13
+ class SSHExecutorError < Error
14
+ end
4
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seijaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kohlrabbit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-12 00:00:00.000000000 Z
11
+ date: 2024-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -71,11 +71,14 @@ files:
71
71
  - docs/1 - basic.md
72
72
  - docs/2 - variables.md
73
73
  - docs/3 - ssh.md
74
+ - docs/4 - scheduler.md
74
75
  - lib/seijaku.rb
75
76
  - lib/seijaku/executors/bash.rb
76
77
  - lib/seijaku/executors/sh.rb
77
78
  - lib/seijaku/executors/ssh.rb
78
79
  - lib/seijaku/payload.rb
80
+ - lib/seijaku/scheduler.rb
81
+ - lib/seijaku/scheduler_executor.rb
79
82
  - lib/seijaku/ssh/group.rb
80
83
  - lib/seijaku/ssh/host.rb
81
84
  - lib/seijaku/step.rb