Skip to content

Instantly share code, notes, and snippets.

@SamantazFox
Last active March 13, 2023 21:35
Show Gist options
  • Save SamantazFox/6d19b8df73c1cda26e3a950da823a052 to your computer and use it in GitHub Desktop.
Save SamantazFox/6d19b8df73c1cda26e3a950da823a052 to your computer and use it in GitHub Desktop.
Extremely lightweight ansible-compatible implementation which allows semi-automatic deployment of config files on a remote server
# Lightweight Ansible-compatible parser
#
# This tool generates one shell file per playbook placed in the ./ansible folder
# Only partial support for the `copy` and `file` builtins is implemented.
#
# Note: `become` is not supported, so all generated files MUST be run as root.
#
# To execute, run `crystal run ansible-light.cr` in your terminal or use the shell file
# provided below (requires the Crystal compiler).
#
# Released under the public domain (CC0 license) by SamantazFox @ 2023
#
require "yaml"
require "digest/sha256"
require "compress/gzip"
STARS = "***********************************************************************************"
def builtin_copy(str : String::Builder, node : YAML::Any, idx : Int)
src = node["src"].as_s
dst = node["dest"].as_s
backup = node["backup"]?.try &.as_bool || false
owner = node["owner"]?.try &.as_s
group = node["group"]?.try &.as_s
mode = node["mode"]?.try &.as_s
io = IO::Memory.new
gzip = Compress::Gzip::Writer.new(io)
gzip << File.read(src)
gzip.close
file_base64 = Base64.encode(io).strip
expected_sha256 = Digest::SHA256.new.file(src).final.hexstring.downcase
str << "dest_#{idx}=" << dst.dump << '\n'
str << "sha256_#{idx}=$(sha256 \"$dest_#{idx}\")\n"
str << %(if ! [ "$sha256_#{idx}" = "#{expected_sha256}" ]; then\n)
str << %( if [ -f "$dest_#{idx}" ]; then mv "$dest_#{idx}" "$dest_#{idx}.bak"; fi\n) if backup
str << <<-STR
cat << EOF_#{idx} | base64 -d | gunzip -c > "$dest_#{idx}"
#{file_base64}
EOF_#{idx}
STR
str << "\nfi\n"
str << %(chown #{owner}:#{group} "$dest_#{idx}"\n) if owner || group
str << %(chmod #{mode} "$dest_#{idx}"\n) if mode
end
def builtin_file(str : String::Builder, node : YAML::Any, idx : Int)
state = node["state"]?.try &.as_s.downcase
dest = node["path"].try &.as_s || node["dest"].as_s
str << "dest_#{idx}=" << dest.dump << '\n'
owner = node["owner"]?.try &.as_s
group = node["group"]?.try &.as_s
mode = node["mode"]?.try &.as_s
case state
when "absent"
# directories will be recursively deleted, and files or symlinks will be unlinked
str << %(if [ -d "$dest_#{idx}" ]; then rm -r "$dest_#{idx}"; fi\n)
str << %(if [ -e "$dest_#{idx}" ]; then rm "$dest_#{idx}"; fi\n)
when "directory"
recurse = node["recurse"]?.try &.as_bool || false
str << %(if ! [ -e "$dest_#{idx}" ]; then\n)
str << " mkdir "
str << "-p " if recurse
str << "-m #{mode} " if mode
str << "\"$dest_#{idx}\"\n"
str << <<-STR
elif ! [ -d "$dest_#{idx}" ]; then
echo "Error: \\"$dest_#{idx}\\" exists but is not a directory"
fi
STR
when "file"
str << %(chown #{owner}:#{group} "$dest_#{idx}"\n) if owner || group
str << %(chmod #{mode} "$dest_#{idx}"\n) if mode
when "hard"
# the hard link will be created or changed.
src = node["src"].as_s
str << %(ln "#{src}" "$dest_#{idx}"\n)
when "link"
# the symbolic link will be created or changed.
src = node["src"].as_s
force = node["force"].try &.as_bool || false
str << "ln -s "
str << "-f " if force
str << %("#{src}" "$dest_#{idx}"\n)
when "touch"
follow = node["follow"]?.try &.as_bool || true
atime = node["access_time"].try &.as_s
mtime = node["modification_time"].try &.as_s
str << %(if [ -e "$dest_#{idx}" ] || [ -d "$dest_#{idx}" ]; then\n)
if atime == mtime && atime != "preserve"
str << " touch "
str << "-h " if !follow
str << %("$dest_#{idx}"\n)
else
if atime != "preserve"
str << " touch -a "
str << "-h " if !follow
str << "-t " << atime << ' ' if atime != "now"
str << %("$dest_#{idx}"\n)
end
if mtime != "preserve"
str << " touch -m "
str << "-h " if !follow
str << "-t " << mtime << ' ' if mtime != "now"
str << %("$dest_#{idx}"\n)
end
end
else
# update permissions
str << %(chown #{owner}:#{group} "$dest_#{idx}"\n) if owner || group
str << %(chmod #{mode} "$dest_#{idx}"\n) if mode
end
end
def run_playbook(file : Path)
filename = file.basename.rchop(".yaml").rchop(".yml")
yaml = YAML.parse(File.read(file)).as_a[0]
name = yaml["name"].as_s
tasks = yaml["tasks"].as_a
content = String.build do |str|
str << <<-SH
#!/bin/sh
CLR_NONE='\033[0m'
CLR_RED='\033[0;31m'
CLR_GRN='\033[0;32m'
CLR_GRY='\033[0;37m'
CLR_WHITE='\033[1;37m'
sha256() {
if [ -d "$1" ]; then return 1
elif [ -e "$1" ]; then sha256sum "$1" | sed -E 's/^(\\w+).+$/\\1/'
else echo ""
fi
}
SH
str << "\n\n"
# Playbook name
str << %(echo "${CLR_GRY}PLAY [${CLR_WHITE}) << name.dump_unquoted << "${CLR_GRY}] "
str << STARS[name.size..] << %(${CLR_NONE}"; echo\n\n\n)
tasks.each_with_index do |entry, idx|
# task name
str << %(echo "${CLR_GRY}TASK [${CLR_WHITE}) << entry["name"].as_s.dump_unquoted << "${CLR_GRY}] "
str << STARS[entry["name"].as_s.size..] << %(${CLR_NONE}"\n)
# Error handling
str << "(\n"
str << " set -euo pipefail\n\n"
if node = entry["ansible.builtin.copy"]?
builtin_copy(str, node, idx)
elsif node = entry["ansible.builtin.file"]?
builtin_file(str, node, idx)
end
# End of error handling + print status
str << ") && echo ${CLR_GRN}OK${CLR_NONE} || echo ${CLR_RED}NOK${CLR_NONE}; echo\n\n"
end
end
# Write output file
output = Path.new("_ansible-bin", "#{filename}.sh")
File.write(output, content)
puts "Generated #{output}"
end
destdir = Path.new(".", "/_ansible-bin")
Dir.mkdir(destdir) if !Dir.exists?(destdir)
basedir = Path.new(".", "ansible")
folder = Dir.new(basedir)
folder.children.sort.each do |filename|
next if !filename.ends_with?(/\.ya?ml/)
puts "Parsing #{filename}"
run_playbook(basedir / filename)
end
#!/bin/sh
#
# Deploy scipt for the lightweith Ansible generator above
#
# This creates a .tgz archive, which is pushed and decompressed to the /root
# directory of 'SERVER' using SSH. The resulting shell scripts must be run
# manually.
#
# This script assumes that 'SERVER' has an entry in '.ssh/config' and that
# the remote server has your pubkey in its '/root/.ssh/authorized_keys' file.
#
# Released under the public domain (CC0 license) by SamantazFox @ 2023
#
SERVER=my-server
crystal run ansible-light.cr
chmod +x _ansible-bin/*
echo; echo "Making archive..."
archive="_ansible-bin_$(date +%F_%H-%M-%S).tgz"
tar -czf $archive _ansible-bin
echo; echo "Deploying archive..."
scp $archive root@${SERVER}:/root/ansible-bin.tgz
ssh root@${SERVER} "cd /root; test -d _ansible-bin && rm -r _ansible-bin; tar -xzf ansible-bin.tgz"
echo; echo "Cleaning up..."
ssh root@${SERVER} "cd /root; rm -r ansible-bin.tgz"
rm $archive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment