consist 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/consist/cli.rb CHANGED
@@ -1,4 +1,17 @@
1
1
  require "thor"
2
+ require "sshkit"
3
+ require "sshkit/dsl"
4
+
5
+ require "consist/recipe"
6
+ require "consist/recipes"
7
+ require "consist/step"
8
+ require "consist/consistfile"
9
+ require "consist/commands/includes/stream_logger"
10
+ require "consist/commands/includes/erbable"
11
+ require "consist/commands/exec"
12
+ require "consist/commands/upload"
13
+ require "consist/commands/mutate"
14
+ require "consist/commands/check"
2
15
 
3
16
  module Consist
4
17
  class CLI < Thor
@@ -6,9 +19,33 @@ module Consist
6
19
 
7
20
  map %w[-v --version] => "version"
8
21
 
9
- desc "version", "Display consist version", hide: true
22
+ desc "version", "Display consist version"
10
23
  def version
11
24
  say "consist/#{VERSION} #{RUBY_DESCRIPTION}"
12
25
  end
26
+
27
+ desc "lightup", "Attempt to connect to a server and execute an idempotent statement."
28
+ def lightup(user, server)
29
+ puts "---> Attempting to connect to #{server} as #{user}"
30
+ on("#{user}@#{server}") do
31
+ as user do
32
+ execute "true"
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "scaffold", "Apply a given recipe to (a) server(s)"
38
+ def scaffold(_recipe, server_ip)
39
+ Consist::Recipes.new(server_ip)
40
+ end
41
+
42
+ option :step, type: :string
43
+ option :consistfile, type: :string
44
+ desc "up", "Run a Consistfile against a server"
45
+ def up(server_ip)
46
+ specified_step = options[:step]
47
+ consistfile = options[:consistfile]
48
+ Consist::Consistfile.new(server_ip, consistfile:, specified_step:)
49
+ end
13
50
  end
