1#!/usr/bin/perl -w
2
3# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
4#
5# SPDX-License-Identifier: MPL-2.0
6#
7# This Source Code Form is subject to the terms of the Mozilla Public
8# License, v. 2.0.  If a copy of the MPL was not distributed with this
9# file, you can obtain one at https://mozilla.org/MPL/2.0/.
10#
11# See the COPYRIGHT file distributed with this work for additional
12# information regarding copyright ownership.
13
14# Framework for starting test servers.
15# Based on the type of server specified, check for port availability, remove
16# temporary files, start the server, and verify that the server is running.
17# If a server is specified, start it. Otherwise, start all servers for test.
18
19use strict;
20use warnings;
21
22use Cwd ':DEFAULT', 'abs_path';
23use English '-no_match_vars';
24use Getopt::Long;
25use Time::HiRes 'sleep'; # allows sleeping fractional seconds
26
27# Usage:
28#   perl start.pl [--noclean] [--restart] [--port port] [--taskset cpus] test [server [options]]
29#
30#   --noclean       Do not cleanup files in server directory.
31#
32#   --restart       Indicate that the server is being restarted, so get the
33#                   server to append output to an existing log file instead of
34#                   starting a new one.
35#
36#   --port port     Specify the default port being used by the server to answer
37#                   queries (default 5300).  This script will interrogate the
38#                   server on this port to see if it is running. (Note: for
39#                   "named" nameservers, this can be overridden by the presence
40#                   of the file "named.port" in the server directory containing
41#                   the number of the query port.)
42#
43#   --taskset cpus  Use taskset to signal which cpus can be used. For example
44#                   cpus=fff0 means all cpus aexcept for 0, 1, 2, and 3 are
45#                   eligible.
46#
47#   test            Name of the test directory.
48#
49#   server          Name of the server directory.  This will be of the form
50#                   "nsN" or "ansN", where "N" is an integer between 1 and 8.
51#                   If not given, the script will start all the servers in the
52#                   test directory.
53#
54#   options         Alternate options for the server.
55#
56#                   NOTE: options must be specified with '-- "<option list>"',
57#                   for instance: start.pl . ns1 -- "-c n.conf -d 43"
58#
59#                   ALSO NOTE: this variable will be filled with the contents
60#                   of the first non-commented/non-blank line of args in a file
61#                   called "named.args" in an ns*/ subdirectory. Only the FIRST
62#                   non-commented/non-blank line is used (everything else in
63#                   the file is ignored). If "options" is already set, then
64#                   "named.args" is ignored.
65
66my $usage = "usage: $0 [--noclean] [--restart] [--port <port>] [--taskset <cpus>] test-directory [server-directory [server-options]]";
67my $clean = 1;
68my $restart = 0;
69my $queryport = 5300;
70my $taskset = "";
71
72GetOptions(
73	'clean!'    => \$clean,
74	'restart!'  => \$restart,
75	'port=i'    => \$queryport,
76	'taskset=s' => \$taskset,
77) or die "$usage\n";
78
79my( $test, $server_arg, $options_arg ) = @ARGV;
80
81if (!$test) {
82	die "$usage\n";
83}
84
85# Global variables
86my $topdir = abs_path($ENV{'SYSTEMTESTTOP'});
87my $testdir = abs_path($topdir . "/" . $test);
88
89if (! -d $testdir) {
90	die "No test directory: \"$testdir\"\n";
91}
92
93if ($server_arg && ! -d "$testdir/$server_arg") {
94	die "No server directory: \"$testdir/$server_arg\"\n";
95}
96
97my $NAMED = $ENV{'NAMED'};
98my $DIG = $ENV{'DIG'};
99my $PERL = $ENV{'PERL'};
100my $PYTHON = $ENV{'PYTHON'};
101
102# Start the server(s)
103
104my @ns;
105my @ans;
106
107if ($server_arg) {
108	if ($server_arg =~ /^ns/) {
109		push(@ns, $server_arg);
110	} elsif ($server_arg =~ /^ans/) {
111		push(@ans, $server_arg);
112	} else {
113		print "$0: ns or ans directory expected";
114		print "I:$test:failed";
115	}
116} else {
117	# Determine which servers need to be started for this test.
118	opendir DIR, $testdir or die "unable to read test directory: \"$test\" ($OS_ERROR)\n";
119	my @files = sort readdir DIR;
120	closedir DIR;
121
122	@ns = grep /^ns[0-9]*$/, @files;
123	@ans = grep /^ans[0-9]*$/, @files;
124}
125
126# Start the servers we found.
127
128foreach my $name(@ns) {
129	&check_ns_port($name);
130	&start_ns_server($name, $options_arg);
131	&verify_ns_server($name);
132}
133
134foreach my $name(@ans) {
135	&start_ans_server($name);
136}
137
138# Subroutines
139
140sub read_ns_port {
141	my ( $server ) = @_;
142	my $port = $queryport;
143	my $options = "";
144
145	if ($server) {
146		my $file = $testdir . "/" . $server . "/named.port";
147
148		if (-e $file) {
149			open(my $fh, "<", $file) or die "unable to read ports file \"$file\" ($OS_ERROR)";
150
151			my $line = <$fh>;
152
153			if ($line) {
154				chomp $line;
155				$port = $line;
156			}
157		}
158	}
159	return ($port);
160}
161
162sub check_ns_port {
163	my ( $server ) = @_;
164	my $options = "";
165	my $port = read_ns_port($server);
166
167	if ($server =~ /(\d+)$/) {
168		$options = "-i $1";
169	}
170
171	my $tries = 0;
172
173	while (1) {
174		my $return = system("$PERL $topdir/testsock.pl -p $port $options");
175
176		if ($return == 0) {
177			last;
178		}
179
180		$tries++;
181
182		if ($tries > 4) {
183			print "$0: could not bind to server addresses, still running?\n";
184			print "I:$test:server sockets not available\n";
185			print "I:$test:failed\n";
186
187			system("$PERL $topdir/stop.pl $test"); # Is this the correct behavior?
188
189			exit 1;
190		}
191
192		print "I:$test:Couldn't bind to socket (yet)\n";
193		sleep 2;
194	}
195}
196
197sub start_server {
198	my ( $server, $command, $pid_file ) = @_;
199
200	chdir "$testdir/$server" or die "unable to chdir \"$testdir/$server\" ($OS_ERROR)\n";
201
202	# start the server
203	my $child = `$command`;
204	chomp($child);
205
206	# wait up to 14 seconds for the server to start and to write the
207	# pid file otherwise kill this server and any others that have
208	# already been started
209	my $tries = 0;
210	while (!-s $pid_file) {
211		if (++$tries > 140) {
212			print "I:$test:Couldn't start server $command (pid=$child)\n";
213			print "I:$test:failed\n";
214			kill "ABRT", $child if ("$child" ne "");
215			chdir "$testdir";
216			system "$PERL $topdir/stop.pl $test";
217			exit 1;
218		}
219		sleep 0.1;
220	}
221
222	# go back to the top level directory
223	chdir $topdir;
224}
225
226sub construct_ns_command {
227	my ( $server, $options ) = @_;
228
229	my $command;
230
231	if ($ENV{'USE_VALGRIND'}) {
232		$command = "valgrind -q --gen-suppressions=all --num-callers=48 --fullpath-after= --log-file=named-$server-valgrind-%p.log ";
233
234		if ($ENV{'USE_VALGRIND'} eq 'helgrind') {
235			$command .= "--tool=helgrind ";
236		} else {
237			$command .= "--tool=memcheck --track-origins=yes --leak-check=full ";
238		}
239
240		$command .= "$NAMED -m none -M external ";
241	} else {
242		if ($taskset) {
243			$command = "taskset $taskset $NAMED ";
244		} else {
245			$command = "$NAMED ";
246		}
247	}
248
249	my $args_file = $testdir . "/" . $server . "/" . "named.args";
250
251	if ($options) {
252		$command .= $options;
253	} elsif (-e $args_file) {
254		open(my $fh, "<", $args_file) or die "unable to read args_file \"$args_file\" ($OS_ERROR)\n";
255
256		while(my $line=<$fh>) {
257			next if ($line =~ /^\s*$/); #discard blank lines
258			next if ($line =~ /^\s*#/); #discard comment lines
259
260			chomp $line;
261
262			$line =~ s/#.*$//;
263
264			$command .= $line;
265
266			last;
267		}
268	} else {
269		$command .= "-D $test-$server ";
270		$command .= "-X named.lock ";
271		$command .= "-m record,size,mctx ";
272
273		foreach my $t_option(
274			"dropedns", "ednsformerr", "ednsnotimp", "ednsrefused",
275			"noaa", "noedns", "nosoa", "maxudp512", "maxudp1460",
276		    ) {
277			if (-e "$testdir/$server/named.$t_option") {
278				$command .= "-T $t_option "
279			}
280		}
281
282		$command .= "-c named.conf -d 99 -g -U 4 -T maxcachesize=2097152";
283	}
284
285	if (-e "$testdir/$server/named.notcp") {
286		$command .= " -T notcp"
287	}
288
289	if ($restart) {
290		$command .= " >>named.run 2>&1 &";
291	} else {
292		$command .= " >named.run 2>&1 &";
293	}
294
295	# get the shell to report the pid of the server ($!)
296	$command .= " echo \$!";
297
298	return $command;
299}
300
301sub start_ns_server {
302	my ( $server, $options ) = @_;
303
304	my $cleanup_files;
305	my $command;
306	my $pid_file;
307
308	$cleanup_files = "{./*.jnl,./*.bk,./*.st,./named.run}";
309
310	$command = construct_ns_command($server, $options);
311
312	$pid_file = "named.pid";
313
314	if ($clean) {
315		unlink glob $cleanup_files;
316	}
317
318	start_server($server, $command, $pid_file);
319}
320
321sub construct_ans_command {
322	my ( $server, $options ) = @_;
323
324	my $command;
325	my $n;
326
327	if ($server =~ /^ans(\d+)/) {
328		$n = $1;
329	} else {
330		die "unable to parse server number from name \"$server\"\n";
331	}
332
333	if (-e "$testdir/$server/ans.py") {
334		$command = "$PYTHON -u ans.py 10.53.0.$n $queryport";
335	} elsif (-e "$testdir/$server/ans.pl") {
336		$command = "$PERL ans.pl";
337	} else {
338		$command = "$PERL $topdir/ans.pl 10.53.0.$n";
339	}
340
341	if ($options) {
342		$command .= $options;
343	}
344
345	if ($restart) {
346		$command .= " >>ans.run 2>&1 &";
347	} else {
348		$command .= " >ans.run 2>&1 &";
349	}
350
351	# get the shell to report the pid of the server ($!)
352	$command .= " echo \$!";
353
354	return $command;
355}
356
357sub start_ans_server {
358	my ( $server, $options ) = @_;
359
360	my $cleanup_files;
361	my $command;
362	my $pid_file;
363
364	$cleanup_files = "{./ans.run}";
365	$command = construct_ans_command($server, $options);
366	$pid_file = "ans.pid";
367
368	if ($clean) {
369		unlink glob $cleanup_files;
370	}
371
372	start_server($server, $command, $pid_file);
373}
374
375sub verify_ns_server {
376	my ( $server ) = @_;
377
378	my $tries = 0;
379
380	my $runfile = "$testdir/$server/named.run";
381
382	while (1) {
383		# the shell *ought* to have created the file immediately, but this
384		# logic allows the creation to be delayed without issues
385		if (open(my $fh, "<", $runfile)) {
386			# the two non-whitespace blobs should be the date and time
387			# but we don't care about them really, only that they are there
388			if (grep /^\S+ \S+ running\R/, <$fh>) {
389				last;
390			}
391		}
392
393		$tries++;
394
395		if ($tries >= 30) {
396			print "I:$test:server $server seems to have not started\n";
397			print "I:$test:failed\n";
398
399			system("$PERL $topdir/stop.pl $test");
400
401			exit 1;
402		}
403
404		sleep 2;
405	}
406
407	$tries = 0;
408
409	my $port = read_ns_port($server);
410	my $tcp = "+tcp";
411	my $n;
412
413	if ($server =~ /^ns(\d+)/) {
414		$n = $1;
415	} else {
416		die "unable to parse server number from name \"$server\"\n";
417	}
418
419	if (-e "$testdir/$server/named.notcp") {
420		$tcp = "";
421	}
422
423	while (1) {
424		my $return = system("$DIG $tcp +noadd +nosea +nostat +noquest +nocomm +nocmd +noedns -p $port version.bind. chaos txt \@10.53.0.$n > /dev/null");
425
426		last if ($return == 0);
427
428		$tries++;
429
430		if ($tries >= 30) {
431			print "I:$test:no response from $server\n";
432			print "I:$test:failed\n";
433
434			system("$PERL $topdir/stop.pl $test");
435
436			exit 1;
437		}
438
439		sleep 2;
440	}
441}
442