ctdb-docs: Add ctdb.conf(5) cross references and documentation tweaks
[metze/samba/wip.git] / ctdb / utils / etcd / ctdb_etcd_lock
1 #!/usr/bin/python
2 #
3 #    This program is free software: you can redistribute it and/or modify
4 #    it under the terms of the GNU General Public License as published by
5 #    the Free Software Foundation, either version 3 of the License, or
6 #    (at your option) any later version.
7 #
8 #    This program is distributed in the hope that it will be useful,
9 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #    GNU General Public License for more details.
12 #
13 #    You should have received a copy of the GNU General Public License
14 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 #
16 #    Copyright (C) 2016 Jose A. Rivera <jarrpa@samba.org>
17 #    Copyright (C) 2016 Ira Cooper <ira@samba.org>
18 """CTDB mutex helper using etcd.
19
20 This script is intended to be run as a mutex helper for CTDB. It will try to
21 connect to an existing etcd cluster and grab an etcd.Lock() to function as
22 CTDB's recovery lock. Please see ctdb/doc/cluster_mutex_helper.txt for
23 details on what we're SUPPOSED to be doing. :) To use this, include
24 the following line in the ctdb.conf:
25
26     recovery lock = !/path/to/script
27
28 You can also pass "-v", "-vv", or "-vvv" to include verbose output in the
29 CTDB log. Additional "v"s indicate increases in verbosity.
30
31 This mutex helper expects the system Python interpreter to have access to the
32 etcd Python module. It also expects an etcd cluster to be configured and
33 running. To integrate with this, there is an optional config file of the
34 following format:
35
36 key = value
37
38 The following configuration variables (and their defaults) are defined for
39 use by this script:
40
41 port      = 2379   # connecting port for the etcd cluster
42 lock_ttl  = 9      # seconds for TTL
43 refresh   = 2      # seconds between attempts to maintain lock
44 locks_dir = _ctdb  # where to store CTDB locks in etcd
45                    # The final etcd directory for any given lock looks like:
46                    #   /_locks/{locks_dir}/{netbios name}/
47
48 In addition, any keyword parameter that can be used to configure an etcd
49 client may be specified and modified here. For more documentation on these
50 parameters, see here: https://github.com/jplana/python-etcd/
51
52 """
53 import signal
54 import time
55 import etcd
56 import sys
57 import os
58 import argparse
59 import logging
60 import subprocess
61
62 # Globals ---------------------------------------------------------------------
63 #
64 defaults = { 'config': os.path.join(
65                          os.getenv('CTDB_BASE', '/usr/local/etc/ctdb'),
66                          'etcd'),
67              'verbose' : 0,
68            }
69 helpmsg  = { 'config': 'Configuration file to use. The default behavior ' + \
70                        'is to look is the base CTDB configuration ' + \
71                        'directory, which can be overwritten by setting the' + \
72                        'CTDB_BASE environment variable, for a file called' + \
73                        '\'etcd\'. Default value is ' + defaults['config'],
74              'verbose' : 'Display verbose output to stderr. Default is no output.',
75            }
76
77 log_levels = { 0: logging.ERROR,
78                1: logging.WARNING,
79                2: logging.DEBUG,
80              }
81
82 config_file = defaults['config']
83 verbose = defaults['verbose']
84
85 # Helper Functions ------------------------------------------------------------
86 #
87 def sigterm_handler(signum, frame):
88   """Handler for SIGTERM signals.
89   """
90   sys.exit()
91
92 def print_nonl(out):
93   """Dumb shortcut for printing to stdout with no newline.
94   """
95   sys.stdout.write(str(out))
96   sys.stdout.flush()
97
98 def int_or_not(s):
99   """Try to convert input to an integer.
100   """
101   try:
102     return int(s)
103   except ValueError:
104     return s
105
106 # Mainline --------------------------------------------------------------------
107 #
108 def main():
109   global config_file
110   global verbose
111
112   logging.basicConfig(level=log_levels[verbose])
113
114   # etcd config defaults
115   etcd_config = {
116     'port'        : 2379,
117     'locks_dir'   : '_ctdb',
118     'lock_ttl'    : 9,
119     'lock_refresh': 2,
120   }
121   # Find and read etcd config file
122   etcd_client_params = (
123     'host',
124     'port',
125     'srv_domain',
126     'version_prefix',
127     'read_timeout',
128     'allow_redirect',
129     'protocol',
130     'cert',
131     'ca_cert',
132     'username',
133     'password',
134     'allow_reconnect',
135     'use_proxies',
136     'expected_cluster_id',
137     'per_host_pool_size',
138   )
139   if os.path.isfile(config_file):
140     f = open(config_file, 'r')
141     for line in f:
142       (key, value) = line.split("=",1)
143       etcd_config[key.strip()] = int_or_not(value.strip())
144
145   # Minor hack: call out to shell to retrieve CTDB netbios name and PNN.
146   tmp = subprocess.Popen("testparm -s --parameter-name 'netbios name'; \
147                           ctdb pnn",
148                          shell=True,
149                          universal_newlines=True,
150                          stdout=subprocess.PIPE
151                         ).stdout.read().strip()
152   nb_name, pnn = tmp.split()
153
154   # Try to get and hold the lock
155   try:
156     client = etcd.Client(**{k: etcd_config[k] for k in \
157                          set(etcd_client_params).intersection(etcd_config)})
158     lock = etcd.Lock(client, etcd_config['locks_dir'] + "/" + nb_name)
159     lock._uuid = lock._uuid + "_" + pnn
160     logging.debug("Updated lock UUID: " + lock.uuid)
161     ppid = os.getppid()
162     while True:
163       lock.acquire(blocking=False, lock_ttl=etcd_config['lock_ttl'])
164       if lock.is_acquired:
165         print_nonl(0)
166       else:
167         locks = "No locks found."
168         if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
169           keys = client.read(lock.path, recursive=True)
170           if keys is not None:
171             locks = "Existing locks:\n  "
172             locks += '\n  '.join((child.key + ": " + child.value  for child in keys.children))
173         logging.debug("Lock contention. " + locks)
174         print_nonl(1)
175         break
176       os.kill(ppid, 0)
177       time.sleep(etcd_config['lock_refresh'])
178   except (OSError, SystemExit) as e:
179     if lock is not None and lock.is_acquired:
180       lock.release()
181   except:
182     print_nonl(3)
183     if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
184       raise
185
186 if __name__== "__main__":
187   signal.signal(signal.SIGTERM, sigterm_handler)
188
189   parser = argparse.ArgumentParser(
190                           description=__doc__,
191                           epilog='',
192                           formatter_class=argparse.RawDescriptionHelpFormatter )
193   parser.add_argument( '-v', '--verbose',
194                        action='count',
195                        help=helpmsg['verbose'],
196                        default=defaults['verbose'],
197                      )
198   parser.add_argument( '-c', '--config',
199                        action='store',
200                        help=helpmsg['config'],
201                        default=defaults['config'],
202                      )
203   args = parser.parse_args()
204
205   config_file = args.config
206   verbose     = args.verbose if args.verbose <= 2 else 2
207
208   main()