URI: 
       timproved zone configuration: directories - gitzone - git-based zone management tool for static and dynamic domains
  HTML git clone https://git.parazyd.org/gitzone
   DIR Log
   DIR Files
   DIR Refs
       ---
   DIR commit ddf67d329f5f9cd8adcd6b156cac5c093f652292
   DIR parent 568aef2c60e8b76545c8d5600f6a08fe25d88189
  HTML Author: tg(x) <*@tg-x.net>
       Date:   Sun, 13 Feb 2011 15:26:51 +0100
       
       improved zone configuration: directories
       
       Diffstat:
         M bin/gitzone                         |     118 +++++++++++++++++--------------
         M etc/gitzone.conf                    |      28 +++++++++++++++++-----------
       
       2 files changed, 81 insertions(+), 65 deletions(-)
       ---
   DIR diff --git a/bin/gitzone b/bin/gitzone
       t@@ -12,7 +12,7 @@ use warnings;
        use strict;
        use POSIX qw/strftime/;
        use Cwd qw/cwd realpath/;
       -use File::Basename qw/basename dirname/;
       +use File::Basename qw/fileparse/;
        
        our ($zone_dir, $git, $named_checkzone, $rndc, $class, $default_view, $update_record, $max_depth, $zones, $verbosity);
        our $user = getpwuid $<;
       t@@ -34,7 +34,7 @@ sub cleanup { unlink $lock_file }
        sub clean_exit { cleanup; exit shift }
        $SIG{__DIE__} = \&cleanup;
        
       -$_ = $cmd &&
       +($_ = $cmd) &&
            /^pre-receive$/ && pre_receive() ||
            /^post-receive$/ && post_receive() ||
            $update_record && /^update-record$/ && update_record($ARGV[2]);
       t@@ -61,21 +61,34 @@ sub git {
        # Load BIND config files specified in the $zones config variable.
        # First load the -default key, then the $user key.
        sub load_zones_config {
       -  my $u = shift || '-default';
       -
       -  for my $f (keys %{$zones->{$u}}) {
       -    next unless $f =~ m,^/, && -f $f;
       -    open FILE, '<', $f or die $!;
       -    while (<FILE>) {
       -      if (/^\s*zone\s+"([^"]+)"/) {
       -        $zones->{$user}->{$1} = $zones->{$u}->{$f};
       +  my $key = shift || '-default';
       +
       +  # move files not in a dir to a . dir for easier processing
       +  for my $file (keys %{$zones->{$key}}) {
       +    next if ref $zones->{$key}->{$file} eq 'HASH';
       +    $zones->{$key}->{'.'}->{$file} = $zones->{$key}->{$file};
       +    delete $zones->{$key}->{$file};
       +  }
       +
       +  for my $dir (keys %{$zones->{$key}}) {
       +    my $d = $zones->{$key}->{$dir};
       +    for my $file (keys %$d) {
       +      $d->{$file} = $default_view if $d->{$file} eq 1;
       +      $d->{$file} = [$d->{$file}] if ref $d->{$file} ne 'ARRAY';
       +      next unless $file =~ m,^/, && -f $file;
       +
       +      open FILE, '<', $file or die $!;
       +      while (<FILE>) {
       +        if (/^\s*zone\s+"([^"]+)"/) {
       +          $zones->{$user}->{$dir}->{$1} = $d->{$file};
       +        }
              }
       +      close FILE;
       +      delete $d->{$file} if $key ne '-default';
            }
       -    close FILE;
       -    delete $zones->{$u}->{$f} if $u ne '-default';
          }
        
       -  load_zones_config($user) if $u eq '-default';
       +  load_zones_config($user) if $key eq '-default';
        }
        
        sub process_files {
       t@@ -86,14 +99,14 @@ sub process_files {
        }
        
        sub process_file {
       -  my $f = shift; # filename
       +  my $file = shift;
          my (@newfile, $changed, @inc_by);
       -  print ">> process_file($f)\n" if $verbosity >= 3;
       +  print ">> process_file($file)\n" if $verbosity >= 3;
        
       -  return 0 if $files{$f}; # already processed
       -  return -1 unless -f $f; # deleted
       +  return 0 if $files{$file}; # already processed
       +  return -1 unless -f $file; # deleted
        
       -  open FILE, '<', $f or die $!;
       +  open FILE, '<', $file or die $!;
          my $n = 0;
          while (<FILE>) {
            $n++;
       t@@ -108,10 +121,10 @@ sub process_file {
              $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 !~ /\.\./) {
       +      my ($a,$inc_file,$z) = ($1,$2,$3);
       +      unless ($inc_file =~ m,^$user/, && $inc_file !~ /\.\./) {
                close FILE;
       -        die "Error in $f:$n: invalid included file name, it should start with: $user/\n";
       +        die "Error in $file:$n: invalid included file name, it should start with: $user/\n";
              }
            } else {
              if ($n == 1 && /^;INCLUDED_BY\s+(.*)$/) {
       t@@ -127,29 +140,27 @@ sub process_file {
          close FILE;
        
          if ($changed) {
       -    open FILE, '>', $f or die $!;
       +    open FILE, '>', $file or die $!;
            print FILE for @newfile;
            close FILE;
        
       -    my $fesc = $f;
       -    $fesc =~ s/'/'\\''/g;
       -    git "commit -m 'auto increment: $fesc' '$fesc'", 1;
       +    git "commit -m 'auto increment: $file' '$file'", 1;
          }
        
          return 1;
        }
        
        sub find_inc_by {
       -  my $f = shift; # filename
       -  my $d = shift || 1; # recursion depth
       +  my $file = shift;
       +  my $depth = shift || 1; # recursion depth
          my @inc_by;
       -  print ">> find_inc_by($f)\n" if $verbosity >= 3;
       +  print ">> find_inc_by($file)\n" if $verbosity >= 3;
        
       -  return 0 if $files{$f}; # already processed
       -  return -1 unless -f $f; # deleted
       +  return 0 if $files{$file}; # already processed
       +  return -1 unless -f $file; # deleted
          $files{$_}++;
        
       -  open FILE, '<', $f or die $!;
       +  open FILE, '<', $file or die $!;
          if (<FILE> =~ /^;INCLUDED_BY\s+(.*)$/) {
            # add files listed after ;INCLUDED_BY to %files
            @inc_by = split /\s+/, $1;
       t@@ -159,8 +170,8 @@ sub find_inc_by {
          }
          close FILE;
        
       -  if ($d++ < $max_depth) {
       -    find_inc_by($_, $d) for @inc_by;
       +  if ($depth++ < $max_depth) {
       +    find_inc_by($_, $depth) 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";
       t@@ -168,14 +179,15 @@ sub find_inc_by {
        }
        
        sub check_zones {
       -  for my $f (keys %files) {
       +  for my $file (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'`;
       +    my ($zone, $dir) = fileparse $file;
       +    $dir = substr $dir, 0, -1;
       +    next unless $files{$file} > 0 && exists $zones->{$user}->{$dir}->{$zone};
       +
       +    print `$named_checkzone -kn -w .. '$zone' '$user/$file'`;
            clean_exit 1 if $?; # error, reject push
       -    push @zones, $f;
       +    push @zones, $file;
          }
        }
        
       t@@ -192,12 +204,11 @@ sub install_zones {
          git 'fetch';
          git 'reset --hard remotes/origin/master';
        
       -  for my $f (@zones) {
       -    my $zone = basename $f;
       -    my $view = $zones->{$user}->{$f};
       -    $view = $default_view if $view eq 1;
       -    $view = [$view] if ref $view ne 'ARRAY';
       -    `$rndc reload '$zone' $class $_` for @$view;
       +  for my $file (@zones) {
       +    my ($zone, $dir) = fileparse $file;
       +    $dir = substr $dir, 0, -1;
       +    my $view = $zones->{$user}->{$dir}->{$zone};
       +    print "$zone: ", `$rndc reload '$zone' $class $_` for @$view;
          }
        
          unlink $list_file;
       t@@ -221,7 +232,8 @@ sub pre_receive {
          # check what changed
          git "checkout -qf $new";
          $_ = git "diff --raw $old..$new";
       -  $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([\w./-]+)$,gm;
       +  # parse diff output, add only valid zone names to %files for parsing
       +  $files{$1} = 0 while m,^:(?:[\w.]+\s+){5}([a-z0-9./-]+)$,gm;
        
          load_zones_config;
          process_files;
       t@@ -255,7 +267,7 @@ sub post_receive {
        }
        
        sub update_record {
       -  my ($c, $f, @record) = split /\s+/, shift;
       +  my ($c, $file, @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);
       t@@ -266,7 +278,7 @@ sub update_record {
          chdir $user;
          git 'checkout -f master';
        
       -  open FILE, '<', $f or die "$f: $!";
       +  open FILE, '<', $file or die "$file: $!";
          while (<FILE>) {
            my $line = $_;
            if (!$matched && s/($re)([\d.]+|[a-f\d:]+)/$1$ip/i) {
       t@@ -285,17 +297,15 @@ sub update_record {
            push @newfile, $line;
          }
          close FILE;
       -  die "No matching record in $f: @record\n" unless $matched;
       +  die "No matching record in $file: @record\n" unless $matched;
        
       -  open FILE, '>', $f or die $!;
       +  open FILE, '>', $file or die $!;
          print FILE for @newfile;
          close FILE;
        
       -  my $fesc = $f;
       -  $fesc =~ s/'/'\\''/g;
       -  git "commit -m 'update-record: $fesc' '$fesc'", 1;
       +  git "commit -m 'update-record: $file' '$file'", 1;
        
       -  process_files $f;
       +  process_files $file;
        
          # save new commits in a new branch
          git 'branch -D new';
   DIR diff --git a/etc/gitzone.conf b/etc/gitzone.conf
       t@@ -6,8 +6,8 @@
        #   $user - name of the user gitzone is invoked by
        
        # 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";
       +# there should be one directory for each user here chowned to them
       +$zone_dir = '/var/bind';
        
        # commands
        $git = '/usr/bin/git';
       t@@ -31,9 +31,10 @@ $default_view = '';
        # $zones defines which files in a user's repo can be loaded as zone files.
        #
        # You can define which view a zone belongs to, this can be
       -#  - a string
       -#  - an array with multiple views is allowed
       +#  - a string for a single view
       +#  - an array for multiple views
        #  - or 1 to use the $default_view
       +# The view is used as a parameter for rndc reload.
        #
        # The basename of the files listed must be identical to the zone name.
        # If a file name starts with a / it's treated as a BIND config file
       t@@ -43,13 +44,18 @@ $default_view = '';
        
        $zones = {
        #  -default => {
       -#    "/etc/bind/users/$user.conf" => 1,             # allow every zone from this file, use the default view
       +#    "/etc/bind/users/$user.conf" => 1,               # allow every zone from this file, use the default view for them
        #  },
       -#  user1 => {
       -#    '/etc/bind/users/user1-local.conf' => 'local', # allow every zone from this file, use the local view
       -#    'example.com' => 1,                            # allow example.com, use the default view
       -#    'local/example.net' => 'local',                # allow example.net, use the local view
       -#    'extern/example.net' => 'extern',              # allow example.net, use the extern view
       -#    'common/example.net' => [qw(extern local)],    # allow example.net, use both the local & extern view
       +#  user1 => { # /etc/bind/users/user1.conf is loaded first and merged with the config below, as specified in -default above
       +#    'example.com' => 1,                              # allow example.com, use the default view for it
       +#    'example.net' => 'extern',                       # allow example.net, use the extern view for it
       +#    'example.org' => [qw(view1 view2)],              # allow example.org, use both view1 & view2 for it
       +#    local => {                                       # local/ dir in the repo
       +#      '/etc/bind/users/user1-local.conf' => 'local', # allow every zone from this file, use the local view for them
       +#      'example.net' => 'local',                      # allow example.net, use the local view for it
       +#    },
       +#    'foo/bar/baz' => {                               # foo/bar/baz/ dir in the repo
       +#      'example.org' => 1,                            # allow example.org, use the default view for it
       +#    },
        #  },
        }