seijaku 0.1.0 → 0.2.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: 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