koch 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e12ed1206655508810cc1607e3866754898e5a64c84ab85e1e7942357a8f9f3
4
+ data.tar.gz: 1cc5c71f2ac9081b75ba48621c21b60544d71057e4b67143bd5dc1d69a934e57
5
+ SHA512:
6
+ metadata.gz: 54ff19ca6f410c9c72787539ca4cadc1e98ed47957e32d1ac49288052915b962180bb783737401092c0ee4458418cabd29dcdd8496f125456ed2a5bb30d9f429
7
+ data.tar.gz: 39818ef03487967415fe4f03cf6174861ee56b6a37bf8fe11e7bed214989f012b965bba50d43e1dace13892cc5c73e1273239333a924b709f91e2638978558fa
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Marius Nünnerich
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Koch
2
+
3
+ Koch is a tool to install software packages, change files and other things
4
+ on a single machine. The changes are described in a file and are written in Ruby.
5
+
6
+ The file describing a machine should be versioned, that way you create a repeatable
7
+ description of how a machine is set up, with history.
8
+
9
+ For an example of how this can look like, check out this [Rezeptfile](example/Rezeptfile)
10
+ and the other files in the directory.
11
+
12
+ ## Notice
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
17
+
18
+ Fully **backup** any machine you run this on! This is alpha grade software and
19
+ might cause havoc, esp when run with root privileges!
20
+ I suggest you use Vagrant to try this out.
21
+
22
+ ## Status
23
+
24
+ ![Tests and Rubocop](https://github.com/marius/koch/actions/workflows/rubocop.yml/badge.svg)
25
+
26
+ ## Philosophy
27
+
28
+ Koch's configuration files (Rezeptfiles) are just Ruby. All Koch does is provide
29
+ a bunch of convenience functions. Feel free to use all the Ruby you want.
30
+
31
+ ## Usage
32
+
33
+ ```
34
+ sudo apt -y install git
35
+ sudo gem install koch
36
+ git clone git@github.com:example/machine.git
37
+ cd machine
38
+ sudo koch
39
+ ```
40
+
41
+ ## Supported platforms
42
+
43
+ - Ubuntu 22.04 (amd64)
44
+ - Debian 11 (amd64)
45
+ - End of list
46
+
47
+ ## TODO
48
+
49
+ - [ ] Interactive mode, ask about each change
50
+ - [ ] publish to rubygems
data/bin/koch ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "thor"
5
+ require_relative "../lib/koch"
6
+
7
+ # KochCLI is the glue between the commandline and the actual Koch implementation
8
+ class KochCLI < Thor
9
+ class_option :verbose, type: :boolean, aliases: "-v"
10
+ class_option :"dry-run", type: :boolean, aliases: "-d", default: true
11
+
12
+ desc "apply", "Apply a Rezeptfile to the local machine"
13
+ option :rezeptfile, aliases: "-f", default: "Rezeptfile"
14
+ def apply
15
+ logger.level = if options[:verbose]
16
+ Logger::DEBUG
17
+ else
18
+ Logger::INFO
19
+ end
20
+ @@dry_run = options[:"dry-run"]
21
+ r = Koch::Runner.new options[:rezeptfile]
22
+ r.go
23
+ end
24
+ default_task :apply
25
+ end
26
+
27
+ KochCLI.start(ARGV)
data/koch.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/koch/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "koch"
7
+ spec.version = Koch::VERSION
8
+ spec.authors = ["Marius Nuennerich"]
9
+ spec.email = ["marius@nuenneri.ch"]
10
+ spec.summary = "Koch automates machine setup."
11
+ spec.description = "Koch automates machine setup by providing a library of helper functions."
12
+ spec.homepage = "https://github.com/marius/koch"
13
+ spec.license = "Apache-2.0"
14
+
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+ spec.files = ["README.md", "koch.gemspec", "LICENSE"]
17
+ spec.files += Dir.glob("lib/**/*.rb")
18
+ spec.executables = ["koch"]
19
+
20
+ spec.add_runtime_dependency "diffy", "~> 3.4"
21
+ spec.add_runtime_dependency "logger", "~> 1.5"
22
+ spec.add_runtime_dependency "thor", "~> 1.2"
23
+ spec.add_runtime_dependency "zeitwerk", "~> 2.6"
24
+ spec.add_runtime_dependency "zlib", "~> 1.1"
25
+ spec.add_development_dependency "guard", "~> 2.18"
26
+ spec.add_development_dependency "guard-minitest", "~> 2.4"
27
+ spec.add_development_dependency "rake", "~> 13.0"
28
+ spec.add_development_dependency "rubocop", "~> 1.48"
29
+ spec.add_development_dependency "rubocop-rake", "~> 0.6"
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Koch
6
+ # Creates a directory if it doesn't exist and/or changes its mode/owner/group
7
+ class CreateDirectory < Resource
8
+ dsl_writer :mode, :owner, :group
9
+
10
+ include Ogm
11
+
12
+ def apply!
13
+ if Dir.exist? name
14
+ debug "Not creating directory #{name}, it already exists"
15
+ else
16
+ @changed = true
17
+ maybe("Creating directory #{name}") do
18
+ FileUtils.mkdir_p name
19
+ end
20
+ end
21
+
22
+ apply_owner
23
+ apply_group
24
+ apply_mode
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Creates and/or changes a files' contents, mode, owner and/or group
5
+ class CreateFile < Resource
6
+ dsl_writer :mode, :contents, :owner, :group
7
+
8
+ include Ogm
9
+
10
+ def apply!
11
+ apply_contents
12
+ apply_owner
13
+ apply_group
14
+ apply_mode
15
+ end
16
+
17
+ private
18
+
19
+ def apply_contents
20
+ if contents.nil?
21
+ debug "No contents specified for file #{name}"
22
+ return
23
+ end
24
+
25
+ old_contents = begin
26
+ File.read name
27
+ rescue Errno::ENOENT
28
+ nil
29
+ end
30
+ if old_contents == contents
31
+ debug "Contents of file #{name} unchanged"
32
+ return
33
+ end
34
+
35
+ info "Diff for #{name}:"
36
+ info diff(old_contents, contents)
37
+ @changed = true
38
+ maybe("Updating file contents of #{name}") do
39
+ File.write(name, contents)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Koch
6
+ # Deletes a directory if it exists
7
+ class DeleteDirectory < Resource
8
+ def apply!
9
+ unless Dir.exist? name
10
+ debug "Not deleting directory #{name}, it does not exist"
11
+ return
12
+ end
13
+
14
+ @changed = true
15
+ maybe("Delete directory #{name}") do
16
+ FileUtils.remove_entry_secure name
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Deletes a file if it exists
5
+ class DeleteFile < Resource
6
+ def apply!
7
+ unless File.exist? name
8
+ debug "Not deleting file #{name}, it does not exist"
9
+ return
10
+ end
11
+
12
+ @changed = true
13
+ maybe("Delete file #{name}") do
14
+ File.delete name
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/koch/group.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Represents a Linux group
5
+ class Group < Resource
6
+ dsl_writer :gid, :system_group
7
+
8
+ def apply!
9
+ if exist? name
10
+ debug "Group #{name} already exists"
11
+ return
12
+ end
13
+
14
+ @changed = true
15
+
16
+ params = +""
17
+ params << " --gid #{gid}" if gid
18
+ params << " --system" if system_group
19
+
20
+ maybe "groupadd#{params} #{name}"
21
+ end
22
+
23
+ private
24
+
25
+ def exist?(group)
26
+ Etc.getgrnam group
27
+ true
28
+ rescue ArgumentError
29
+ false
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "diffy"
4
+
5
+ module Koch
6
+ # A random collection of helpers
7
+ module Helpers
8
+ @@dry_run = true
9
+
10
+ def maybe(msg_or_cmd)
11
+ if @@dry_run
12
+ info "DRY RUN: #{msg_or_cmd}"
13
+ else
14
+ info msg_or_cmd
15
+ if block_given?
16
+ yield
17
+ else
18
+ system msg_or_cmd, exception: true, err: :out
19
+ end
20
+ end
21
+ end
22
+
23
+ def diff(old, new)
24
+ Diffy::Diff.new(old, new, include_diff_info: true, context: 3).to_s(:color).lines[2..].join
25
+ end
26
+
27
+ def debian?
28
+ !File.read("/etc/issue").match(/Debian/).nil?
29
+ end
30
+
31
+ def ubuntu?
32
+ !File.read("/etc/issue").match(/Ubuntu/).nil?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # A list of packages
5
+ class Packages < Array
6
+ def apply!
7
+ ENV["DEBIAN_FRONTEND"] = "noninteractive"
8
+ maybe("apt -y install #{installs.join(" ")}") unless installs.empty?
9
+ maybe("apt -y purge #{deletes.join(" ")}") unless deletes.empty?
10
+ end
11
+
12
+ private
13
+
14
+ def installs
15
+ select { |pkg| pkg.is_a?(InstallPackage) && !installed_packages.include?(pkg.to_s) }
16
+ end
17
+
18
+ def deletes
19
+ select { |pkg| pkg.is_a?(DeletePackage) && installed_packages.include?(pkg.to_s) }
20
+ end
21
+
22
+ def installed_packages
23
+ return @installed_packages if @installed_packages
24
+
25
+ @installed_packages = `dpkg -l`.lines.grep(/^ii/).map { |pkg| pkg.split[1] }
26
+ debug "Installed deb packages: "
27
+ debug @installed_packages.join(" ")
28
+ @installed_packages
29
+ end
30
+ end
31
+
32
+ # Represents an abstract package resource, can not be used on its own
33
+ class AbstractPackage < Resource
34
+ def to_s
35
+ name
36
+ end
37
+ end
38
+
39
+ # Installs a package
40
+ class InstallPackage < AbstractPackage
41
+ end
42
+
43
+ # Deletes a package
44
+ class DeletePackage < AbstractPackage
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Koch
6
+ # A list of snap packages
7
+ class SnapPackages < Array
8
+ def apply!
9
+ # We cannot install from multiple stores in one go, let's split the installs up.
10
+ installs.each do |pkg|
11
+ maybe("snap install --classic #{pkg}")
12
+ end
13
+ maybe("snap remove #{deletes.join(" ")}") unless deletes.empty?
14
+ end
15
+
16
+ private
17
+
18
+ def installs
19
+ select { |pkg| pkg.is_a?(InstallSnapPackage) && !installed_packages.include?(pkg.to_s) }
20
+ end
21
+
22
+ def deletes
23
+ select { |pkg| pkg.is_a?(DeleteSnapPackage) && installed_packages.include?(pkg.to_s) }
24
+ end
25
+
26
+ def installed_packages
27
+ return @installed_packages if @installed_packages
28
+
29
+ @installed_packages = `snap list`.lines[1..].map { |pkg| pkg.split[0] }
30
+ debug "Installed snap packages: "
31
+ debug @installed_packages.join(" ")
32
+ @installed_packages
33
+ end
34
+ end
35
+
36
+ # Represents an abstract package resource, can not be used on its own
37
+ class AbstractSnapPackage < Resource
38
+ def to_s
39
+ name
40
+ end
41
+ end
42
+
43
+ # Installs a snap package
44
+ class InstallSnapPackage < AbstractPackage
45
+ end
46
+
47
+ # Deletes a snap package
48
+ class DeleteSnapPackage < AbstractPackage
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Koch
6
+ # LogHelper provides simple logging for the whole program
7
+ module LogHelper
8
+ @@logger = Logger.new($stdout)
9
+ @@logger.formatter = proc do |_severity, datetime, _progname, msg|
10
+ _ts = datetime.strftime("%F %T")
11
+ # format("%s%s %s\n", severity[0], ts, msg)
12
+ "#{msg}\n"
13
+ end
14
+
15
+ def logger
16
+ @@logger
17
+ end
18
+
19
+ def debug(*args)
20
+ @@logger.debug(*args)
21
+ end
22
+
23
+ def info(*args)
24
+ @@logger.info(*args)
25
+ end
26
+
27
+ def warn(*args)
28
+ @@logger.warn(*args)
29
+ end
30
+
31
+ def error(*args)
32
+ @@logger.error(*args)
33
+ end
34
+
35
+ def fatal(*args)
36
+ @@logger.fatal(*args)
37
+ exit 1
38
+ end
39
+ end
40
+ end
data/lib/koch/ogm.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Koch
6
+ # This module applies changes to of file's or directory's owner, group, mode.
7
+ module Ogm
8
+ def apply_owner
9
+ return if @owner.nil?
10
+
11
+ @owner = Etc.getpwnam(@owner).uid if @owner.is_a? String
12
+ return if stat.uid == @owner
13
+
14
+ @changed = true
15
+ maybe("Owner changed: #{name} #{stat.uid} to #{@owner}") do
16
+ File.chown @owner, nil, name
17
+ end
18
+ end
19
+
20
+ def apply_group
21
+ return if @group.nil?
22
+
23
+ @group = Etc.getgrnam(@group).gid if @group.is_a? String
24
+ return if stat.gid == @group
25
+
26
+ @changed = true
27
+ maybe("Group changed: #{name} #{stat.gid} to #{@group}") do
28
+ File.chown nil, @group, name
29
+ end
30
+ end
31
+
32
+ def apply_mode
33
+ return if @mode.nil?
34
+
35
+ @mode = Integer(@mode, 8) if @mode.is_a? String
36
+ curr_mode = stat.mode & 0o7777
37
+ return if curr_mode == @mode
38
+
39
+ @changed = true
40
+ maybe(format("Mode changed: #{name} old: %o new: %o", (curr_mode || 0), @mode)) do
41
+ File.chmod @mode, name
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def stat
48
+ @stat ||= begin
49
+ File.stat name
50
+ rescue Errno::ENOENT
51
+ Struct.new(:uid, :gid, :mode).new
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Resource represents a thing in the system that can be changed by Koch
5
+ class Resource
6
+ attr_reader :changed, :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+
12
+ def self.dsl_writer(*syms)
13
+ syms.each do |sym|
14
+ define_method sym do |*args|
15
+ if args.empty?
16
+ instance_variable_get "@#{sym}"
17
+ elsif args.size == 1
18
+ instance_variable_set "@#{sym}", args[0]
19
+ else
20
+ instance_variable_set "@#{sym}", args
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ dsl_writer :reload, :restart, :on_change
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Represents a list of resources
5
+ class Resources < Array
6
+ def initialize
7
+ super
8
+ @reloads = []
9
+ @restarts = []
10
+ @on_changes = []
11
+ end
12
+
13
+ def apply!
14
+ each do |r|
15
+ r.apply!
16
+ next unless r.changed
17
+
18
+ @reloads << r.reload
19
+ @restarts << r.restart
20
+ @on_changes << r.on_change
21
+ end
22
+ end
23
+
24
+ def reloads
25
+ @reloads.compact.flatten.uniq
26
+ end
27
+
28
+ def restarts
29
+ @restarts.compact.flatten.uniq
30
+ end
31
+
32
+ def on_changes
33
+ @on_changes.compact.flatten.uniq
34
+ end
35
+ end
36
+ end
data/lib/koch/run.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # A single command to run
5
+ class Run < Resource
6
+ dsl_writer :command
7
+
8
+ def apply!
9
+ @changed = true
10
+ maybe(command || name)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ # Creates a single swap file
5
+ class Swapfile < Resource
6
+ dsl_writer :size
7
+
8
+ def initialize(name)
9
+ super
10
+ @size = "1G"
11
+ end
12
+
13
+ def apply!
14
+ if File.exist? name
15
+ debug "Swap file #{name} already exists"
16
+ return
17
+ end
18
+
19
+ @changed = true
20
+
21
+ maybe "fallocate -l #{size} #{name}"
22
+ maybe "Chmod 600 swap file #{name}" do
23
+ File.chmod 0o600, name
24
+ end
25
+ maybe "mkswap #{name}"
26
+ maybe "swapon #{name}"
27
+ maybe "Add swap file #{name} to /etc/fstab" do
28
+ File.write("/etc/fstab", "#{name} none swap sw 0 0\n", mode: "a")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Koch
6
+ # A Systemd service
7
+ class SystemdService < Resource
8
+ dsl_writer :contents
9
+
10
+ def apply!
11
+ fatal "Systemd services require contents." if contents.nil?
12
+
13
+ full_name = "/etc/systemd/system/#{name}.service"
14
+ old_contents = begin
15
+ File.read(full_name)
16
+ rescue Errno::ENOENT
17
+ nil
18
+ end
19
+ if old_contents == contents
20
+ debug "Systemd service #{name} unchanged"
21
+ return
22
+ end
23
+
24
+ @changed = true
25
+
26
+ info "Diff for Systemd service #{name}:"
27
+ info diff(old_contents, contents)
28
+ maybe("Changed Systemd service #{name}") do
29
+ File.write(full_name, contents)
30
+ end
31
+ if system("systemctl is-enabled --quiet #{name}.service")
32
+ maybe "systemctl daemon-reload"
33
+ else
34
+ maybe "systemctl enable --now #{name}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require_relative "resource"
5
+
6
+ module Koch
7
+ # A systemd service that is triggered by a timer
8
+ class SystemdTimerService < Resource
9
+ dsl_writer :contents, :timer
10
+
11
+ def initialize(name)
12
+ super
13
+ # Pick a pseudo-random time between 00:00 and 06:00 (360 minutes)
14
+ hash = Zlib.crc32(name) % 360
15
+ hour = hash / 60
16
+ minute = hash % 60
17
+ @timer = format("OnCalendar=%02d:%02d", hour, minute)
18
+ end
19
+
20
+ # rubocop:disable Metrics/AbcSize
21
+ # rubocop:disable Metrics/MethodLength
22
+ # rubocop:disable Metrics/CyclomaticComplexity
23
+ # rubocop:disable Metrics/PerceivedComplexity
24
+ def apply!
25
+ fatal "Systemd services require contents." if contents.nil?
26
+
27
+ name_prefix = "/etc/systemd/system/#{name}"
28
+ old_contents = begin
29
+ File.read("#{name_prefix}.service")
30
+ rescue Errno::ENOENT
31
+ nil
32
+ end
33
+
34
+ old_timer_contents = begin
35
+ File.read("#{name_prefix}.timer")
36
+ rescue Errno::ENOENT
37
+ nil
38
+ end
39
+
40
+ new_timer_contents = <<~TIMER
41
+ [Unit]
42
+ Description=Run #{name} regularly
43
+
44
+ [Timer]
45
+ #{@timer}
46
+
47
+ [Install]
48
+ WantedBy=timers.target
49
+ TIMER
50
+ if old_contents == contents && old_timer_contents == new_timer_contents
51
+ debug "Systemd #{name} service and timer unchanged"
52
+ return
53
+ end
54
+
55
+ @changed = true
56
+
57
+ if old_contents != contents
58
+ info "Diff for Systemd service #{name}:"
59
+ info diff(old_contents, contents)
60
+ maybe("Changed Systemd service #{name}") do
61
+ File.write("#{name_prefix}.service", contents)
62
+ end
63
+ end
64
+
65
+ if old_timer_contents != new_timer_contents
66
+ info "Diff for Systemd timer #{name}:"
67
+ info diff(old_timer_contents, new_timer_contents)
68
+
69
+ maybe("Changed Systemd timer #{name}") do
70
+ File.write("#{name_prefix}.timer", new_timer_contents)
71
+ end
72
+ end
73
+
74
+ if system("systemctl is-enabled --quiet #{name}.timer")
75
+ maybe "systemctl daemon-reload"
76
+ else
77
+ maybe "systemctl enable --now #{name}.timer"
78
+ end
79
+ end
80
+ # rubocop:enable Metrics/AbcSize
81
+ # rubocop:enable Metrics/MethodLength
82
+ # rubocop:enable Metrics/CyclomaticComplexity
83
+ # rubocop:enable Metrics/PerceivedComplexity
84
+ end
85
+ end
data/lib/koch/user.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Koch
6
+ # Represents a Linux user
7
+ class User < Resource
8
+ dsl_writer :uid, :gid, :home, :shell, :system_user
9
+
10
+ def apply!
11
+ if exist? name
12
+ debug "User #{name} already exists"
13
+ return
14
+ end
15
+
16
+ @changed = true
17
+
18
+ params = +""
19
+ params << " --uid #{uid}" if uid
20
+ params << " --gid #{gid}" if gid
21
+ params << " --home-dir #{home}" if home
22
+ params << " --shell #{shell}" if shell
23
+ params << " --system" if system_user
24
+
25
+ maybe "useradd#{params} #{name}"
26
+ end
27
+
28
+ private
29
+
30
+ def exist?(user)
31
+ Etc.getpwnam user
32
+ true
33
+ rescue ArgumentError
34
+ false
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koch
4
+ VERSION = "0.1.5"
5
+ end
data/lib/koch.rb ADDED
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ module Koch
8
+ # This class evaluates the Rezeptfile and applies the resources
9
+ class Runner
10
+ def initialize(rezeptfile)
11
+ @rezeptfile = rezeptfile
12
+
13
+ @packages = Packages.new
14
+ @snap_packages = SnapPackages.new
15
+
16
+ @resources = Resources.new
17
+
18
+ @delete = false
19
+ end
20
+
21
+ def go
22
+ info("Starting Koch runner (version #{VERSION}).")
23
+ info("DRY RUN MODE! Add --no-dry-run to apply changes.") if @@dry_run
24
+
25
+ eval_rezeptfile
26
+ apply!
27
+
28
+ @resources.reloads.each do |r|
29
+ maybe "systemctl reload #{r}"
30
+ end
31
+ @resources.restarts.each do |r|
32
+ maybe "systemctl restart #{r}"
33
+ end
34
+ @resources.on_changes.each do |r|
35
+ maybe r
36
+ end
37
+
38
+ info("Run complete.")
39
+ end
40
+
41
+ def self.make_method_body(add, delete = nil, collection = nil)
42
+ proc do |name, &block|
43
+ r = if @delete
44
+ fatal "Delete not supported for resource #{name}" if delete.nil?
45
+
46
+ delete.new(name)
47
+ else
48
+ add.new(name)
49
+ end
50
+
51
+ # block_given? does not work.
52
+ r.instance_eval(&block) if block
53
+
54
+ if collection.nil?
55
+ @resources << r
56
+ else
57
+ c = instance_variable_get(collection)
58
+ c << r
59
+ end
60
+ end
61
+ end
62
+
63
+ define_method :run, &make_method_body(Run)
64
+ define_method :file, &make_method_body(CreateFile, DeleteFile)
65
+ define_method :package, &make_method_body(InstallPackage, DeletePackage, :@packages)
66
+ define_method :snap_package, &make_method_body(InstallSnapPackage, DeleteSnapPackage, :@snap_packages)
67
+ define_method :directory, &make_method_body(CreateDirectory, DeleteDirectory)
68
+ define_method :systemd_service, &make_method_body(SystemdService)
69
+ define_method :systemd_timer_service, &make_method_body(SystemdTimerService)
70
+ define_method :swapfile, &make_method_body(Swapfile)
71
+ define_method :group, &make_method_body(Group)
72
+ define_method :user, &make_method_body(User)
73
+
74
+ private
75
+
76
+ def eval_rezeptfile
77
+ begin
78
+ rezepte = File.read(@rezeptfile)
79
+ rescue Errno::ENOENT, Errno::EISDIR
80
+ fatal "Did not find a file called #{@rezeptfile} in the current directory: #{Dir.pwd}"
81
+ end
82
+ instance_eval(rezepte)
83
+ end
84
+
85
+ def apply!
86
+ @packages.apply!
87
+ @snap_packages.apply!
88
+ @resources.apply!
89
+ end
90
+
91
+ def delete
92
+ old = @delete
93
+ @delete = true
94
+ yield
95
+ ensure
96
+ @delete = old
97
+ end
98
+ end
99
+ end
100
+
101
+ include Koch::LogHelper # rubocop:disable Style/MixinUsage
102
+ include Koch::Helpers # rubocop:disable Style/MixinUsage
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: koch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Marius Nuennerich
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: diffy
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: zlib
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.18'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.18'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '13.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '13.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.48'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.48'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-rake
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.6'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.6'
153
+ description: Koch automates machine setup by providing a library of helper functions.
154
+ email:
155
+ - marius@nuenneri.ch
156
+ executables:
157
+ - koch
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - LICENSE
162
+ - README.md
163
+ - bin/koch
164
+ - koch.gemspec
165
+ - lib/koch.rb
166
+ - lib/koch/create_directory.rb
167
+ - lib/koch/create_file.rb
168
+ - lib/koch/delete_directory.rb
169
+ - lib/koch/delete_file.rb
170
+ - lib/koch/group.rb
171
+ - lib/koch/helpers.rb
172
+ - lib/koch/install_package.rb
173
+ - lib/koch/install_snap_package.rb
174
+ - lib/koch/log_helper.rb
175
+ - lib/koch/ogm.rb
176
+ - lib/koch/resource.rb
177
+ - lib/koch/resources.rb
178
+ - lib/koch/run.rb
179
+ - lib/koch/swapfile.rb
180
+ - lib/koch/systemd_service.rb
181
+ - lib/koch/systemd_timer_service.rb
182
+ - lib/koch/user.rb
183
+ - lib/koch/version.rb
184
+ homepage: https://github.com/marius/koch
185
+ licenses:
186
+ - Apache-2.0
187
+ metadata: {}
188
+ post_install_message:
189
+ rdoc_options: []
190
+ require_paths:
191
+ - lib
192
+ required_ruby_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: 2.7.0
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ requirements: []
203
+ rubygems_version: 3.3.5
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Koch automates machine setup.
207
+ test_files: []