Page MenuHomePhabricator
Paste P4916

Python version of toollabs remote crontab
ActivePublic

Authored by zhuyifei1999 on Feb 9 2017, 2:41 PM.
Tags
None
Referenced Files
F5571113: Python version of toollabs remote crontab
Feb 9 2017, 2:41 PM
Subscribers
None
#! /usr/bin/python -Es
# -*- coding: UTF-8 -*-
#
# Copyright © 2013 Marc-André Pelletier <mpelletier@wikimedia.org>
# Copyright © 2017 Zhuyifei1999 <zhuyifei1999@gmail.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# This goes in /usr/local/bin to override the system crontab (which will
# have its permissions restricted).
# THIS FILE IS MANAGED BY PUPPET
#
# This goes in /usr/local/bin to override the system crontab (which will
# have its permissions restricted).
#
# Source: modules/toollabs/templates/crontab.erb
# From: toollabs::submit
from __future__ import print_function
import argparse
import pwd
import os
import re
import subprocess
import sys
import tempfile
# TODO: read from somewhere like /etc/toollabs-cronhost
CRON_HOST = 'tools-cron-01.tools.eqiad.wmflabs' # FIXME: <%= @cron_host %>
JSUB_MODIFIED = '''
NOTE: some crontab entries have been modified to grid submissions.
You may want to examine the result with 'crontab -e'.
'''
DEFAULT_CRONTAB = '''\
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# Wikimedia Tool Labs specific note:
# Please be aware that *only* jsub and jstart are acceptable
# commands to schedule via cron. Any command specified here will
# be modified to be invoked through jsub unless it is one of
# the two.
#
# m h dom mon dow command
'''
parser = argparse.ArgumentParser()
parser.add_argument('-u', help=argparse.SUPPRESS,
dest='user')
parser.add_argument('-i', help='prompt before deleting crontab',
action='store_true')
group = parser.add_mutually_exclusive_group()
group.add_argument('file', help='replace crontab with file (default)',
default=sys.stdin, nargs='?', type=argparse.FileType('r'))
subgroup = group.add_mutually_exclusive_group()
subgroup.add_argument('-e', help='edit crontab',
action='store_const', const='e', dest='operation')
subgroup.add_argument('-l', help='list crontab',
action='store_const', const='l', dest='operation')
subgroup.add_argument('-r', help='delete crontab',
action='store_const', const='r', dest='operation')
class NoCrontab(Exception):
pass
class Crontab(object):
def __init__(self, user):
self.user = user
def _remote(self, args, stdin=None):
"""Execute remote crontab command and returns stdout."""
args = ['/usr/bin/ssh', CRON_HOST, '/usr/bin/crontab'] + args
if self.user.pw_uid != os.getuid():
args += ['-u', self.user.pw_name]
ssh = subprocess.Popen(args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdoutdata, stderrdata = ssh.communicate(input=stdin)
if ssh.returncode:
if stderrdata.lower().startswith('no crontab for '):
raise NoCrontab
else:
print(stderrdata, end='', file=sys.stderr)
err('unable to execute remote crontab command')
sys.exit(ssh.returncode)
return stdoutdata
@staticmethod
def _add_jsub(text):
# TODO: regex insanity here
return text
def load(self):
return self._remote(['-l'])
def save(self, text):
jsub_text = self._add_jsub(text)
if jsub_text != text:
print(JSUB_MODIFIED, file=sys.stderr)
self._remote([], stdin=jsub_text)
def remove(self):
self._remote(['-r'])
def editor(text):
with tempfile.NamedTemporaryFile() as f:
f.write(text)
f.flush()
subprocess.check_call(['/usr/bin/sensible-editor', f.name])
f.seek(0)
return f.read()
def err(message):
print('{}: {}'.format(sys.argv[0], message), file=sys.stderr)
def main():
args = parser.parse_args()
if args.i and args.operation != 'r':
parser.error('argument -i: only applicable with -r')
target = pwd.getpwuid(os.getuid())
if args.user is not None:
if os.getuid():
parser.error('argument -u: must be privileged')
try:
target = pwd.getpwnam(args.user)
except KeyError:
parser.error('argument -u: unknown user "{}"'.format(args.user))
if target.pw_uid < 500:
# If the target user is not managed in LDAP and thus likely
# a system user, invoke the original crontab instead.
os.execv('/usr/bin/crontab', sys.argv[1:])
elif target.pw_uid < 40000 and os.getuid():
err('only tools are allowed crontabs')
sys.exit(1)
else:
try:
crontab = Crontab(target)
if args.operation is None:
# Replace
crontab.save(args.file.read())
elif args.operation == 'e':
# Edit
try:
crontab_text = crontab.load()
except NoCrontab:
crontab_text = DEFAULT_CRONTAB
new_crontab_text = editor(crontab_text)
if new_crontab_text == crontab_text:
print('No modification made', file=sys.stderr)
elif not new_crontab_text.strip():
err('cowardly refusing to install empty crontab')
err('use `{} -r` if you want to remove the crontab'.format(
sys.srgv[0]))
sys.exit(1)
else:
crontab.save(new_crontab_text)
elif args.operation == 'l':
# List
try:
print(crontab.load(), end='') # crontab already has lf
except NoCrontab:
print('no crontab for {}'.format(target.pw_name),
file=sys.stderr)
sys.exit(1)
elif args.operation == 'r':
# Delete
try:
crontab.load()
except NoCrontab:
print('no crontab for {}'.format(target.pw_name),
file=sys.stderr)
sys.exit(1)
if args.i:
prompt = "{}: really delete {}'s crontab? (y/n) ".format(
sys.argv[0], target.pw_name)
while True:
try:
stdin = raw_input(prompt).lower()
except EOFError:
stdin = ''
except KeyboardInterrupt:
print(file=sys.stderr)
raise
if stdin == 'y':
crontab.remove()
break
elif stdin == 'n':
break
else:
prompt = 'Please enter Y or N: '
else:
crontab.remove()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
main()