14
51
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Check
6
+ def initialize(command)
7
+ @command = command
8
+ end
9
+
10
+ def perform!(executor)
11
+ status = @command[:status]
12
+
13
+ flag, val = if @command.has_key?(:path) && !@command[:path].nil?
14
+ ["d", @command[:path]]
15
+ else
16
+ ["f", @command[:file]]
17
+ end
18
+
19
+ exists = executor.test("[ -#{flag} #{val} ]")
20
+
21
+ if status == :exist && !exists
22
+ @command[:block].call
23
+ elsif status == :nonexistant && !exists
24
+ @command[:block].call
25
+ else
26
+ tense = if status == :exist
27
+ "should"
28
+ elsif status == :nonexistant
29
+ "shoudlnt"
30
+ end
31
+ puts "Checking path `#{status}` - `#{val}` - #{exists ? "exists" : "doesn't exist"} and #{tense} - skipping"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Exec
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ @command[:commands].each do
14
+ executor.execute(erb_template(_1), interaction_handler: Consist::Commands::StreamLogger.new,
15
+ **@command[:params])
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Consist
6
+ module Commands
7
+ module Erbable
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ def erb_template(contents)
13
+ b = binding
14
+ Consist.config.keys.each do |key|
15
+ b.local_variable_set(key, Consist.config[key])
16
+ end
17
+ ERB.new(contents).result(b)
18
+ end
19
+
20
+ module ClassMethods
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class StreamLogger
6
+ def initialize(log_level = :info)
7
+ @log_level = log_level
8
+ end
9
+
10
+ def on_data(_command, _stream_name, data, _channel)
11
+ log(data)
12
+ end
13
+
14
+ private
15
+
16
+ def log(message)
17
+ SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Mutate
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ delim = @command[:delim]
14
+ target_file = @command[:target_file]
15
+ target_string = erb_template(@command[:target_string])
16
+ match = erb_template(@command[:match])
17
+
18
+ case @command[:mode]
19
+ when :replace
20
+ cmd = "sed -i -E 's#{delim}#{match}#{delim}#{target_string}#{delim}' #{target_file} "
21
+ end
22
+
23
+ executor.execute(cmd, interaction_handler: Consist::Commands::StreamLogger.new)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Upload
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ # rubocop:disable Style/ClassEqualityComparison
14
+ if @command[:local_file].class == Symbol
15
+ puts "---> Uploading defined file #{@command[:local_file]}"
16
+ target_file = Consist.files.detect { |f| f[:id] == @command[:local_file] }
17
+ raise "\n\nNo declared file of ID `#{@command[:local_file]}`" unless target_file
18
+
19
+ contents = StringIO.new(erb_template(target_file[:contents]))
20
+ upload_defined_file(executor, contents, @command[:remote_path])
21
+ else
22
+ local_path = File.expand_path("../steps/#{@id}/#{@command[:local_file]}", __dir__)
23
+ upload(executor, local_path, @command[:remote_path])
24
+ end
25
+ # rubocop:enable Style/ClassEqualityComparison
26
+ end
27
+
28
+ def upload(executor, local_path, remote_path)
29
+ executor.send(:upload!, local_path, remote_path, interaction_handler: Consist::Commands::StreamLogger.new)
30
+ end
31
+
32
+ def upload_defined_file(executor, contents, remote_path)
33
+ executor.upload! contents, remote_path, interaction_handler: Consist::Commands::StreamLogger.new
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true37236
2
+
3
+ module Consist
4
+ class << self
5
+ attr_accessor :files, :config
6
+ end
7
+
8
+ @files = []
9
+ @config = {}
10
+
11
+ class Consistfile
12
+ include SSHKit::DSL
13
+
14
+ def initialize(server_ip, specified_step:, consistfile:)
15
+ @server_ip = server_ip
16
+ @specified_step = specified_step
17
+ consistfile_path = if consistfile
18
+ File.expand_path(consistfile, Dir.pwd)
19
+ else
20
+ File.expand_path("Consistfile", Dir.pwd)
21
+ end
22
+
23
+ consistfile_contents = File.read(consistfile_path)
24
+ instance_eval(consistfile_contents)
25
+ end
26
+
27
+ def consist(&definition)
28
+ instance_eval(&definition)
29
+ end
30
+
31
+ def recipe(id, &definition)
32
+ recipe = Consist::Recipe.new(id, &definition)
33
+
34
+ puts "Executing Recipe: #{recipe.name}"
35
+
36
+ if @specified_step.nil?
37
+ recipe.steps.each { exec_step(_1) }
38
+ else
39
+ puts "Specific step targeted: #{@specified_step.to_sym}"
40
+ specified_step, *_rest = recipe.steps.select { _1.id === @specified_step.to_sym }
41
+ raise "Step with id #{@specified_step.to_sym} not found." unless specified_step
42
+
43
+ exec_step(specified_step)
44
+ end
45
+
46
+ puts "Execution of #{recipe.name} has completed."
47
+ end
48
+
49
+ def file(id, &definition)
50
+ return unless definition
51
+
52
+ contents = yield
53
+
54
+ Consist.files << {id:, contents:}
55
+ end
56
+
57
+ def config(id, val)
58
+ Consist.config[id] = val
59
+ end
60
+
61
+ private
62
+
63
+ def exec_step(specified_step)
64
+ puts "Executing Step: #{specified_step.name}"
65
+
66
+ on("#{specified_step.required_user}@#{@server_ip}") do
67
+ specified_step.perform(self)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ class Recipe
5
+ def initialize(_id = nil, &definition)
6
+ @steps = []
7
+ instance_eval(&definition)
8
+ end
9
+
10
+ def name(name = nil)
11
+ @name = name if name
12
+ @name
13
+ end
14
+
15
+ def description(description = nil)
16
+ @description = description if description
17
+ @description
18
+ end
19
+
20
+ def user(user = nil)
21
+ @user = user if @user
22
+ @user
23
+ end
24
+
25
+ def steps(&block)
26
+ instance_eval(&block) if block
27
+ @steps
28
+ end
29
+
30
+ def step(step_name, &block)
31
+ if block
32
+ @steps << Step.new(id: step_name, &block)
33
+ return
34
+ end
35
+
36
+ target_path = File.join("../../", "steps", step_name.to_s, "step.rb")
37
+ step_path = File.expand_path(target_path, __FILE__)
38
+ step_content = File.read(step_path)
39
+ @steps << Step.new(id: step_name) { instance_eval(step_content) }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ class Recipes
5
+ include SSHKit::DSL
6
+
7
+ def initialize(server_ip)
8
+ recipe_directory = File.expand_path("../recipes", __dir__)
9
+ recipes = Dir[File.join(recipe_directory, "*.rb")]
10
+
11
+ recipes.each do |recipe_file|
12
+ recipe_content = File.read(recipe_file)
13
+ recipe = Recipe.new { instance_eval(recipe_content) }
14
+
15
+ puts "Executing Recipe: #{recipe.name}"
16
+ recipe.steps.each do |step|
17
+ puts "Executing Step: #{step.name}"
18
+
19
+ on("#{step.required_user}@#{server_ip}") do
20
+ step.perform(self)
21
+ end
22
+ end
23
+
24
+ puts "Execution of #{recipe.name} has completed."
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Consist
6
+ class Step
7
+ include SSHKit::DSL
8
+
9
+ def initialize(id:, &block)
10
+ @commands = []
11
+ @id = id
12
+ @required_user = :root
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def id(id = nil)
17
+ @id = id if id
18
+ @id
19
+ end
20
+
21
+ def name(name = nil)
22
+ @name = name if name
23
+ @name
24
+ end
25
+
26
+ def required_user(user = nil)
27
+ @required_user = user if user
28
+ @required_user
29
+ end
30
+
31
+ def shell(message = "", params: {})
32
+ return unless block_given?
33
+
34
+ command = yield
35
+ commands = command.split(/(?<!\\)\n/).select { !_1.start_with?("#") }.compact
36
+
37
+ @commands << {message:, type: :exec, commands:, params:}
38
+ end
39
+
40
+ def mutate_file(mode:, target_file:, match:, target_string:, delim: "/", message: "")
41
+ @commands << {type: :mutate, mode:, message:, match:, target_file:, delim:, target_string:}
42
+ end
43
+
44
+ def upload_file(local_file:, remote_path:, message: "")
45
+ @commands << {message:, type: :upload, local_file:, remote_path:}
46
+ end
47
+
48
+ def check(status:, path: nil, file: nil, message: "", &block)
49
+ @commands << {type: :check, message:, status:, file:, path:, block: -> { instance_eval(&block) }}
50
+ end
51
+
52
+ def perform(executor)
53
+ @commands.each do |command|
54
+ banner(command[:message]) unless command[:message].empty?
55
+
56
+ execable = Object.const_get("Consist::Commands::#{command[:type].capitalize}").new(command)
57
+ executor.as @required_user do
58
+ execable.perform!(executor)
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def banner(message)
66
+ return if message.empty?
67
+
68
+ msg = "********* #{message} ********"
69
+ puts "*" * msg.length
70
+ puts msg
71
+ puts "*" * msg.length
72
+ end
73
+ end
74
+ end
@@ -28,12 +28,12 @@ module Consist
28
28
  base.check_unknown_options!
