seijaku 0.1.0 → 0.2.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: 473323709a5634fbd29899d6745d1145f5bfbd69c073b94739aa988e6851f13e
4
- data.tar.gz: 5ed3dd59fa73d5bcd30664b480d1c1dcef7b826447c9e2229ab90e0115c1de05
3
+ metadata.gz: 63fdce33cce3f7b4496f7ba0968a069554f351f5b1ea37d84c0dc20b7cfb3d49
4
+ data.tar.gz: 01fc22c0d064e13bbf514633545ec4f947176a531bf57fe479294d816f60154b
5
5
  SHA512:
6
- metadata.gz: 72e7fcf802e5e1ad2c9813eca4a7cf1f5e88db0d083ce29afb64e94c14453cbe0494f9f7bb71ebd5844b8d2ce947418b2d02e5099b476446505be2999c85cdc1
7
- data.tar.gz: a5e9f5c1459a24fcb327d2d272589f84a3ec1eb323da7afe93e6f2840605e3f70d0c833cb706c21df60a0f384020991c34ed738d1009aff64e0e2cb8553be460
6
+ metadata.gz: ffc15482a72317f1617f65374e2de9dd86008d7d4faa64dc3a34fe9dc9e5f940f77982f2a009a82c54613b1226135e3b8a58a2a37d126e6b0a6b105d4de7b0f6
7
+ data.tar.gz: 3bdfd31aac02d9bdd5be33b5f77f05327ac4eaa05af82654b00d9fb4c30382be07c0879ae1ac91deaaf36468e934a6f0696b7e6223659332de25a2ed22f31fca
data/README.md CHANGED
@@ -6,7 +6,7 @@ Seijaku is a program that allows you to execute shell commands listed in YAML pa
6
6
 
7
7
  Payloads are YAML files that describe the various tasks Seijaku will have to perform (in order). Each task contains one or more steps.
8
8
 
9
- A step is a shell command to be executed. Seijaku currently supports the following shells: bash and sh.
9
+ A step is a shell command to be executed. Seijaku currently supports the following shells: bash, sh and ssh.
10
10
 
11
11
  Each task can have "pre" and "post" tasks, for example to create and delete folders, or install and uninstall software needed to run a task.
12
12
 
@@ -17,6 +17,11 @@ A step sometimes needs variables in order to be performed correctly: Seijaku sup
17
17
  ```yaml
18
18
  name: my payload
19
19
 
20
+ ssh:
21
+ - host: my-host
22
+ user: user
23
+ port: 22
24
+
20
25
  variables:
21
26
  MY_VARIABLE: a static variable
22
27
  MY_ENV_VARIABLE: $MY_ENV_VARIABLE
@@ -37,6 +42,11 @@ tasks:
37
42
  soft_fail: true
38
43
  post:
39
44
  - sh: "do something after"
45
+
46
+ - name: task with SSH executor
47
+ host: my-host
48
+ steps:
49
+ - ssh: echo "executed on host"
40
50
  ```
41
51
 
42
52
  ## Installation
@@ -46,11 +56,15 @@ tasks:
46
56
  * Ruby 2.5+
47
57
  * Rubygem
48
58
 
49
-
50
59
  Install Seijaku using Gem:
51
60
 
52
61
  ```bash
53
62
  gem install seijaku
54
63
  ```
55
64
 
65
+ ## Usage
56
66
 
