seijaku 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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