29
29
  end
30
30
 
31
- def start(given_args=ARGV, config={})
31
+ def start(given_args = ARGV, config = {})
32
32
  config[:shell] ||= Thor::Base.shell.new
33
33
  handle_help_switches(given_args) do |args|
34
34
  dispatch(nil, args, nil, config)
35
35
  end
36
- rescue StandardError => e
36
+ rescue => e
37
37
  handle_exception_on_start(e, config)
38
38
  end
39
39
 
@@ -1,3 +1,3 @@
1
1
  module Consist
2
- VERSION = "0.1.0".freeze
2
+ VERSION = '0.1.1'.freeze
3
3
  end
@@ -0,0 +1,8 @@
1
+ name "Kamal Single Server"
2
+ description "Sets up a single server to run Kamal"
3
+ user :root
4
+
5
+ steps do
6
+ step :update_apt_packages
7
+ step :install_apt_packages
8
+ end
@@ -0,0 +1,19 @@
1
+ name "Install APT packages"
2
+ required_user :root
3
+
4
+ shell "Installing essential packages" do
5
+ <<~EOS
6
+ apt-get -y remove systemd-timesyncd
7
+ timedatectl set-ntp no
8
+ apt-get -y install build-essential curl fail2ban git ntp vim
9
+ apt-get autoremove
10
+ apt-get autoclean
11
+ EOS
12
+ end
13
+
14
+ shell "Start NTP and Fail2Ban" do
15
+ <<~EOS
16
+ service ntp restart
17
+ service fail2ban restart
18
+ EOS
19
+ end
@@ -0,0 +1,3 @@
1
+ APT::Periodic::AutocleanInterval "7";
2
+ APT::Periodic::Update-Package-Lists "1";
3
+ APT::Periodic::Unattended-Upgrade "1";
@@ -0,0 +1,11 @@
1
+ name "Update the APT packages"
2
+ required_user :root
3
+
4
+ upload_file message: "Uploading APT config...", local_file: "apt_auto_upgrades",
5
+ remote_path: "/etc/apt/apt.conf.d/20auto-upgrades"
6
+
7
+ shell do
8
+ <<~EOS
9
+ apt-get update && apt-get upgrade -y
10
+ EOS
11
+ end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John McDowall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-20 00:00:00.000000000 Z
11
+ date: 2023-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sshkit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.21'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.21'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: thor
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -26,7 +54,7 @@ dependencies:
26
54
  version: '1.2'
27
55
  description:
28
56
  email:
29
- - j@jmd.fm
57
+ - john@kantan.io
30
58
  executables:
31
59
  - consist
32
60
  extensions: []
@@ -37,11 +65,25 @@ files:
37
65
  - exe/consist
38
66
  - lib/consist.rb
39
67
  - lib/consist/cli.rb
68
+ - lib/consist/commands/check.rb
69
+ - lib/consist/commands/exec.rb
70
+ - lib/consist/commands/includes/erbable.rb
71
+ - lib/consist/commands/includes/stream_logger.rb
72
+ - lib/consist/commands/mutate.rb
73
+ - lib/consist/commands/upload.rb
74
+ - lib/consist/consistfile.rb
75
+ - lib/consist/recipe.rb
76
+ - lib/consist/recipes.rb
77
+ - lib/consist/step.rb
40
78
  - lib/consist/thor_ext.rb
41
79
  - lib/consist/version.rb
80
+ - lib/recipes/kamal_single_server.rb
81
+ - lib/steps/install_apt_packages/step.rb
82
+ - lib/steps/update_apt_packages/apt_auto_upgrades
83
+ - lib/steps/update_apt_packages/step.rb
42
84
  homepage: https://github.com/johnmcdowall/consist
43
85
  licenses:
44
- - MIT
86
+ - LGPL-3.0
45
87
  metadata:
46
88
  bug_tracker_uri: https://github.com/johnmcdowall/consist/issues
47
89
  changelog_uri: https://github.com/johnmcdowall/consist/releases