67
+ ```bash
68
+ seijaku -h
69
+ seijaku -f ./my-payload.yaml
70
+ ```
@@ -0,0 +1,10 @@
1
+ # Basic
2
+
3
+ ```yaml
4
+ name: basic payload
5
+ tasks:
6
+ - name: do something useful
7
+ steps:
8
+ - sh: echo "sh executor"
9
+ - bash: echo "bash executor"
10
+ ```
@@ -0,0 +1,24 @@
1
+ # Variables
2
+
3
+ Steps can call environment variables, listed in the `variables` dictionary.
4
+
5
+ Variable values can either be given in the payload (static) or be defined from the executive shell.
6
+
7
+ ```yaml
8
+ name: payload with variables
9
+
10
+ variables:
11
+ ENV_VARIABLE: value
12
+ PROVIDED_ENV_VARIABLE: $PROVIDED_ENV_VARIABLE
13
+
14
+ tasks:
15
+ - name: do something useful
16
+ steps:
17
+ - sh: echo "sh executor: $ENV_VARIABLE"
18
+ - bash: echo "bash executor: $PROVIDED_ENV_VARIABLE"
19
+ ```
20
+
21
+ ```bash
22
+ export PROVIDED_ENV_VARIABLE=value
23
+ seijaku -f ./docs/variables.yaml
24
+ ```
data/docs/3 - ssh.md ADDED
@@ -0,0 +1,78 @@
1
+ # SSH
2
+
3
+ Since version `0.1.0`, Seijaku implements a SSH step executor. Only one SSH host can be set per task.
4
+
5
+
6
+ ```yaml
7
+ name: payload with SSH executor
8
+
9
+ ssh:
10
+ - host: my-host
11
+ port: 22
12
+ user: ubuntu
13
+
14
+ tasks:
15
+ - name: do something useful
16
+ host: my-host
17
+ steps:
18
+ - ssh: echo "connection test"
19
+ ```
20
+
21
+ It is recommended to run `ssh-add` before running Seijaku, as each step would require you to enter your password.
22
+
23
+ ## Variables
24
+
25
+ Variables can be set from the Seijaku executive host and ran in the SSH host context:
26
+
27
+ ```yaml
28
+ name: payload with SSH executor (variables)
29
+
30
+ ssh:
31
+ - host: my-host
32
+ port: 22
33
+ user: ubuntu
34
+
35
+ variables:
36
+ ENV_VARIABLE: value
37
+ PROVIDED_ENV_VARIABLE: $PROVIDED_ENV_VARIABLE
38
+
39
+ tasks:
40
+ - name: do something useful
41
+ host: my-host
42
+ steps:
43
+ - ssh: echo "connection test"
44
+ - ssh: echo "$ENV_VARIABLE"
45
+ - ssh: echo "$PROVIDED_ENV_VARIABLE"
46
+ ```
47
+
48
+ ## Bastion / Jump host / Proxy
49
+
50
+ You sometimes need to jump over a publicly exposed server to reach another one. Seijaku supports SSH Proxy, with a maximum of 1 hop.
51
+
52
+ ```yaml
53
+ name: payload with SSH executor (bastion)
54
+
55
+ ssh:
56
+ - host: bastion-host
57
+ port: 22
58
+ user: bastion_user
59
+
60
+ - host: my-host
61
+ port: 22
62
+ user: ubuntu
63
+ bastion: bastion-host
64
+
65
+ variables:
66
+ ENV_VARIABLE: value
67
+ PROVIDED_ENV_VARIABLE: $PROVIDED_ENV_VARIABLE
68
+
69
+ tasks:
70
+ - name: do something useful
71
+ host: my-host
72
+ steps:
73
+ - ssh: echo "connection test"
74
+ - ssh: echo "$ENV_VARIABLE"
75
+ - ssh: echo "$PROVIDED_ENV_VARIABLE"
76
+ ```
77
+
78
+ The `ssh.[].host` must be a reachable address (domain name or IP).
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Seijaku
6
+ # executes `bash` commands
7
+ class BashExecutor
8
+ def initialize(raw, variables)
9
+ @command = ["bash", "-c", "'#{raw}'"]
10
+ @variables = variables
11
+ end
12
+
13
+ def run!
14
+ stdout, stderr, exit_status = Open3.capture3(
15
+ @variables,
16
+ @command.join(" ")
17
+ )
18
+
19
+ {
20
+ stdout: stdout.chomp,
21
+ stderr: stderr.chomp,
22
+ exit_status: exit_status.exitstatus,
23
+ command: @command.join(" ")
24
+ }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Seijaku
6
+ # Execute `sh` commands
7
+ class SHExecutor
8
+ def initialize(raw, variables)
9
+ @command = ["sh", "-c", "'#{raw}'"]
10
+ @variables = variables
11
+ end
12
+
13
+ def run!
14
+ stdout, stderr, exit_status = Open3.capture3(
15
+ @variables,
16
+ @command.join(" ")
17
+ )
18
+
19
+ {
20
+ stdout: stdout.chomp,
21
+ stderr: stderr.chomp,
22
+ exit_status: exit_status.exitstatus,
23
+ command: @command.join(" ")
24
+ }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/ssh"
4
+ require "net/ssh/proxy/jump"
5
+
6
+ module Seijaku
7
+ # SSHExecutor connects to SSH host and runs command
8
+ class SSHExecutor
9
+ def initialize(raw, variables, task, ssh_hosts)
10
+ @command = ["sh", "-c", "'#{raw}'"]
11
+ @hosts = ssh_hosts
12
+ @variables = variables
13
+ @task = task
14
+ @ssh_hosts = ssh_hosts
15
+
16
+ raise SSHExecutorError, "no ssh host defined in payload", [] if ssh_hosts.nil?
17
+ end
18
+
19
+ def run!
20
+ machine = @ssh_hosts.hosts[@task.host]
21
+ result = { command: @command.join(" "), stdout: nil, stderr: nil }
22
+ status = {}
23
+ options = machine.bastion ? { proxy: bastion_setup } : {}
24
+ ssh = Net::SSH.start(machine.host, machine.user, port: machine.port, **options)
25
+ ssh.exec!(@command.join(" "), status: status) do |_ch, stream, data|
26
+ result[:stdout] = data.chomp if stream == :stdout
27
+ result[:stderr] = data.chomp unless stream == :stdout
28
+ end
29
+ ssh.close
30
+
31
+ result[:exit_status] = status[:exit_code]
32
+ result
33
+ end
34
+
35
+ def bastion_setup
36
+ bastion_name = @ssh_hosts.hosts[@task.host].bastion
37
+ bastion_host = @ssh_hosts.hosts[bastion_name]
38
+
39
+ connect_str = "#{bastion_host.user}@#{bastion_host.host}:#{bastion_host.port}"
40
+ Net::SSH::Proxy::Jump.new(connect_str)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # Payload refers to the payload YAML file submitted by the user.
5
+ # it includes tasks, steps, variables and name.
6
+ class Payload
7
+ 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)
13
+ end
14
+ end
15
+
16
+ def execute!
17
+ @tasks.each(&:execute!)
18
+ end
19
+
20
+ def get_variables(payload_variables)
21
+ sorted_variables = {}
22
+ variables = payload_variables.map do |var|
23
+ Variable.new(var)
24
+ end
25
+
26
+ variables.each do |var|
27
+ sorted_variables.merge!({ var.key => var.value })
28
+ end
29
+
30
+ sorted_variables
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # Group of SSH hosts
5
+ class SSHGroup
6
+ attr_reader :hosts
7
+
8
+ def initialize(ssh_group)
9
+ @hosts = {}
10
+ ssh_group.each do |host|
11
+ @hosts[host["host"]] = Host.new(host)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # Host of SSHGroup
5
+ class Host
6
+ attr_reader :host, :port, :user, :bastion
7
+
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")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # A step is a bash or sh command that must be executed.
5
+ # steps execution is always fifo.
6
+ class Step
7
+ attr_reader :command, :pipeline
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)
15
+ @variables = variables
16
+ @pipeline = pipeline
17
+ @logger = logger
18
+ @task = task
19
+ @ssh_hosts = ssh_hosts
20
+
21
+ @command = (@sh || @bash) || @ssh
22
+ end
23
+
24
+ def execute!
25
+ result = SHExecutor.new(@sh, @variables).run! if @sh
26
+ result = BashExecutor.new(@bash, @variables).run! if @bash
27
+ result = SSHExecutor.new(@ssh, @variables, @task, @ssh_hosts).run!
28
+
29
+ if result[:exit_status] != 0
30
+ logger_output(result)
31
+ exit(1) unless @soft_fail
32
+ end
33
+
34
+ return unless @output
35
+
36
+ %i[stdout stderr].each do |stream|
37
+ puts format("%<stream>s:\t %<result>s", stream: stream, result: result[stream])
38
+ end
39
+ end
40
+
41
+ def logger_output(result)
42
+ @logger.info <<~OUTPUT
43
+ command: `#{result[:command]}`
44
+ exit_code: #{result[:exit_status]}
45
+ stdout: #{result[:stdout]}
46
+ stderr: #{result[:stderr]}"
47
+ OUTPUT
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # Task is composed of a name, an array of Step (Pre, Post)
5
+ class Task
6
+ attr_reader :host
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) }
14
+ @logger = logger
15
+
16
+ raise TaskError, "no name set in task", [] if @name.nil?
17
+ end
18
+
19
+ def execute!
20
+ [@pre_steps, @steps, @post_steps].each do |pipeline_steps|
21
+ pipeline_steps.each do |step|
22
+ @logger.info(format("[%<name>s:%<pipeline>s] running %<command>s",
23
+ name: @name,
24
+ pipeline: step.pipeline,
25
+ command: step.command))
26
+
27
+ step.execute!
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seijaku
4
+ # variable
5
+ class Variable
6
+ attr_reader :key, :value
7
+
8
+ def initialize(variable)
9
+ @key = variable.first
10
+
11
+ value = variable.last
12
+ @value = value
13
+
14
+ return unless value.start_with?("$")
15
+
16
+ env_key = value.split("$").last
17
+ env_value = ENV.fetch(env_key, nil)
18
+
19
+ raise VariableError, "no value set for #{env_key}", [] if env_value.nil?
20
+
21
+ @value = env_value
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Seijaku
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/seijaku.rb CHANGED
@@ -5,12 +5,16 @@ require_relative "seijaku/payload"
5
5
  require_relative "seijaku/variable"
