gitzone

git-based zone management tool for static and dynamic domains
git clone https://git.parazyd.org/gitzone
Log | Files | Refs

commit 0ee7507d69e87c816eefdb0fcaf3db75f7ecf91a
Author: tg(x) <*@tg-x.net>
Date:   Thu,  3 Feb 2011 23:54:09 +0100

initial commit

Diffstat:
AREADME.org | 3+++
Abin/gitzone | 277+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/gitzone-shell | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aetc/gitzone.conf | 24++++++++++++++++++++++++
Ahooks/post-receive | 5+++++
Ahooks/pre-receive | 3+++
6 files changed, 361 insertions(+), 0 deletions(-)

diff --git a/README.org b/README.org @@ -0,0 +1,3 @@ +% git init zones +% cd zones +% git config receive.denyCurrentBranch ignore diff --git a/bin/gitzone b/bin/gitzone @@ -0,0 +1,277 @@ +#!/usr/bin/env perl +# +# gitzone by tg +# +# this program should be called from a pre-receive hook, it receives +# <old-value> SP <new-value> SP <ref-name> LF +# on STDIN for each branch, the push is rejected if the exit code is non-zero +# +# changed files are validated with named-checkzone and the push is rejected +# if there's an error in the zones specified in the config file ($ARGV[0]), +# if everything is OK, the zone files are copied to $zone_dir and the zone is +# reloaded with rndc reload + +use warnings; +use strict; +use POSIX qw/strftime/; +use Cwd qw/cwd realpath/; +use File::Basename qw/basename dirname/; + +our ($zone_dir, $git, $named_checkzone, $rndc, $class, $default_view, $max_depth, $zones, $verbosity); +our $user = getpwuid $<; + +my $config_file = $ARGV[0] or die "Usage: gitzone /path/to/gitzone.conf\n"; +do $config_file or die "Can't load config: $!\n"; + +my $lock_file = realpath '.gitzone-lock'; +my $list_file = realpath '.gitzone-list'; +my (%files, %inc_files, @zones, $date); +delete $ENV{GIT_DIR}; + +!-e $lock_file or die "Error: lock file exists\n"; +open FILE, '>', $lock_file or die $!; close FILE; + +sub cleanup { unlink $lock_file } +sub clean_exit { cleanup; exit shift } +$SIG{__DIE__} = \&cleanup; + +$_ = $ARGV[1]; +/^pre-receive$/ && pre_receive() || /^post-receive$/ && post_receive() || /^update-record$/ && update_record($ARGV[2]); +cleanup; + +sub git { + my ($args, $print, $ret) = @_; + $ret ||=0; + print "% git $args\n" if $verbosity >= 2; + $_ = `$git $args 2>&1`; + $print = 1 if !defined $print && $verbosity >= 1; + if ($print) { + #my $cwd = cwd; s/$cwd//g; # print relative paths + print; + } + die if $ret >= 0 && $? >> 8 != $ret; + return $_; +} + +sub process_files { + $files{$_} = 0 for (@_); + $files{$_} += process_file($_) for keys %files; + find_inc_by($_) for keys %inc_files; + check_zones(); +} + +sub process_file { + my $f = shift; # filename + my (@newfile, $changed, @inc_by); + print ">> process_file($f)\n" if $verbosity >= 3; + + return 0 if $files{$f}; # already processed + return -1 unless -f $f; # deleted + + open FILE, '<', $f or die $!; + my $n = 0; + while (<FILE>) { + $n++; + my $line = $_; + if (/^(.*)(\b\d+\b)(.*?;AUTO_INCREMENT\b.*)$/) { + # increment serial where marked with ;AUTO_INCREMENT + # if length of serial is 8 and starts with 20 treat it as a date + my ($a,$s,$z) = ($1,int $2,$3); + $date ||= strftime '%Y%m%d', localtime; + $s = ($s =~ /^$date/ || $s < 20000000 || $s >= 21000000) ? $s + 1 : $date.'00'; + $line = "$a$s$z\n"; + $changed = 1; + } elsif (/^(\W*\$INCLUDE\W+)(\S+)(.*)$/) { + # check $INCLUDE lines for files outside the user dir + my ($a,$file,$z) = ($1,$2,$3); + unless ($file =~ m,^$user/, && $file !~ /\.\./) { + close FILE; + die "Error in $f:$n: invalid included file name, it should start with: $user/\n"; + } + } else { + if ($n == 1 && /^;INCLUDED_BY\s+(.*)$/) { + # add files listed after ;INCLUDED_BY to %inc_files + @inc_by = split /\s+/, $1; + for (@inc_by) { + $inc_files{$_} = 0 unless exists $files{$_}; + } + } + } + push @newfile, $line; + } + close FILE; + + if ($changed) { + open FILE, '>', $f or die $!; + print FILE for @newfile; + close FILE; + + my $fesc = $f; + $fesc =~ s/'/'\\''/g; + git "commit -m 'auto increment: $fesc' '$fesc'", 1; + } + + return 1; +} + +sub find_inc_by { + my $f = shift; # filename + my $d = shift || 1; # recursion depth + my @inc_by; + print ">> find_inc_by($f)\n" if $verbosity >= 3; + + return 0 if $files{$f}; # already processed + return -1 unless -f $f; # deleted + $files{$_}++; + + open FILE, '<', $f or die $!; + if (<FILE> =~ /^;INCLUDED_BY\s+(.*)$/) { + # add files listed after ;INCLUDED_BY to %files + @inc_by = split /\s+/, $1; + for (@inc_by) { + $files{$_} = 0 unless exists $files{$_}; + } + } + close FILE; + + if ($d++ < $max_depth) { + find_inc_by($_, $d) for @inc_by; + } else { + print "Warning: ;INCLUDED_BY is followed only up to $max_depth levels,\n". + " the following files are not reloaded: @inc_by\n"; + } +} + +sub check_zones { + for my $f (keys %files) { + # skip files with errors and those that are not in the config + next unless $files{$f} > 0 && $zones->{$user}->{$f}; + next if $f =~ /'/; + my $zone = basename $f; + print `$named_checkzone -kn -w .. '$zone' '$user/$f'`; + clean_exit 1 if $?; # error, reject push + push @zones, $f; + } +} + +sub install_zones { + print "Reloading changed zones: @zones\n"; + + my $cwd = cwd; + # move master to new + git 'checkout -f master'; + git 'reset --hard new'; + + chdir "$zone_dir/$user" or die $!; + git "clone $cwd ." unless -d '.git'; + git 'reset --hard'; + git 'pull'; + + for my $f (@zones) { + my $zone = basename $f; + my $view = $zones->{$user}->{$f}; + $view = $default_view if $view eq 1; + `$rndc reload '$zone' $class $view`; + } + + unlink $list_file; +} + +sub pre_receive { + my ($old, $new, $ref); + chdir '..'; + + while (<STDIN>) { # <old-value> SP <new-value> SP <ref-name> LF + print if $verbosity >= 1; + next unless m,(\w+) (\w+) ([\w/]+),; + next if $3 ne 'refs/heads/master'; # only process master branch + die "Denied branch 'new', choose another name\n" if $3 eq 'refs/head/new'; + ($old, $new, $ref) = ($1, $2, $3); + } + + # nothing for master branch, exit + clean_exit 0 unless $ref; + + # check what changed + git "checkout -qf $new"; + $_ = git "diff --raw $old..$new"; + $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([\w./-]+)$,gm; + + process_files; + + if (@zones) { + print "Zone check passed: @zones\n"; + # save changed zone list for post-receive hook + open FILE, '>>', $list_file or die $!; + print FILE join(' ', @zones), "\n"; + close FILE; + } else { + print "No zones to reload\n"; + } + + # save new commits in a new branch + git 'branch -D new'; + git 'checkout -b new'; +} + +sub post_receive { + print "\n"; + chdir '..'; + + open FILE, '<', $list_file or die $!; + push @zones, split /[\s\n\r]+/ while <FILE>; + close FILE; + + install_zones; + print "Done. Don't forget to pull if you use auto increment.\n"; +} + +sub update_record { + my ($c, $f, @record) = split /\s+/, shift; + my ($ip) = $ENV{SSH_CLIENT} =~ /^([\d.]+|[a-f\d:]+)\s/i or die "Invalid IP address\n"; + my $re = qr/^\s*/i; + $re = qr/$re$_\s+/i for (@record); + my $matched = 0; + my $changed = 0; + my @newfile; + + chdir $user; + git 'checkout -f master'; + + open FILE, '<', $f or die "$f: $!"; + while (<FILE>) { + my $line = $_; + if (!$matched && s/($re)([\d.]+|[a-f\d:]+)/$1$ip/i) { + print "Matched record:\n$line"; + $matched = 1; + if ($line ne "$1$ip\n") { + $changed = 1; + $line = "$1$ip\n"; + print "Updating it with:\n$line"; + } else { + print "Not updating: already up-to-date\n"; + close FILE; + clean_exit 0; + } + } + push @newfile, $line; + } + close FILE; + die "No matching record in $f: @record\n" unless $matched; + + open FILE, '>', $f or die $!; + print FILE for @newfile; + close FILE; + + my $fesc = $f; + $fesc =~ s/'/'\\''/g; + git "commit -m 'update-record: $fesc' '$fesc'", 1; + + process_files $f; + + # save new commits in a new branch + git 'branch -D new'; + git 'checkout -b new'; + + install_zones if @zones; +} diff --git a/bin/gitzone-shell b/bin/gitzone-shell @@ -0,0 +1,49 @@ +#!/bin/sh + +# only repo allowed for git pull/push +repo='zones' +# allow ssh key add/del/list commands +allow_key_management=1 + +# paths +git_shell=/usr/bin/git-shell +gitzone=/usr/bin/gitzone +config=/etc/gitzone.conf +grep=grep + +function error { + echo "fatal: What do you think I am? A shell?" + exit 128 +} + +if [ "$1" != "-c" ]; then error; fi +cmd=$2 + +if [[ "$cmd" == git-upload-pack* ]]; then + $git_shell -c "git-upload-pack '$repo'" +elif [[ "$cmd" == git-receive-pack* ]]; then + $git_shell -c "git-receive-pack '$repo'" +elif [[ "$cmd" == update-record* ]]; then + $gitzone $config update-record "$cmd" +elif [ "$allow_key_management" == 1 ]; then + if [ "$cmd" == list-keys ]; then + cat .ssh/authorized_keys + elif [[ "$cmd" == add-key* ]]; then + key="${cmd:8}" + if [[ "$key" =~ ^ssh-(rsa|dss)\ [a-zA-Z0-9/+]+=*\ [a-zA-Z0-9_.]+@[a-zA-Z0-9.-]+$ ]]; then + echo "$key" >> .ssh/authorized_keys && \ + echo "key added" + else + echo "invalid key" + fi + elif [[ "$cmd" == del-key* ]]; then + key="${cmd:8}" + $grep -v "$key" .ssh/authorized_keys > .ssh/authorized_keys-new && \ + mv .ssh/authorized_keys-new .ssh/authorized_keys && \ + echo "key deleted" + else + error + fi +else + error +fi diff --git a/etc/gitzone.conf b/etc/gitzone.conf @@ -0,0 +1,24 @@ +# directory where the zone files are copied to (no trailing slash) +# there should be one directory for each user here chowned to the users +$zone_dir = "/var/bind"; + +# commands +$git = 'git'; +$named_checkzone = '/usr/sbin/named-checkzone'; +$rndc = '/usr/sbin/rndc'; + +# parameters for rndc reload: class & view +$class = 'IN'; +# default view of the zones +$default_view = ''; + +# max depth to follow INCLUDED_BY files +$max_depth = 256; +# output verbosity (0..3) +$verbosity = 0; + +# defines which files in a user's repo can be loaded as zone files, +# optionally you can define which view a zone belongs to +$zones = { +# user1 => { 'example.com' => 1, 'local/example.net' => 'local', }, +} diff --git a/hooks/post-receive b/hooks/post-receive @@ -0,0 +1,5 @@ +#!/bin/sh + +if [ -f .gitzone-list ]; then + gitzone /etc/gitzone.conf post-receive +fi diff --git a/hooks/pre-receive b/hooks/pre-receive @@ -0,0 +1,3 @@ +#!/bin/sh + +gitzone /etc/gitzone.conf pre-receive