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 os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from gitosis import util
|
from gitosis import util
|
||||||
|
@ -119,3 +120,28 @@ def export(git_dir, path):
|
||||||
)
|
)
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
raise GitCheckoutIndexError('exit status %d' % returncode)
|
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 import repository
|
||||||
|
|
||||||
from gitosis.test.util import mkdir, maketemp, readFile, check_mode
|
from gitosis.test.util import mkdir, maketemp, readFile, check_mode
|
||||||
|
from gitosis.test.util import assert_raises
|
||||||
|
|
||||||
def check_bare(path):
|
def check_bare(path):
|
||||||
# we want it to be a bare repository
|
# we want it to be a bare repository
|
||||||
|
@ -102,3 +103,30 @@ Frobitz the quux and eschew obfuscation.
|
||||||
eq(got[5], '')
|
eq(got[5], '')
|
||||||
eq(got[6], 'Frobitz the quux and eschew obfuscation.')
|
eq(got[6], 'Frobitz the quux and eschew obfuscation.')
|
||||||
eq(got[7:], [])
|
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()
|
f.close()
|
||||||
return data
|
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):
|
def check_mode(path, mode, is_file=None, is_dir=None):
|
||||||
st = os.stat(path)
|
st = os.stat(path)
|
||||||
if is_dir:
|
if is_dir:
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -18,6 +18,7 @@ setup(
|
||||||
'gitosis-serve = gitosis.serve:main',
|
'gitosis-serve = gitosis.serve:main',
|
||||||
'gitosis-gitweb = gitosis.gitweb:main',
|
'gitosis-gitweb = gitosis.gitweb:main',
|
||||||
'gitosis-run-hook = gitosis.run_hook:main',
|
'gitosis-run-hook = gitosis.run_hook:main',
|
||||||
|
'gitosis-init = gitosis.init:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue