xref: /6.0.3/ns_server/cluster_run (revision cba22b9b)
1#!/usr/bin/env python
2#
3# @author Couchbase <info@couchbase.com>
4# @copyright 2011-2018 Couchbase, Inc.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17import os, os.path, subprocess, sys, signal, atexit, socket, getopt, select, shlex
18import platform, fnmatch
19
20base_direct_port = 12000
21base_api_port = 9000
22base_couch_port = 9500
23base_projector_port = 10000
24base_xdcr_port = 13000
25base_indexer_port = 9100
26base_fts_port = 9200
27base_eventing_port = 9300
28base_cbas_port = 9600
29
30LOGLEVELS = ["debug", "info", "warn", "error", "critical"]
31
32def read_configuration():
33    with open("build/cluster_run.configuration") as f:
34        def fn(line):
35            k, v = line.strip().split('=')
36            return k, shlex.split(v)[0]
37
38        return dict(fn(line) for line in f.readlines())
39
40config = read_configuration()
41PREFIX = config['prefix']
42
43def setup_path():
44    def ebin_search(path):
45        dirs = os.walk(path)
46        ebins = []
47
48        for d, _, _ in dirs:
49            if os.path.basename(d) == "ebin":
50                ebins.append(d)
51
52        return ebins
53
54    path = ebin_search(".")
55    couchpath = ebin_search("{0}/lib/couchdb/erlang/lib".format(PREFIX))
56    couch_plugins = ebin_search("{0}/lib/couchdb/plugins".format(PREFIX))
57
58    if len(couchpath) == 0:
59       sys.exit("Couch libs wasn't found.\nCan't handle it")
60
61    return couchpath + path + couch_plugins
62
63def mk_node_couch_config(i):
64    try:
65        os.mkdir("couch")
66    except os.error:
67        pass
68
69    with open("couch/n_{0}_conf.ini".format(i), "w") as f:
70        f.write("[httpd]\n")
71        f.write("port={0}\n".format(base_couch_port + i))
72        f.write("[couchdb]\n")
73        f.write("database_dir={0}/data/n_{1}/data\n".format(os.getcwd(), i))
74        f.write("view_index_dir={0}/data/n_{1}/data\n".format(os.getcwd(), i))
75        f.write("max_dbs_open=10000\n")
76        f.write("[upr]\n")
77        f.write("port={0}\n".format(base_direct_port + i * 2))
78        f.write("[dcp]\n")
79        f.write("port={0}\n".format(base_direct_port + i * 2))
80
81
82def couch_configs(i):
83    mk_node_couch_config(i)
84    return ["{0}/etc/couchdb/default.ini".format(PREFIX),
85            "{0}/etc/couchdb/default.d/capi.ini".format(PREFIX),
86            "{0}/etc/couchdb/default.d/geocouch.ini".format(PREFIX),
87            "couch/n_{0}_conf.ini".format(i)]
88
89def os_specific(args, params):
90    """Add os-specific junk to the cluster startup."""
91    if platform.system() == 'Windows':
92        args += ["dont_suppress_stderr_logger", "false"]
93    else:
94        args += ["dont_suppress_stderr_logger", "true"]
95    if platform.system() == 'Darwin':
96        import resource
97        ## OS X has a pretty tiny default fd limit.  Let's increase it (if it hasn't already been).
98        (soft, hard) = resource.getrlimit(resource.RLIMIT_NOFILE)
99        if soft < 2048:
100            resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 2048))
101        params['env'] = {"ERL_MAX_PORTS": "2048"}
102        params['env'].update(os.environ)
103
104ebin_path = None
105cluster_extra_args = None
106cluster_args_prefix = None
107
108def is_ipv6_setup():
109    return os.getenv("IPV6", "false") == "true"
110
111def prepare_start_cluster(extra_args, args_prefix):
112    global cluster_args_prefix
113    global cluster_extra_args
114    global ebin_path
115
116    ebin_path = setup_path()
117    cluster_extra_args = extra_args
118    cluster_args_prefix = args_prefix
119
120def quote_string_for_erl(s):
121    return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"") + '"'
122
123def erlang_args_for_node(i):
124    logdir = "logs/n_{0}".format(i)
125
126    args = cluster_args_prefix + ["erl", "+MMmcs" "30",
127                                  "+A", "16", "+sbtu",
128                                  "+P", "327680", "-pa"] + ebin_path
129    args += [
130        "-setcookie", "nocookie",
131        "-kernel", "inet_dist_listen_min", "21100",
132        "inet_dist_listen_max", "21199",
133        "error_logger", "false",
134        "-sasl", "sasl_error_logger", "false",
135        "-couch_ini"] + couch_configs(i)
136
137    datadir = os.path.abspath('data/n_{0}'.format(i))
138    tempdir = os.path.abspath('tmp/')
139    nodefile = os.path.join(datadir, "nodefile")
140    babysitternodefile = os.path.join(datadir, "couchbase-server.babysitter.node")
141    babysittercookiefile = os.path.join(datadir, "couchbase-server.babysitter.cookie")
142
143    args += [
144        "-name", "babysitter_of_n_{0}@::1".format(i) if ipv6 else "babysitter_of_n_{0}@127.0.0.1".format(i),
145        "-proto_dist", "inet6_tcp" if ipv6 else "inet_tcp",
146        "-hidden",
147        "-kernel", "global_enable_tracing", "true",
148        "-ns_babysitter", "cookiefile", quote_string_for_erl(babysittercookiefile),
149        "-ns_babysitter", "nodefile", quote_string_for_erl(babysitternodefile),
150        "-ns_server", "config_path", '"etc/static_config.in"',
151        "-ns_server", "ipv6", "true" if ipv6 else "false",
152        "error_logger_mf_dir", quote_string_for_erl(logdir),
153        "path_config_etcdir", '"priv"',
154        "path_config_bindir", quote_string_for_erl(PREFIX+"/bin"),
155        "path_config_libdir", quote_string_for_erl(PREFIX+"/lib"),
156        "path_config_datadir", quote_string_for_erl(datadir),
157        "path_config_tmpdir", quote_string_for_erl(tempdir),
158        "path_config_secdir", quote_string_for_erl(PREFIX+"/etc/security"),
159        "path_audit_log", quote_string_for_erl(logdir),
160        "rest_port", str(base_api_port + i),
161        "query_port", str(base_couch_port - 1 - i),
162        "ssl_query_port", str(10000 + base_couch_port - 1 - i),
163        "projector_port", str(base_projector_port + i),
164        "ssl_rest_port", str(10000 + base_api_port + i),
165        "capi_port", str(base_couch_port + i),
166        "ssl_capi_port", str(10000 + base_couch_port + i),
167        "memcached_port", str(base_direct_port + i * 2),
168        "moxi_port", str(base_direct_port + i * 2 + 1),
169        "memcached_dedicated_port", str(base_direct_port - 1 - i * 4),
170        "ssl_proxy_downstream_port", str(base_direct_port - 2 - i * 4),
171        "ssl_proxy_upstream_port", str(base_direct_port - 3 - i * 4),
172        "memcached_ssl_port", str(base_direct_port - 4 - i * 4),
173        "nodefile", quote_string_for_erl(nodefile),
174        "short_name", quote_string_for_erl('n_{0}'.format(i)),
175        "xdcr_rest_port", str(base_xdcr_port + i),
176        "indexer_admin_port", str(base_indexer_port + i * 6),
177        "indexer_scan_port", str(base_indexer_port + i * 6 + 1),
178        "indexer_http_port", str(base_indexer_port + i * 6 + 2),
179        "indexer_https_port", str(10000 + base_indexer_port + i * 6 + 2),
180        "indexer_stinit_port", str(base_indexer_port + i * 6 + 3),
181        "indexer_stcatchup_port", str(base_indexer_port + i * 6 + 4),
182        "indexer_stmaint_port", str(base_indexer_port + i * 6 + 5),
183        "fts_http_port", str(base_fts_port + i),
184        "fts_ssl_port", str(10000 + base_fts_port + i),
185        "eventing_http_port", str(base_eventing_port + i),
186        "eventing_https_port", str(10000 + base_eventing_port + i),
187        "cbas_http_port", str(base_cbas_port + i * 15),
188        "cbas_cc_http_port", str(base_cbas_port + i * 15 + 1),
189        "cbas_cc_cluster_port", str(base_cbas_port + i * 15 + 2),
190        "cbas_cc_client_port", str(base_cbas_port + i * 15 + 3),
191        "cbas_hyracks_console_port", str(base_cbas_port + i * 15 + 4),
192        "cbas_cluster_port", str(base_cbas_port + i * 15 + 5),
193        "cbas_data_port", str(base_cbas_port + i * 15 + 6),
194        "cbas_result_port", str(base_cbas_port + i * 15 + 7),
195        "cbas_messaging_port", str(base_cbas_port + i * 15 + 8),
196        "cbas_debug_port", str(base_cbas_port + i * 15 + 9),
197        "cbas_auth_port", str(base_cbas_port + i * 15 + 10),
198        "cbas_admin_port", str(base_cbas_port + i * 15 + 11),
199        "cbas_replication_port", str(base_cbas_port + i * 15 + 12),
200        "cbas_metadata_port", str(base_cbas_port + i * 15 + 13),
201        "cbas_metadata_callback_port", str(base_cbas_port + i * 15 + 14),
202        "cbas_ssl_port", str(10000 + base_cbas_port + i)
203        ] + cluster_extra_args
204
205    return args
206
207def start_cluster(num_nodes, start_index, host, extra_args, args_prefix):
208    prepare_start_cluster(extra_args, args_prefix)
209
210    def start_node(i):
211        logdir = "logs/n_{0}".format(i)
212        try:
213            os.makedirs(logdir)
214        except:
215            pass
216
217        args = erlang_args_for_node(i)
218
219        params = {}
220
221        os_specific(args, params)
222
223        if not params.has_key('env'):
224            params['env'] = {}
225            params['env'].update(os.environ)
226        path = params['env']['PATH']
227        path = (PREFIX+"/bin") + os.pathsep + path
228        if not params['env'].has_key('ERL_FULLSWEEP_AFTER'):
229            params['env']['ERL_FULLSWEEP_AFTER'] = '512'
230        params['env']['PATH'] = path
231
232        crash_dump_base = 'erl_crash.dump.n_%d' % i
233        params['env']['ERL_CRASH_DUMP_BASE'] = crash_dump_base
234        params['env']['ERL_CRASH_DUMP'] = crash_dump_base + '.babysitter'
235
236        params['env']['COUCHBASE_SMALLER_PKEYS'] = '1'
237
238        params['close_fds'] = True
239        if platform.system() == "Windows":
240            params['close_fds'] = False
241
242        w = None
243
244        if "-noinput" in args:
245            (r,w) = os.pipe()
246
247            params['stdin'] = r
248
249            if 'setpgrp' in os.__dict__ and params.get('close_fds'):
250                # this puts child out of our process group. So that
251                # Ctrl-C doesn't deliver SIGINT to it, leaving us
252                # ability to it shutdown carefully or otherwise
253                params['preexec_fn'] = os.setpgrp
254
255        pr = subprocess.Popen(args, **params)
256        if w != None:
257            os.close(r)
258        pr.write_side = w
259        return pr
260
261    return [start_node(i + start_index) for i in xrange(num_nodes)]
262
263def usage():
264    sys.exit("Usage: {0} [--nodes=N] [--dont-rename] [--dont-start] "
265             "[--interactive] [--static-cookie] [--start-index=N] "
266             "[--static-cookie] [--host=H] [--loglevel=L] "
267             "[--pluggable-config=File] [--minified] [--disable-autocomplete]"
268             "[ns_server args]".format(sys.argv[0]))
269
270def find_primary_addr():
271    Family = socket.AF_INET6 if ipv6 else socket.AF_INET
272    DnsAddr = "2001:4860:4860::8844" if ipv6 else "8.8.8.8"
273    s = socket.socket(Family, socket.SOCK_DGRAM)
274    try:
275        s.connect((DnsAddr, 53))
276        if ipv6:
277            addr, port, _, _ = s.getsockname()
278        else:
279            addr, port = s.getsockname()
280
281        return addr
282    except socket.error:
283        return None
284    finally:
285        s.close()
286
287def main():
288    try:
289        optlist, args = getopt.gnu_getopt(sys.argv[1:], "hn:i",
290                                          ["help", "start-index=", "nodes=",
291                                           "dont-rename", "interactive",
292                                           "static-cookie", "dont-start",
293                                           "host=", "loglevel=",
294                                           "prepend-extras", "pluggable-config=",
295                                           "minified", "disable-autocomplete"])
296    except getopt.GetoptError, err:
297        # print help information and exit:
298        print str(err) # will print something like "option -a not recognized"
299        usage()
300        sys.exit(2)
301
302    global ipv6
303    ipv6 = is_ipv6_setup()
304
305    dont_rename = False
306    dont_start = False
307    static_cookie = False
308    start_index = 0
309    num_nodes = 1
310    prepend_extras = False
311    host = "127.0.0.1"
312    loglevel = 'debug'
313    pluggable_config = []
314    use_minified = False
315    disable_autocomplete = "{disable_autocomplete,false}"
316
317    for o, a in optlist:
318        if o in ("--nodes", "-n"):
319            num_nodes = int(a)
320        elif o == '--dont-start':
321            dont_start = True
322        elif o == '--host':
323            host = a
324        elif o == '--start-index':
325            start_index = int(a)
326        elif o == '--dont-rename':
327            dont_rename = True
328        elif o in ("--help", "-h"):
329            usage()
330            exit(0)
331        elif o in("--static-cookie"):
332            static_cookie = True
333        elif o == '--loglevel':
334            loglevel = a
335        elif o == "--prepend-extras":
336            prepend_extras = True
337        elif o == "--pluggable-config":
338            pluggable_config.append(a)
339        elif o == "--minified":
340            use_minified = True
341        elif o == "--disable-autocomplete":
342            disable_autocomplete = "{disable_autocomplete,true}"
343        else:
344            assert False, "unhandled options"
345
346    nodes = []
347    terminal_attrs = None
348
349    def kill_nodes(*args):
350        for n in nodes:
351            if n.write_side != None:
352                print("Closing %d\n" % n.write_side)
353                # os.write(n.write_side, "shutdown\n") # this line does graceful shutdown versus quick
354                os.close(n.write_side)
355            else:
356                try:
357                    n.kill()
358                except OSError:
359                    pass
360
361        for n in nodes:
362            n.wait()
363
364        if terminal_attrs != None:
365            termios.tcsetattr(sys.stdin, termios.TCSANOW, terminal_attrs)
366
367    atexit.register(kill_nodes)
368
369    try:
370        import termios
371        terminal_attrs = termios.tcgetattr(sys.stdin)
372    except:
373        pass
374
375    extra_args = []
376    if not dont_rename:
377        primary_addr = find_primary_addr()
378        if primary_addr == None:
379            print("was unable to detect 'internet' address of this machine."
380                  + " node rename will be disabled")
381        else:
382            extra_args += ["rename_ip", '"' + primary_addr + '"']
383
384    extra_args += args[1:]
385    if prepend_extras:
386        prepend_args = args[0:]
387    else:
388        prepend_args = []
389        extra_args += args[0:]
390
391    if static_cookie:
392        extra_args += ["-ns_server", "dont_reset_cookie", "true"]
393
394    if dont_start:
395        extra_args += ["-run", "t", "fake_loggers"]
396    else:
397        extra_args += ["-noinput"]
398        extra_args += ["-run", "child_erlang", "child_start", "ns_babysitter_bootstrap"]
399        extra_args += ["-ns_babysitter", "handle_ctrl_c", "true"]
400
401    if loglevel not in LOGLEVELS:
402        print "Valid log levels are the following: %s" % ', '.join(LOGLEVELS)
403        sys.exit(1)
404    extra_args += ["-ns_server", "loglevel_stderr", loglevel]
405
406    plugins_dir = '../build/cluster_run_ui_plugins'
407    if os.path.isdir(plugins_dir):
408        for file in os.listdir(plugins_dir):
409            if fnmatch.fnmatch(file, 'pluggable-ui-*.cluster_run.json'):
410                pluggable_config.append(os.path.join(plugins_dir, file))
411
412    if pluggable_config:
413        extra_args += ["-ns_server", "ui_plugins",
414                        quote_string_for_erl(','.join(pluggable_config))]
415
416    ui_env = [disable_autocomplete]
417
418    extra_args += ["-ns_server", "use_minified", "true" if use_minified else "false"]
419    extra_args += ["-ns_server", "ui_env", '[' + ','.join(ui_env) + ']']
420
421    nodes = start_cluster(num_nodes, start_index, host, extra_args, prepend_args)
422
423    for node in nodes:
424        node.wait()
425
426
427if __name__ == '__main__':
428    main()
429