koch 0.1.5

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 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: []