source: trunk/server/common/oursrc/hacron/hacron @ 1467

Last change on this file since 1467 was 1467, checked in by gdb, 14 years ago
Minor hacron fixes
  • Property svn:executable set to *
File size: 10.7 KB
Line 
1#!/usr/bin/env python
2from __future__ import with_statement
3import glob
4import logging.handlers
5import fcntl
6import optparse
7import os
8import socket
9import shutil
10import subprocess
11import sys
12import time
13from os import path
14
15OCF_SUCCESS=0
16OCF_ERR_GENERIC=1
17OCF_ERR_ARGS=2
18OCF_ERR_UNIMPLEMENTED=3
19OCF_ERR_PERM=4
20OCF_ERR_INSTALLED=5
21OCF_ERR_CONFIGURED=6
22OCF_NOT_RUNNING=7
23
24logger = logging.getLogger('cron')
25
26HA_LOGD = os.environ.get('HA_LOGD') == 'yes'
27
28class HacronError(Exception):
29    def __init__(self, errno, msg='Something went wrong'):
30        self.errno = errno
31        self.msg = msg
32        logger.error(msg)
33   
34class HaLogHandler(logging.Handler):
35    """
36    A handler class which writes to ha_logger.
37    """
38    def __init__(self, ha_tag):
39        """
40        Initialize the handler.  ha_tag is the name of this resource.
41        """
42        logging.Handler.__init__(self)
43        self.ha_tag = ha_tag
44
45    def emit(self, record):
46        """
47        Emit a record.
48        """
49        print 'Passed', record
50        try:
51            levelname = record.levelname
52            msg = self.format(record)
53            subprocess.call(['/usr/sbin/ha_logger', '-t', self.ha_tag, msg])
54        except (KeyboardInterrupt, SystemExit):
55            raise
56        except:
57            self.handleError(record)
58
59class lock(object):
60    def __init__(self, filename):
61        self.filename = filename
62        if not _touch(filename):
63            raise
64
65    def __enter__(self):
66        f = open(self.filename)
67        fcntl.flock(f, fcntl.LOCK_EX)
68           
69    def __exit__(self, type, value, traceback):
70        f = open(self.filename)
71        fcntl.flock(f, fcntl.LOCK_UN)
72       
73def _touch(path):
74    """Effectively touches a file.  Returns true if successful, false
75    otherwise"""
76    try:
77        open(path, 'a').close()
78    except IOError:
79        return False
80    else:
81        return True
82
83def _remove(dest):
84    if not path.exists(dest) and not path.islink(dest):
85        logger.info('Tried to remove nonexistant path %s' % dest)
86        return True
87
88    try:
89        if path.isdir(dest):
90            os.rmdir(dest)
91        else:
92            os.remove(dest)
93    except OSError, e:
94        logging.error('Could not remove %s: %s' % (dest, e))
95        return False
96    else:
97        return True
98
99def _mkdir(dir):
100    try:
101        os.mkdir(dir)
102    except OSError, e:
103        logging.error('Could not mkdir %s: %s' % (dir, e))
104        return False
105    else:
106        return True
107   
108def _strip(name):
109    """Strip off the file extension, and leading /'s, if they exist"""
110    return path.splitext(path.basename(name))[0]
111
112def _suffix(name, suffix):
113    return '%s.%s' % (name, suffix)
114
115def _crondir(server):
116    return path.join(CRONSPOOL_DIR, _suffix(server, 'cronspool'))
117
118def _serverfile(server):
119    return path.join(SERVER_DIR, server)
120
121def _servers():
122    """Get a list of the servers."""
123    return [_strip(f) for f in glob.glob(path.join(SERVER_DIR, '*'))]
124
125def _is_master(server):
126    crondir = path.join(CRONSPOOL_DIR, _suffix(server, 'cronspool'))
127    return path.islink(crondir)
128
129def _restart_crond(args, options):
130    # TODO: insert correct cmd here.  Also, should we capture and log
131    # stdout?
132    if options.development:
133        cmd = ['echo', 'called crond reset']
134    else:
135        cmd = ['service', 'crond', 'reload']
136    try:
137        subprocess.check_call(cmd)
138    except OSError, e:
139        raise HacronError(OCF_ERR_GENERIC, 'Cron restart exited with return code %d' % e.errno)
140    else:
141        logger.info('Restarted crond')
142
143def start_cron(args, options):
144    if not _touch(_serverfile(HOSTNAME)):
145        return OCF_ERR_CONFIGURED
146    elif _is_master(HOSTNAME):
147        logger.error('%s is already the master!' % HOSTNAME)
148        return OCF_SUCCESS
149
150    logger.info('Starting %s' % HOSTNAME)
151    for server in _servers():
152        crondir = _crondir(server)
153        if server == HOSTNAME:
154            # Get rid of current crondir, and leave if that fails.
155            if not _remove(crondir):
156                logger.error("Could not remove dummy cronspool dir %s" % crondir)
157                return OCF_ERR_GENERIC
158            os.symlink('../cronspool', crondir)
159            logger.info('Created master symlink %s' % crondir)
160        else:
161            if path.islink(crondir):
162                _remove(crondir)
163                logger.info('Removed old master symlink: %s' % crondir)
164            if not path.exists(crondir):
165                _mkdir(crondir)
166                logger.info('Created slave dummy directory %s' % crondir)
167    try:
168        _restart_crond(args, options)
169    except OSError, e:
170        return e.errno
171    return OCF_SUCCESS
172
173def stop_cron(args, options):
174    """Stop cron."""
175    if not _is_master(HOSTNAME):
176        logger.error('I am not the master!')
177        return OCF_NOT_RUNNING
178    else:
179        crondir = _crondir(HOSTNAME)
180        logger.info('Removing symlink %s' % crondir)
181        _remove(crondir)
182        _mkdir(crondir)
183        # TODO: should we do something else here?
184        try:
185            _restart_crond(args, options)
186        except OSError, e:
187            return e.errno
188        return OCF_SUCCESS
189
190def monitor_cron(args, options):
191    """Check whether cron is running.  For now just makes sure that the
192    current machine is the master, although this should likely be fixed."""
193    if _is_master(HOSTNAME):
194        return OCF_SUCCESS
195    else:
196        return OCF_NOT_RUNNING
197
198def validate_all_cron(args, options):
199    if not _touch(_serverfile(HOSTNAME)):
200        logger.error('Could not touch %s' % _serverfile(HOSTNAME))
201        return OCF_GENERIC_ERR
202    elif not path.exists(CRONSPOOL_DIR):
203        return OCF_GENERIC_ERR
204    else:
205        return OCF_SUCCESS
206
207def setup(args, options):
208    for d in [CRONSPOOL_DIR, SERVER_DIR]:
209        if not path.exists(d):
210            os.makedirs(d)
211            logger.info('Created %s' % d)
212        else:
213            logger.info('Already exists: %s' % d)
214
215def remove_servers(servers, options):
216    """Remove servers from the list of available ones."""
217    for server in servers:
218        os.unlink(_serverfile(server))
219
220def meta_data_cron(args, options):
221    print """<?xml version="1.0"?>
222<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
223<resource-agent name="hacron" version="0.1">
224<version>1.0</version>
225
226<longdesc lang="en">
227This is the high-availability cron manager.  It uses an extremely overpowered
228clustering solution to make it so that people can have their crontabs.  Yay.
229</longdesc>
230<shortdesc lang="en">HA Cron</shortdesc>
231
232<parameters>
233<parameter name="cron_root" required="1">
234<longdesc lang="en">
235Base directory for storage of crontabs and server information.
236</longdesc>
237<shortdesc lang="en">Cron base directory</shortdesc>
238<content type="string" />
239</parameter>
240</parameters>
241
242<actions>
243<action name="start"        timeout="90" />
244<action name="stop"         timeout="100" />
245<action name="monitor"      timeout="20" interval="10" depth="0" start-delay="0" />
246<action name="reload"       timeout="90" />
247<action name="meta-data"    timeout="5" />
248<action name="validate-all"   timeout="30" />
249</actions>
250</resource-agent>
251"""
252    return OCF_SUCCESS
253
254def usage(parser):
255    parser.print_help()
256    return 1
257
258def _set_globals(args, options):
259    global HOSTNAME, CRONROOT, CRONSPOOL_DIR, SERVER_DIR, \
260        HA_RSCTMP, OCF_RESOURCE_INSTANCE
261    if options.development:
262        logging.basicConfig(level=logging.DEBUG)
263    else:
264        if HA_LOGD:
265            handler = HaLogHandler('hacron')
266        else:
267            handler = logging.handlers.SysLogHandler('/dev/log')
268        formatter = logging.Formatter("%(module)s: %(levelname)s %(message)s")
269        handler.setLevel(logging.INFO)
270        handler.setFormatter(formatter)
271        logger.addHandler(handler)
272    HOSTNAME = options.server or os.environ.get('HA_CURHOST') or socket.gethostname()
273    CRONROOT = options.cronroot or os.environ.get('OCF_RESKEY_cron_root')
274    if not CRONROOT:
275        raise HacronError(OCF_ERR_CONFIGURED, 'No cron_root specified.')
276    CRONSPOOL_DIR = path.join(CRONROOT, 'server-cronspools')
277    SERVER_DIR = path.join(CRONROOT, 'servers')
278    HA_RSCTMP = os.environ.get('HA_RSCTMP', '/tmp')
279    OCF_RESOURCE_INSTANCE = os.environ.get('OCF_RESOURCE_INSTANCE', 'default')
280    return OCF_SUCCESS
281
282def main():
283    usage_str = """usage: %prog [-s server] [-c cronroot] [-d] cmd
284
285Script for starting and stopping cron in a multiserver environment.
286One server is designated the master.
287
288== HA available commands: ==
289start: Make this server into the master and reload crond.
290reload: Same as start.
291stop: Demote this server to a spare and reload crond.
292monitor: Indicate whether this server is successfully the master.
293validate-all: Make sure that things look right and this server is
294  ready to be promoted to master.
295meta-data: Print out the XML meta data for this service
296
297== User-only commands: ==
298setup: Create the folders, etc. necessary for running hacron.
299remove-servers server1 server2 ...: Take a list of servers out of the
300  list of available ones.
301    """
302    parser = optparse.OptionParser(usage=usage_str)
303    parser.add_option("-s", "--server",
304                      action="store", dest="server",
305                      default=None,
306                      help="choose which server to run script as")
307    parser.add_option("-c", "--cronroot",
308                      action="store", dest="cronroot",
309                      default=None,
310                      help="pick root of cron dir")
311    parser.add_option("-d", "--development",
312                      action="store_true", dest="development",
313                      default=False,
314                      help="run in development mode")
315    (options, args) = parser.parse_args()
316    if len(args) < 1:
317        return usage(parser)
318    command = args[0]
319    args = args[1:]
320
321    if command == 'meta-data':
322        return meta_data_cron(args, options)
323
324    try:
325        _set_globals(args, options)
326    except HacronError, e:
327        return e.errno
328
329    with lock('%s/hacron-%s.lock' % (HA_RSCTMP, OCF_RESOURCE_INSTANCE)):
330        if command == 'start':
331            return start_cron(args, options)
332        elif command == 'reload':
333            return start_cron(args, options)
334        elif command == 'stop':
335            return stop_cron(args, options)
336        elif command == 'monitor':
337            return monitor_cron(args, options)
338        elif command == 'validate-all':
339            return validate_all_cron(args, options)
340        elif command == 'setup':
341            return setup(args, options)
342        elif command == 'remove-servers':
343            return remove_servers(args, options)
344        else:
345            usage(parser)
346            return OCF_ERR_UNIMPLEMENTED
347
348if __name__ == '__main__':
349    try:
350        ret = main()
351    except Exception, e:
352        logger.error('exception from main: %s' % e)
353        ret = OCF_ERR_GENERIC
354        raise
355    sys.exit(ret)
Note: See TracBrowser for help on using the repository browser.