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.
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.
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/>.
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.
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:
26 recovery lock = !/path/to/script
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.
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
38 The following configuration variables (and their defaults) are defined for
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}/
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/
62 # Globals ---------------------------------------------------------------------
64 defaults = { 'config': os.path.join(
65 os.getenv('CTDB_BASE', '/usr/local/etc/ctdb'),
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.',
77 log_levels = { 0: logging.ERROR,
82 config_file = defaults['config']
83 verbose = defaults['verbose']
85 # Helper Functions ------------------------------------------------------------
87 def sigterm_handler(signum, frame):
88 """Handler for SIGTERM signals.
93 """Dumb shortcut for printing to stdout with no newline.
95 sys.stdout.write(str(out))
99 """Try to convert input to an integer.
106 # Mainline --------------------------------------------------------------------
112 logging.basicConfig(level=log_levels[verbose])
114 # etcd config defaults
117 'locks_dir' : '_ctdb',
121 # Find and read etcd config file
122 etcd_client_params = (
136 'expected_cluster_id',
137 'per_host_pool_size',
139 if os.path.isfile(config_file):
140 f = open(config_file, 'r')
142 (key, value) = line.split("=",1)
143 etcd_config[key.strip()] = int_or_not(value.strip())
145 # Minor hack: call out to shell to retrieve CTDB netbios name and PNN.
146 tmp = subprocess.Popen("testparm -s --parameter-name 'netbios name'; \
149 universal_newlines=True,
150 stdout=subprocess.PIPE
151 ).stdout.read().strip()
152 nb_name, pnn = tmp.split()
154 # Try to get and hold the lock
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)
163 lock.acquire(blocking=False, lock_ttl=etcd_config['lock_ttl'])
167 locks = "No locks found."
168 if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
169 keys = client.read(lock.path, recursive=True)
171 locks = "Existing locks:\n "
172 locks += '\n '.join((child.key + ": " + child.value for child in keys.children))
173 logging.debug("Lock contention. " + locks)
177 time.sleep(etcd_config['lock_refresh'])
178 except (OSError, SystemExit) as e:
179 if lock is not None and lock.is_acquired:
183 if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
186 if __name__== "__main__":
187 signal.signal(signal.SIGTERM, sigterm_handler)
189 parser = argparse.ArgumentParser(
192 formatter_class=argparse.RawDescriptionHelpFormatter )
193 parser.add_argument( '-v', '--verbose',
195 help=helpmsg['verbose'],
196 default=defaults['verbose'],
198 parser.add_argument( '-c', '--config',
200 help=helpmsg['config'],
201 default=defaults['config'],
203 args = parser.parse_args()
205 config_file = args.config
206 verbose = args.verbose if args.verbose <= 2 else 2