summaryrefslogtreecommitdiffstats
path: root/roles/git/checks/files
diff options
context:
space:
mode:
authorMathieu Bridon <bochecha@fedoraproject.org>2014-09-23 14:14:24 +0200
committerPierre-Yves Chibon <pingou@pingoured.fr>2014-10-27 10:48:12 +0100
commit16ffb744be3e4b99cc1f3170b7ea76abdfbe7a53 (patch)
tree3f191b6eb9e910aa560a991db9bd7b53bc6269ad /roles/git/checks/files
parent7ab3ff28179a07037fef4207b7b5446c45a8479c (diff)
downloadansible-16ffb744be3e4b99cc1f3170b7ea76abdfbe7a53.tar.gz
ansible-16ffb744be3e4b99cc1f3170b7ea76abdfbe7a53.tar.xz
ansible-16ffb744be3e4b99cc1f3170b7ea76abdfbe7a53.zip
Rearrange some tasks
We have a gitolite/check_fedmsg_hooks role, which installs a script and schedules it. Turns out, this script does more than just checking the fedmsg hooks, depending on the command-line arguments used when running it. As such, it makes sense to separate it out into its own role, and make the gitolite/check_fedmsg_hooks role (and any other one using the script) depend on it. For example, this script is used for Fedora Hosted (still in Puppet), and will soon be used for a new distgit hook check.
Diffstat (limited to 'roles/git/checks/files')
-rwxr-xr-xroles/git/checks/files/check-perms.py419
1 files changed, 419 insertions, 0 deletions
diff --git a/roles/git/checks/files/check-perms.py b/roles/git/checks/files/check-perms.py
new file mode 100755
index 000000000..31d421567
--- /dev/null
+++ b/roles/git/checks/files/check-perms.py
@@ -0,0 +1,419 @@
+#!/usr/bin/python -tt
+"""Check permissions of a tree of git repositories, optionally fixing any
+problems found.
+"""
+
+import os
+import re
+import sys
+import optparse
+from stat import *
+from subprocess import call, PIPE, Popen
+
+ALL_CHECKS = ['bare', 'shared', 'mail-hook', 'fedmsg-hook', 'perms', 'post-update-hook']
+DEFAULT_CHECKS = ['bare', 'shared', 'perms', 'post-update-hook']
+
+OBJECT_RE = re.compile('[0-9a-z]{40}')
+
+def error(msg):
+ print >> sys.stderr, msg
+
+def is_object(path):
+ """Check if a path is a git object."""
+ parts = path.split(os.path.sep)
+ if 'objects' in parts and len(parts) > 2 and \
+ OBJECT_RE.match(''.join(path.split(os.path.sep)[-2:])):
+ return True
+ return False
+
+def is_bare_repo(gitdir):
+ """Check if a git repository is bare."""
+ cmd = ['git', '--git-dir', gitdir, 'config', '--bool', 'core.bare']
+ p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ bare, error = p.communicate()
+ if bare.rstrip() != 'true' or p.returncode:
+ return False
+ return True
+
+def is_shared_repo(gitdir):
+ """Check if a git repository is shared."""
+ cmd = ['git', '--git-dir', gitdir, 'config', 'core.sharedRepository']
+ p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ shared, error = p.communicate()
+ sharedmodes = ['1', 'group', 'true', '2', 'all', 'world', 'everybody']
+ if shared.rstrip() not in sharedmodes or p.returncode:
+ return False
+ return True
+
+def uses_version1_mail_hook(gitdir):
+ """Check if a git repository uses the old fedora-git-commit-mail-hook."""
+ hook = os.path.join(gitdir, 'hooks/update')
+ oldpath = '/usr/bin/fedora-git-commit-mail-hook'
+ return os.path.realpath(hook) == oldpath
+
+def uses_version2_mail_hook(gitdir):
+ """Check if a git repository uses the pre-fedmsg mail-hook setup."""
+ hook = os.path.join(gitdir, 'hooks/post-receive')
+ oldpath = '/usr/share/git-core/mail-hooks/gnome-post-receive-email'
+ return os.path.realpath(hook) == oldpath
+
+def check_post_update_hook(gitdir, fix=False):
+ """Check if a repo's post-update hook is setup correctly."""
+ hook = os.path.join(gitdir, 'hooks/post-update')
+ realpath = os.path.realpath(hook)
+ goodpath = '/usr/share/git-core/templates/hooks/post-update.sample'
+ badpath = '/usr/bin/git-update-server-info'
+
+ if realpath == goodpath:
+ return True
+
+ errmsg = ''
+ if realpath == badpath:
+ errmsg = '%s: symlinked to %s' % (hook, badpath)
+ elif not os.path.exists(hook):
+ errmsg = '%s: does not exist' % hook
+ elif not os.access(hook, os.X_OK):
+ errmsg = '%s: is not executable' % hook
+ elif not os.path.islink(hook):
+ errmsg = '%s: not a symlink' % hook
+ else:
+ errmsg = '%s: symlinked to %s' % (hook, realpath)
+
+ error(errmsg)
+
+ if not fix:
+ return False
+
+ if not os.path.exists(goodpath):
+ error('%s: post-update hook (%s) does not exist.' % (gitdir, goodpath))
+ return False
+
+ if os.path.exists(hook):
+ try:
+ os.rename(hook, '%s~' % hook)
+ except (IOError, OSError), err:
+ error('%s: Error renaming %s: %s' % (gitdir, hook, err.strerror))
+ return False
+ try:
+ os.symlink(goodpath, hook)
+ except (IOError, OSError), err:
+ error('%s: Error creating %s symlink: %s' % (gitdir, hook, err.strerror))
+ return False
+
+ return True
+
+def set_bare_repo(gitdir):
+ """Set core.bare for a git repository."""
+ cmd = ['git', '--git-dir', gitdir, 'config', '--bool', 'core.bare', 'true']
+ ret = call(cmd)
+ if ret:
+ return False
+ return True
+
+def set_shared_repo(gitdir, value='group'):
+ """Set core.sharedRepository for a git repository."""
+ mode_re = re.compile('06[0-7]{2}')
+ if value in [0, 'false', 'umask']:
+ value = 'umask'
+ elif value in [1, 'true', 'group']:
+ value = 'group'
+ elif value in [2, 'all', 'world', 'everybody']:
+ value = 'all'
+ elif mode_re.match(value):
+ pass
+ else:
+ raise SystemExit('Bogus core.sharedRepository value "%s"' % value)
+ cmd = ['git', '--git-dir', gitdir, 'config', 'core.sharedRepository',
+ value]
+ ret = call(cmd)
+ if ret:
+ return False
+ return True
+
+
+def set_post_receive_hook_version2(gitdir):
+ """Configure a git repository to use the gnome mail hook without fedmsg."""
+
+ # Get recipients from the commit-list file.
+ commit_list = os.path.join(gitdir, 'commit-list')
+ if not os.path.exists(commit_list):
+ error('%s: No commit-list file found' % gitdir)
+ return False
+ try:
+ addrs = open(commit_list).read().strip()
+ addrs = ', '.join(addrs.split())
+ except:
+ error('%s: Unable to read commit-list file' % gitdir)
+ return False
+
+ # Set hooks.mailinglist
+ if '@' not in addrs:
+ addrs = '%s@lists.fedorahosted.org'
+ cmd = ['git', '--git-dir', gitdir, 'config', 'hooks.mailinglist', addrs]
+ p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ error('%s: Error setting hooks.mailinglist: %s' % (gitdir, stderr))
+ return False
+
+ # Set hooks.maildomain
+ cmd = ['git', '--git-dir', gitdir, 'config', 'hooks.maildomain',
+ 'fedoraproject.org']
+ p = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ error('%s: Error setting hooks.maildomain: %s' % (gitdir, stderr))
+ return False
+
+ # Symlink mail notification script to post-receive hook
+ script = '/usr/share/git-core/mail-hooks/gnome-post-receive-email'
+ if not os.path.exists(script):
+ error('%s: Mail hook (%s) does not exist.' % (gitdir, script))
+ return False
+
+ hook = os.path.join(gitdir, 'hooks', 'post-receive')
+ if os.path.exists(hook):
+ try:
+ os.remove(hook)
+ except Exception, e:
+ errstr = hasattr(e, 'strerror') and e.strerror or e
+ error('%s: Error removing %s: %s' % (gitdir, hook, errstr))
+ return False
+ try:
+ os.symlink(script, hook)
+ except Exception, e:
+ errstr = hasattr(e, 'strerror') and e.strerror or e
+ error('%s: Error creating %s symlink: %s' % (gitdir, hook, errstr))
+ return False
+
+ # Clean up commit-list file and old update hook link
+ try:
+ os.rename(commit_list, '%s~' % commit_list)
+ except (IOError, OSError), err:
+ error('%s: Unable to backup commit-list: %s' % (gitdir, err.strerror))
+ return False
+ try:
+ oldhook = os.path.join(gitdir, 'hooks/update')
+ os.remove(oldhook)
+ except (IOError, OSError), err:
+ error('%s: Unable to backup commit-list: %s' % (gitdir, err.strerror))
+ return False
+
+ # We ran the gauntlet.
+ return True
+
+
+def set_post_receive_hook_version3(gitdir):
+ """Configure a git repository to use the fedmsg+gnome-mail hooks."""
+
+ if not uses_version2_mail_hook(gitdir):
+ error('%s: Not yet on version2 mail hook; do --fix=mail-hook' % gitdir)
+ return False
+
+ # Check that the destination is 'okay'
+ dest_prefix = os.path.join(gitdir, 'hooks', 'post-receive-chained.d')
+
+ if not os.path.exists(dest_prefix):
+ os.mkdir(dest_prefix)
+
+ if not os.path.isdir(dest_prefix):
+ error('%s: %s is not a directory.' % (gitdir, dest_prefix))
+ return False
+
+ # Symlink mail notification and fedmsg scripts to post-receive hook
+ scripts = {
+ '/usr/share/git-core/mail-hooks/gnome-post-receive-email':
+ os.path.join(dest_prefix, 'post-receive-email'),
+ '/usr/share/git-core/post-receive-fedmsg':
+ os.path.join(dest_prefix, 'post-receive-fedmsg'),
+
+ # This one kicks off all the others.
+ '/usr/share/git-core/post-receive-chained':
+ os.path.join(gitdir, 'hooks', 'post-receive'),
+
+ }
+
+ for script, hook in scripts.items():
+ if not os.path.exists(script):
+ error('%s: Hook (%s) does not exist.' % (gitdir, script))
+ return False
+
+ if os.path.exists(hook):
+ try:
+ os.remove(hook)
+ except Exception, e:
+ errstr = hasattr(e, 'strerror') and e.strerror or e
+ error('%s: Error removing %s: %s' % (gitdir, hook, errstr))
+ return False
+ try:
+ os.symlink(script, hook)
+ except Exception, e:
+ errstr = hasattr(e, 'strerror') and e.strerror or e
+ error('%s: Error creating %s symlink: %s' % (gitdir, hook, errstr))
+ return False
+
+ # We ran the gauntlet.
+ return True
+
+
+def list_checks():
+ print 'Available checks: %s' % ', '.join(ALL_CHECKS)
+ print 'Default checks: %s' % ', '.join(DEFAULT_CHECKS)
+
+
+def check_git_perms(path, fix=False):
+ """Check if permissions on a git repo are correct.
+
+ If fix is true, problems found are corrected.
+ """
+ object_mode = S_IRUSR | S_IRGRP | S_IROTH
+ oldmode = mode = S_IMODE(os.lstat(path)[ST_MODE])
+ errors = []
+ if os.path.isdir(path):
+ newmode = mode | S_ISGID
+ if mode != newmode:
+ msg = 'Not SETGID (should be "%s")' % oct(newmode)
+ errors.append(msg)
+ mode = newmode
+ elif is_object(path) and mode ^ object_mode:
+ msg = 'Wrong object mode "%s" (should be "%s")' % (
+ oct(mode), oct(object_mode))
+ errors.append(msg)
+ mode = object_mode
+ if mode & S_IWUSR and not is_object(path):
+ newmode = mode | S_IWGRP
+ exempt = \
+ any(map(path.endswith, ['commit-list', 'gl-conf'])) or \
+ any(map(path.__contains__, ['/hooks/']))
+
+ if mode != newmode and not exempt:
+ msg = 'Not group writable (should be "%s")' % oct(newmode)
+ errors.append(msg)
+ mode = newmode
+ if mode != oldmode and not os.path.islink(path):
+ errmsg = '%s:' % path
+ errmsg += ', '.join(['%s' % e for e in errors])
+ error(errmsg)
+ if not fix:
+ return False
+ try:
+ os.chmod(path, mode)
+ return True
+ except Exception, e:
+ errstr = hasattr(e, 'strerror') and e.strerror or e
+ mode = oct(mode)
+ error('%s: Error setting "%s" mode on %s: %s' % (gitdir,
+ mode, path, errstr))
+ return False
+ return True
+
+def main():
+ usage = '%prog [options] [gitroot]'
+ parser = optparse.OptionParser(usage=usage)
+ parser.add_option('-f', '--fix', action='store_true', default=False,
+ help='Correct any problems [%default]')
+ parser.add_option('-l', '--list-checks', action='store_true',
+ help='List default checks')
+ parser.add_option('-c', '--check', dest='checks', action='append',
+ default=[], metavar='check',
+ help='Add a check, may be used multiple times')
+ parser.add_option('-s', '--skip', action='append', default=[],
+ metavar='check',
+ help='Skip a check, may be used multiple times')
+ parser.add_option('-r', '--repo', default=None,
+ help="Check only a certain repo, not all of them.")
+ opts, args = parser.parse_args()
+
+ # Check options
+ if opts.list_checks:
+ list_checks()
+ raise SystemExit
+
+ if opts.checks:
+ checks = set(opts.checks)
+ bad_check_opts = checks.difference(set(ALL_CHECKS))
+ if bad_check_opts:
+ msg = 'Bad check(s): %s' % ', '.join(sorted(bad_check_opts))
+ msg += '\nAvailable checks: %s' % ', '.join(ALL_CHECKS)
+ raise SystemExit(msg)
+ else:
+ bad_skip_opts = set(opts.skip).difference(set(ALL_CHECKS))
+ if bad_skip_opts:
+ msg = 'Bad skip option(s): %s' % ', '.join(sorted(bad_skip_opts))
+ msg += '\nAvailable checks: %s' % ', '.join(ALL_CHECKS)
+ raise SystemExit(msg)
+ checks = set()
+ for check in DEFAULT_CHECKS:
+ if check not in opts.skip:
+ checks.add(check)
+
+ # Check args
+ if len(args) > 1:
+ raise SystemExit(parser.get_usage().strip())
+
+ gitroot = args and args[0] or '/git'
+
+ if not os.path.isdir(gitroot):
+ raise SystemExit('%s does not exist or is not a directory' % gitroot)
+
+ if opts.repo:
+ gitdirs = ['/'.join([gitroot, opts.repo])]
+ else:
+ gitdirs = []
+ for path, dirs, files in os.walk(gitroot):
+ if path in gitdirs:
+ continue
+ if 'description' in os.listdir(path):
+ gitdirs.append(path)
+
+ problems = []
+ for gitdir in sorted(gitdirs):
+ if 'bare' in checks and not is_bare_repo(gitdir):
+ error('%s: core.bare not true' % gitdir)
+ if not opts.fix or not set_bare_repo(gitdir):
+ problems.append(gitdir)
+ if 'shared' in checks and not is_shared_repo(gitdir):
+ error('%s: core.sharedRepository not set' % gitdir)
+ if not opts.fix or not set_shared_repo(gitdir):
+ problems.append(gitdir)
+
+ if 'mail-hook' in checks and uses_version1_mail_hook(gitdir):
+ error('%s: uses old mail hook' % gitdir)
+ if not opts.fix or not set_post_receive_hook_version2(gitdir):
+ problems.append(gitdir)
+
+ if 'fedmsg-hook' in checks and (uses_version1_mail_hook(gitdir) or
+ uses_version2_mail_hook(gitdir)):
+ error('%s: uses the gnome mail hook or older' % gitdir)
+ if not opts.fix or not set_post_receive_hook_version3(gitdir):
+ problems.append(gitdir)
+
+ if 'post-update-hook' in checks and not check_post_update_hook(gitdir,
+ opts.fix):
+ problems.append(gitdir)
+
+ if 'perms' in checks:
+ paths = []
+ for path, dirs, files in os.walk(gitdir):
+ for d in dirs:
+ d = os.path.join(path, d)
+ if d not in paths:
+ paths.append(d)
+ for f in files:
+ f = os.path.join(path, f)
+ if f not in paths:
+ paths.append(f)
+ for path in paths:
+ if not check_git_perms(path, fix=opts.fix):
+ problems.append(path)
+
+ if problems:
+ raise SystemExit('%d problems remain unfixed' % len(problems))
+
+ raise SystemExit()
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ raise SystemExit('\nExiting on user cancel (Ctrl-C)')