Add gitosis-init, for bootstrapping a new installation.
This commit is contained in:
parent
acf005ea35
commit
97c093470e
178
gitosis/init.py
Normal file
178
gitosis/init.py
Normal 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.')
|
|
@ -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)
|
||||
|
|
3
gitosis/templates/__init__.py
Normal file
3
gitosis/templates/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Git templates for use by gitosis-init.
|
||||
"""
|
4
gitosis/templates/admin/hooks/post-update
Executable file
4
gitosis/templates/admin/hooks/post-update
Executable 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
83
gitosis/test/test_init.py
Normal 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'),
|
||||
]))
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue