Import Upstream version 1.8.5
[hcoop/debian/openafs.git] / src / libuafs / afsload / lib / AFS / Load / Config.pm
1 package AFS::Load::Config;
2
3 =head1 NAME
4
5 AFS::Load::Config - afsload configuration file format
6
7 =head1 SYNOPSIS
8
9 nodeconfig
10 node * afsconfig "-fakestat -cachedir /tmp/afsload/cache.$RANK"
11 node 0-15 logfile "/tmp/afsload/log.$RANK"
12 node 16-* logfile /dev/null
13 step
14 node * chdir /afs/.localcell/afsload
15 step
16 node 0 creat foo "foo contents"
17 step name "read newly created file"
18 node * read foo "foo contents"
19 step
20 node 0 unlink foo
21
22 =head1 DESCRIPTION
23
24 The afsload scripts run certain operations on various OpenAFS userspace
25 client nodes, according to a test configuration. The general syntax of
26 this configuration is described here, but the documentation for
27 individual test actions are documented in AFS::Load::Action.
28
29 In general, keywords are composed of any characters besides whitespace
30 and quotes. Keywords are separated by whitespace, except when quoted,
31 and any duplicate whitespace is ignored. No interpolation or
32 preprocessing is done when reading the configuration file itself, though
33 individual actions or directives may perform some kind of interpolation
34 on the given arguments to the directive.
35
36 =head1 RANGES
37
38 Everything in the configuration can be specified to apply to all nodes,
39 some subset of the nodes, or a specific node. This is specified by
40 giving a range of node ranks that the configuration directive applies
41 to. This range can take one of the following forms:
42
43 =over 4
44
45 =item B<number>
46
47 A single number by itself only applies to a node with that rank.
48
49 =item B<number-number>
50
51 Two numbers separated by a hyphen applies to any node that has a rank
52 equal to either of those two numbers, or is between those two numbers.
53
54 =item B<*>
55
56 An asterisk applies to all nodes.
57
58 =item B<number-*>
59
60 A number followed by a hyphen and an asterisk applies to any node whose
61 rank is equal to the specified number or higher. You can think of this
62 as the same as the B<number-number> case, where the asterisk is treated
63 as an infinite number.
64
65 =item <range,range>
66
67 Any combination of the above range specifications can be specified,
68 separated by commas, and it will apply to any node to which any of the
69 supplied ranges apply.
70
71 =back
72
73 For example, a range of 0,4-7,10-* will apply to all nodes that have a
74 rank of 0, 4, 5, 6, 7, 10, and any rank higher than 10.
75
76 =head1 NODECONFIG
77
78 The first directive that should be specified is the 'nodeconfig'
79 directive, which defines the configuration for the various nodes. To
80 specify some configuration for some nodes, specify the 'node' directive,
81 followed by a range of node ranks, followed by the configuration
82 directive and any arguments:
83
84 node <range> <directive> <arguments>
85
86 Right now only two directives can be given:
87
88 =over 4
89
90 =item B<afsconfig>
91
92 This specifies the arguments to give to the userspace client equivalent
93 of afsd. Specify this as a single string; so if you want to use multiple
94 arguments, you must quote the string and separate arguments by spaces.
95
96 The literal string $RANK is replaced with the numeric rank of the node,
97 anywhere the string $RANK appears in the config.
98
99 For example:
100
101 node * afsconfig "-fakestat -cachedir /tmp/afsload/cache.$RANK"
102
103 will make all nodes turn on fakestat, and will use a cache directory in
104 /tmp/afsload/cache.$RANK. Note that the afsload scripts do not interpret
105 the given afsd-like parameters; they are just passed to libuafs. In
106 particular this means that you must create all of the given cache
107 directorie before running afsload, as libuafs/afsd does not create it
108 for you.
109
110 =item B<logfile>
111
112 This specifies where to direct output for this node. The userspace
113 client as well as perl itself may print some information or warnings,
114 etc to stdout or stderr. Since having all nodes print to the same stdout
115 can be unreadable, this allows you to specify a file for each node that
116 you can look at later if necessary.
117
118 The literal string $RANK is replaced with the numeric rank of the node,
119 anywhere the string $RANK appears in the given log file name. If this is
120 unspecified, it defaults to /dev/null, so all output from the node will
121 be lost.
122
123 =back
124
125 =head1 STEP
126
127 After the nodeconfig directives are specified, the rest of the
128 configuration consists of 'step' directives. Each step directive marks a
129 synchronization point between all running nodes; all nodes must complete
130 all previous actions before any node will proceed beyond a step
131 directive.
132
133 Each step is specified by just the directive "step" in the configuration
134 file. Each step may be given a name to make it easier to identify in the
135 test run output. To do this, just specify "step name myname" instead of
136 just "step".
137
138 In each step, you must specify a series of action directives that
139 dictate what each node does during each step. If you don't specify that
140 a node should do anything, that node just waits for the other nodes to
141 complete their actions.
142
143 Each action is specified like so
144
145 node <range> <action> <action arguments>
146
147 Where the action and action arguments are documented in
148 AFS::Load::Action, for all defined actions.
149
150 All actions on different nodes between step directives are performed in
151 parallel, with no guarantee on the ordering in which they occur. If you
152 specify multiple actions for the same node between step directives,
153 those actions occur sequentially in the order they were specified. For
154 example:
155
156 step
157 node 0 creat file1 foo
158 node 0 read file1 foo
159 node 1-* read file2 bar
160
161 In this step, node 0 will create file1 and then read it. While that is
162 ocurring, all other nodes will read file2, which may occur before,
163 after, or during one of the other actions node 0 is performing.
164
165 =head1 AUTHORS
166
167 Andrew Deason E<lt>adeason@sinenomine.netE<gt>, Sine Nomine Associates.
168
169 =head1 COPYRIGHT
170
171 Copyright 2010-2011 Sine Nomine Associates.
172
173 =cut
174
175 use strict;
176 use Text::ParseWords qw(parse_line);
177
178 use AFS::Load::Action;
179
180 my @saw_nodes = ();
181 my $in_nodeconfig = 0;
182
183 sub _range_check($$) {
184 my ($max, $word) = @_;
185 if ($max == 0) {
186 return;
187 }
188 foreach (split /,/, $word) {
189 if (m/^(\d+)-(\d+|[*])$/) {
190 # X-Y range
191 my ($lo, $hi);
192 $lo = int($1);
193
194 if ($2 eq "*") {
195 $hi = $max;
196 } else {
197 $hi = int($2);
198 }
199
200 if ($lo < 0 || $lo > $max || $hi < $lo || $hi > $max) {
201 die("Invalid range $lo-$hi; you can only specify from 0 to $max, ".
202 "and the second range element must be greater than the first");
203 }
204
205 if (not $in_nodeconfig) {
206 for (my $i = $lo; $i <= $hi; $i++) {
207 $saw_nodes[$i] = 1;
208 }
209 }
210 } elsif (m/^(\d+)$/) {
211 # plain number
212 my $n = int($1);
213 if ($n < 0 || $n > $max) {
214 die("Invalid node id $n; you can only specify from 0 to $max\n");
215 }
216 if (not $in_nodeconfig) {
217 $saw_nodes[$n] = 1;
218 }
219 } elsif ($_ eq "*") {
220 if (not $in_nodeconfig) {
221 for (my $i = 0; $i <= $max; $i++) {
222 $saw_nodes[$i] = 1;
223 }
224 }
225 } else {
226 die("unparseable range element $_");
227 }
228 }
229 }
230
231 sub _range_match($$) {
232 my ($rank, $word) = @_;
233
234 if ($rank < 0) {
235 $rank *= -1;
236 $rank--;
237 _range_check($rank, $word);
238 return 1;
239 }
240
241 foreach (split /,/, $word) {
242 if (m/^(\d+)-(\d+|[*])$/) {
243 # X-Y range
244 my ($lo, $hi);
245 $lo = int($1);
246 if ($rank < $lo) {
247 next;
248 }
249 if ($2 eq "*") {
250 return 1;
251 }
252 $hi = int($2);
253 if ($rank <= $hi) {
254 return 1;
255 }
256 } elsif (m/^(\d+)$/) {
257 # plain number
258 if (int($1) == $rank) {
259 return 1;
260 }
261 } elsif ($_ eq "*") {
262 return 1;
263 } else {
264 die("unparseable range element $_");
265 }
266 }
267 return 0;
268 }
269
270 sub _nextword($$) {
271 my ($wordref, $iref) = @_;
272 my $ret = undef;
273 while (!defined($ret) and $$iref < scalar(@$wordref)) {
274 $ret = $$wordref[$$iref];
275 $$iref++;
276 }
277 return $ret;
278 }
279
280 sub check_conf($$) {
281 my ($np, $conf_file) = @_;
282 my $max;
283 my $rank;
284 my @steps;
285 my %nodeconf;
286 my $counter = 0;
287
288 # subtract 2 from the number of processes, since node ids are 0-indexed,
289 # and we need one process for the 'director' node
290 $max = $np - 2;
291
292 $rank = -1 * $max;
293 $rank--;
294
295 load_conf($rank, $conf_file, \@steps, \%nodeconf)
296 or die("Error parsing configuration file\n");
297
298 for (my $i = 0; $i <= $max; $i++) {
299 if (not defined($saw_nodes[$i]) or !$saw_nodes[$i]) {
300 $counter++;
301 if ($counter > 5) {
302 next;
303 }
304 print STDERR "# WARNING: node $i does not appear to have any\n";
305 print STDERR "# actions associated with it\n";
306 }
307 }
308 if ($counter > 5) {
309 print STDERR "# ... along with ".($counter-5)." other nodes\n";
310 }
311 }
312
313 sub load_conf($$$$) {
314 my ($rank, $conf_file, $stepsref, $nodeconfref) = @_;
315 my $conf_h;
316 my $conf;
317
318 open($conf_h, "<$conf_file") or die("Cannot open $conf_file: $!\n");
319 {
320 local $/;
321 $conf = <$conf_h>;
322 }
323 close($conf_h);
324
325 my @words = parse_line(qr/\s+/, 0, $conf);
326 push(@words, "step");
327 my @actwords = ();
328 my @acts = ();
329 my $didRange = 0;
330 my $ignore = 0;
331
332 my $i = 0;
333
334 while ($i < scalar @words) {
335 my $word;
336 $word = _nextword(\@words, \$i);
337 if (not defined($word)) {
338 next;
339 }
340 if ($word eq "nodeconfig") {
341 $in_nodeconfig = 1;
342
343 # keep going until we see a "step"
344 while ($i < scalar @words && $words[$i] ne "step") {
345 my ($key, $val);
346
347 $word = _nextword(\@words, \$i);
348 if ($word ne "node") {
349 die("Expected nodeconfig/node, got nodeconfig/$word");
350 }
351
352 $word = _nextword(\@words, \$i);
353 if (!_range_match($rank, $word)) {
354 # skip this 'node' directive
355 while ($i < scalar @words) {
356 # skip until we see the next 'node'
357 $word = _nextword(\@words, \$i);
358 if ($word eq "node" || $word eq "step") {
359 $i--;
360 last;
361 }
362 }
363 next;
364 }
365
366 $key = _nextword(\@words, \$i);
367 $val = _nextword(\@words, \$i);
368
369 $$nodeconfref{$key} = $val;
370 }
371
372 $in_nodeconfig = 0;
373
374 } elsif ($word eq "step") {
375 my @acts = ();
376 my $nAct = 0;
377 my $name = undef;
378
379 if (!($i < scalar @words)) {
380 last;
381 }
382
383 if (defined($words[$i]) && $words[$i] eq "name") {
384 $word = _nextword(\@words, \$i);
385 $name = _nextword(\@words, \$i);
386 }
387
388 # keep going until we see the next "step"
389 while ($i < scalar @words && $words[$i] ne "step") {
390 $word = _nextword(\@words, \$i);
391
392 if ($word ne "node") {
393 die("Expected step/node, got step/$word");
394 }
395
396 $word = _nextword(\@words, \$i);
397 if (!_range_match($rank, $word)) {
398 $nAct++;
399 while ($i < scalar @words) {
400 # skip until we see the next 'node'
401 $word = _nextword(\@words, \$i);
402 if ($word eq "node" || $word eq "step") {
403 $i--;
404 last;
405 }
406 }
407 next;
408 }
409
410 my @actwords = ();
411
412 while ($i < scalar @words) {
413 $word = _nextword(\@words, \$i);
414 if ($word eq "node" || $word eq "step") {
415 $i--;
416 last;
417 }
418 push(@actwords, $word);
419 }
420
421 my $act = AFS::Load::Action->parse($nAct, @actwords);
422 push(@acts, \$act);
423 $nAct++;
424 }
425 push(@$stepsref, [$name, @acts]);
426 } else {
427 die("Unknown top-level config directive '$word'\n");
428 }
429 }
430
431 foreach my $key (keys %$nodeconfref) {
432 $$nodeconfref{$key} =~ s/\$RANK/$rank/g;
433 }
434
435 return 1;
436 }
437
438 1;