aboutsummaryrefslogtreecommitdiff
path: root/grok-shell
blob: 16b7c34392c2960bf6e2e4c944389b4beb97fe72 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/python3

import logging
import logging.handlers
import os
import shlex
import subprocess
import sys
import time
import urllib.parse

import configparser

log = logging.getLogger('grok-shell')


def setup_logging():
    log.setLevel(logging.INFO)

    # rotate logs every sunday, and retain for 5 weeks
    handler = logging.handlers.TimedRotatingFileHandler(
        '/tmp/grok-shell.log', when='W6', backupCount=5)
    formatter = logging.Formatter(
        '%(asctime)s %(levelname)-5s %(name)s: %(message)s',
        datefmt='%H:%M:%S')
    handler.setFormatter(formatter)
    log.addHandler(handler)


def _slave_local_fetch(cmd):
    cmd, repo = cmd.split(' ', 1)
    # git-shell command has an undocumented requirement that the repo be
    # wrapped in quotes
    assert repo[0] == "'" and repo[-1] == "'"
    repo = "'/srv/repositories/" + repo[1:]
    args = ['/usr/bin/git-shell', '-c', "%s %s" % (cmd, repo)]
    os.execv(args[0], args)


def _get_master():
    ini = configparser.ConfigParser()
    ini.read('/etc/grokmirror-repos.conf')
    master = ini.get(ini.sections()[0], 'site')
    return urllib.parse.urlparse(master).netloc


def _slave_remote_cmd(user, cmd):
    # we have to use posix=False or shlex will try and strip out quotes for
    # an arg like 'people/foo.bar/repo'. The quotes are required by git-shell
    # as noted in _slave_local_fetch
    cmd = shlex.split(cmd, posix=False)
    args = ['/usr/bin/ssh', _get_master(), user] + cmd
    start = time.time()
    ret = subprocess.call(args)
    if ret == 0 and args[3] == 'git-receive-pack':
        log.info('remote update for %s %s took %d ms',
                 user, cmd, time.time() - start)
        start = time.time()
        # help make the change show up on the local grok slave as quick as
        # possible, this won't update the grok-manifest, but our cron job will
        # take care of that when it runs next.
        repo = args[4]
        assert repo[0] == "'" and repo[-1] == "'"
        repo = '/srv/repositories/' + repo[1:-1]
        if not repo.endswith('.git'):
            repo += '.git'
        # only do updates to existing repos, let cron job handle clones
        if os.path.exists(repo):
            os.write(2, b'Updating local grokmirror with changes on master\n')
            subprocess.call(
                ['/usr/bin/git', 'remote', 'update', '--prune'], cwd=repo)
            log.info('local update for %s %s took %d ms',
                     user, cmd, time.time() - start)

    sys.exit(ret)


def _master_shell(cmd):
    user, cmd = cmd.split(' ', 1)

    os.environ['SSH_ORIGINAL_COMMAND'] = cmd
    os.environ['SSH_CONNECTION'] = '1'

    args = ['/home/git/gitolite/src/gitolite-shell', user]
    os.execv(args[0], args)


if __name__ == '__main__':
    '''This script is called in 2 different ways:
    1) on grokmirror slave, it will determine if we are doing a read or write
       operation. If a read, we'll keep the request local. If a write, we'll
       send a request to the master to complete
    2) on grokmirror master, it will accept the remote command from #1 and
       handle its request.

    Both slave and master require special .ssh/authorzied_key entries to be
    in place.
    '''
    setup_logging()

    cmd = os.environ.get('SSH_ORIGINAL_COMMAND', 'info')

    if len(sys.argv) < 2 or sys.argv[1] not in ('slave', 'master'):
        sys.exit('Usage: %s [slave|master] ...' % sys.argv[0])

    shell_type = sys.argv[1]
    if shell_type == 'master':
        log.info('command from slave: %s', cmd)
        _master_shell(cmd)
    elif shell_type == 'slave':
        user = sys.argv[2]

        if cmd.startswith('git-upload-'):
            log.info('%s: local: %s', user, cmd)
            _slave_local_fetch(cmd)
        else:
            log.info('%s: remote: %s', user, cmd)
            _slave_remote_cmd(user, cmd)