Add gitosis-init, for bootstrapping a new installation.

This commit is contained in:
Tommi Virtanen 2007-09-01 18:27:25 -07:00
parent acf005ea35
commit 97c093470e
8 changed files with 336 additions and 0 deletions

178
gitosis/init.py Normal file
View file

@ -0,0 +1,178 @@
"""
Initialize a user account for use with gitosis.
"""
import errno
import logging
import optparse
import os
import re
import subprocess
import sys
from pkg_resources import resource_filename
from cStringIO import StringIO
from ConfigParser import RawConfigParser
from gitosis import repository
from gitosis import util
log = logging.getLogger('gitosis.init')
def die(msg):
log.error(msg)
sys.exit(1)
def read_ssh_pubkey(fp=None):
if fp is None:
fp = sys.stdin
line = fp.readline()
return line
_ACCEPTABLE_USER_RE = re.compile(r'^[a-z][a-z0-9]*@[a-z][a-z0-9]*$')
class InsecureSSHKeyUsername(Exception):
"""Username contains not allowed characters"""
def __str__(self):
return '%s: %s' % (self.__doc__, ': '.join(self.args))
def ssh_extract_user(pubkey):
_, user = pubkey.rsplit(None, 1)
if _ACCEPTABLE_USER_RE.match(user):
return user
else:
raise InsecureSSHKeyUsername(repr(user))
def initial_commit(git_dir, cfg, pubkey, user):
repository.fast_import(
git_dir=git_dir,
commit_msg='Automatic creation of gitosis repository.',
committer='Gitosis Admin <%s>' % user,
files=[
('keydir/%s.pub' % user, pubkey),
('gitosis.conf', cfg),
],
)
def run_post_update(git_dir):
args = [os.path.join(git_dir, 'hooks', 'post-update')]
returncode = subprocess.call(
args=args,
cwd=git_dir,
close_fds=True,
env=dict(GIT_DIR='.'),
)
if returncode != 0:
die(
("post-update returned non-zero exit status %d"
% returncode),
)
def symlink_config(git_dir):
dst = os.path.expanduser('~/.gitosis.conf')
tmp = '%s.%d.tmp' % (dst, os.getpid())
try:
os.unlink(tmp)
except OSError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
os.symlink(
os.path.join(git_dir, 'gitosis.conf'),
tmp,
)
os.rename(tmp, dst)
def getParser():
parser = optparse.OptionParser(
usage='%prog',
description='Initialize a user account for use with gitosis',
)
parser.set_defaults(
config=os.path.expanduser('~/.gitosis.conf'),
)
parser.add_option('--config',
metavar='FILE',
help='read config from FILE',
)
return parser
def init_admin_repository(
git_dir,
pubkey,
user,
):
repository.init(
path=git_dir,
template=resource_filename('gitosis.templates', 'admin')
)
repository.init(
path=git_dir,
)
if not repository.has_initial_commit(git_dir):
log.info('Making initial commit...')
# ConfigParser does not guarantee order, so jump through hoops
# to make sure [gitosis] is first
cfg_file = StringIO()
print >>cfg_file, '[gitosis]'
print >>cfg_file
cfg = RawConfigParser()
cfg.add_section('group gitosis-admin')
cfg.set('group gitosis-admin', 'members', user)
cfg.set('group gitosis-admin', 'writable', 'gitosis-admin')
cfg.write(cfg_file)
initial_commit(
git_dir=git_dir,
cfg=cfg_file.getvalue(),
pubkey=pubkey,
user=user,
)
def main():
logging.basicConfig(level=logging.INFO)
os.umask(0022)
parser = getParser()
(options, args) = parser.parse_args()
if args:
parser.error('Did not expect arguments.')
cfg = RawConfigParser()
try:
conffile = file(options.config)
except (IOError, OSError), e:
if e.errno == errno.ENOENT:
# not existing is ok
pass
else:
# I trust the exception has the path.
die("Unable to read config file: %s." % e)
else:
try:
cfg.readfp(conffile)
finally:
conffile.close()
log.info('Reading SSH public key...')
pubkey = read_ssh_pubkey()
user = ssh_extract_user(pubkey)
if user is None:
die('Cannot parse user from SSH public key.')
log.info('Admin user is %r', user)
log.info('Creating repository structure...')
repositories = util.getRepositoryDir(cfg)
util.mkdir(repositories)
admin_repository = os.path.join(repositories, 'gitosis-admin.git')
init_admin_repository(
git_dir=admin_repository,
pubkey=pubkey,
user=user,
)
log.info('Running post-update hook...')
run_post_update(git_dir=admin_repository)
log.info('Symlinking ~/.gitosis.conf to repository...')
symlink_config(git_dir=admin_repository)
log.info('Done.')

View file

@ -1,4 +1,5 @@
import os
import re
import subprocess
from gitosis import util
@ -119,3 +120,28 @@ def export(git_dir, path):
)
if returncode != 0:
raise GitCheckoutIndexError('exit status %d' % returncode)
class GitHasInitialCommitError(GitError):
"""Check for initial commit failed"""
class GitRevParseError(GitError):
"""rev-parse failed"""
def has_initial_commit(git_dir):
child = subprocess.Popen(
args=['git', 'rev-parse', 'HEAD'],
cwd=git_dir,
stdout=subprocess.PIPE,
close_fds=True,
env=dict(GIT_DIR='.'),
)
got = child.stdout.read()
returncode = child.wait()
if returncode != 0:
raise GitRevParseError('exit status %d' % returncode)
if got == 'HEAD\n':
return False
elif re.match('^[0-9a-f]{40}\n$', got):
return True
else:
raise GitHasInitialCommitError('Unknown git HEAD: %r' % got)

View file

@ -0,0 +1,3 @@
"""
Git templates for use by gitosis-init.
"""

View file

@ -0,0 +1,4 @@
#!/bin/sh
set -e
gitosis-run-hook post-update
git-update-server-info

83
gitosis/test/test_init.py Normal file
View file

@ -0,0 +1,83 @@
from nose.tools import eq_ as eq
from gitosis.test.util import assert_raises, maketemp
import os
from ConfigParser import RawConfigParser
from gitosis import init
from gitosis import repository
from gitosis.test import util
def test_ssh_extract_user_simple():
got = init.ssh_extract_user(
'ssh-somealgo '
+'0123456789ABCDEFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= fakeuser@fakehost')
eq(got, 'fakeuser@fakehost')
def test_ssh_extract_user_bad():
e = assert_raises(
init.InsecureSSHKeyUsername,
init.ssh_extract_user,
'ssh-somealgo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= ER3%#@e%')
eq(str(e), "Username contains not allowed characters: 'ER3%#@e%'")
def test_init_admin_repository():
tmp = maketemp()
admin_repository = os.path.join(tmp, 'admin.git')
pubkey = (
'ssh-somealgo '
+'0123456789ABCDEFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= fakeuser@fakehost')
user = 'jdoe'
init.init_admin_repository(
git_dir=admin_repository,
pubkey=pubkey,
user=user,
)
eq(os.listdir(tmp), ['admin.git'])
hook = os.path.join(
tmp,
'admin.git',
'hooks',
'post-update',
)
util.check_mode(hook, 0755, is_file=True)
got = util.readFile(hook).splitlines()
assert 'gitosis-run-hook post-update' in got
export_dir = os.path.join(tmp, 'export')
repository.export(git_dir=admin_repository,
path=export_dir)
eq(sorted(os.listdir(export_dir)),
sorted(['gitosis.conf', 'keydir']))
eq(os.listdir(os.path.join(export_dir, 'keydir')),
['jdoe.pub'])
got = util.readFile(
os.path.join(export_dir, 'keydir', 'jdoe.pub'))
eq(got, pubkey)
# the only thing guaranteed of initial config file ordering is
# that [gitosis] is first
got = util.readFile(os.path.join(export_dir, 'gitosis.conf'))
got = got.splitlines()[0]
eq(got, '[gitosis]')
cfg = RawConfigParser()
cfg.read(os.path.join(export_dir, 'gitosis.conf'))
eq(sorted(cfg.sections()),
sorted([
'gitosis',
'group gitosis-admin',
]))
eq(cfg.items('gitosis'), [])
eq(sorted(cfg.items('group gitosis-admin')),
sorted([
('writable', 'gitosis-admin'),
('members', 'jdoe'),
]))

View file

@ -6,6 +6,7 @@ import subprocess
from gitosis import repository
from gitosis.test.util import mkdir, maketemp, readFile, check_mode
from gitosis.test.util import assert_raises
def check_bare(path):
# we want it to be a bare repository
@ -102,3 +103,30 @@ Frobitz the quux and eschew obfuscation.
eq(got[5], '')
eq(got[6], 'Frobitz the quux and eschew obfuscation.')
eq(got[7:], [])
def test_has_initial_commit_fail_notAGitDir():
tmp = maketemp()
e = assert_raises(
repository.GitRevParseError,
repository.has_initial_commit,
git_dir=tmp)
eq(str(e), 'rev-parse failed: exit status 128')
def test_has_initial_commit_no():
tmp = maketemp()
repository.init(path=tmp)
got = repository.has_initial_commit(git_dir=tmp)
eq(got, False)
def test_has_initial_commit_yes():
tmp = maketemp()
repository.init(path=tmp)
repository.fast_import(
git_dir=tmp,
commit_msg='fakecommit',
committer='John Doe <jdoe@example.com>',
files=[],
)
got = repository.has_initial_commit(git_dir=tmp)
eq(got, True)

View file

@ -52,6 +52,19 @@ def readFile(path):
f.close()
return data
def assert_raises(excClass, callableObj, *args, **kwargs):
"""
Like unittest.TestCase.assertRaises, but returns the exception.
"""
try:
callableObj(*args, **kwargs)
except excClass, e:
return e
else:
if hasattr(excClass,'__name__'): excName = excClass.__name__
else: excName = str(excClass)
raise AssertionError("%s not raised" % excName)
def check_mode(path, mode, is_file=None, is_dir=None):
st = os.stat(path)
if is_dir:

View file

@ -18,6 +18,7 @@ setup(
'gitosis-serve = gitosis.serve:main',
'gitosis-gitweb = gitosis.gitweb:main',
'gitosis-run-hook = gitosis.run_hook:main',
'gitosis-init = gitosis.init:main',
],
},
)