6
6
  require_relative "seijaku/task"
7
7
  require_relative "seijaku/step"
8
+ require_relative "seijaku/ssh/group"
9
+ require_relative "seijaku/ssh/host"
8
10
 
9
11
  require_relative "seijaku/executors/sh"
10
12
  require_relative "seijaku/executors/bash"
13
+ require_relative "seijaku/executors/ssh"
11
14
 
12
15
  module Seijaku
13
16
  class Error < StandardError; end
14
17
  class VariableError < Error; end
15
18
  class TaskError < Error; end
19
+ class SSHExecutorError < Error; end
16
20
  end
data/seijaku.gemspec CHANGED
@@ -32,9 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = %w[seijaku]
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- # Uncomment to register a new dependency of your gem
36
- # spec.add_dependency "example-gem", "~> 1.0"
37
-
38
- # For more information and examples about making a new gem, check out our
39
- # guide at: https://bundler.io/guides/creating_gem.html
35
+ spec.add_dependency "bcrypt_pbkdf", "~> 1.1"
36
+ spec.add_dependency "ed25519", "~> 1.3"
37
+ spec.add_dependency "net-ssh", "~> 7.2"
40
38
  end
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seijaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.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-11 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-12-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bcrypt_pbkdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ed25519
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-ssh
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '7.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '7.2'
13
55
  description: CLI tool ingesting YAML files to execute tasks on different shells with
14
56
  pre and post routines.
15
57
  email:
@@ -26,7 +68,19 @@ files:
26
68
  - README.md
27
69
  - Rakefile
28
70
  - bin/seijaku
71
+ - docs/1 - basic.md
72
+ - docs/2 - variables.md
73
+ - docs/3 - ssh.md
29
74
  - lib/seijaku.rb
75
+ - lib/seijaku/executors/bash.rb
76
+ - lib/seijaku/executors/sh.rb
77
+ - lib/seijaku/executors/ssh.rb
78
+ - lib/seijaku/payload.rb
79
+ - lib/seijaku/ssh/group.rb
80
+ - lib/seijaku/ssh/host.rb
81
+ - lib/seijaku/step.rb
82
+ - lib/seijaku/task.rb
83
+ - lib/seijaku/variable.rb
30
84
  - lib/seijaku/version.rb
31
85
  - seijaku.gemspec
32
86
  - sig/seijaku.rbs