os_load.in 10.1 KB
Newer Older
1
2
#!/usr/bin/perl -wT
use English;
3
use Getopt::Std;
4
5
   
#
6
# XXX boss.emulab.net and users.emulab.net wired in. 
7
#     wd0 wired in. Should come from node_types table in DB
8
9
10
# 

#
11
12
13
# Load an image onto a disk. The image must be in the DB images table,
# which defines how/where to load, and what partitions are affected.
# The nodes and partitions tables are updated appropriately. 
14
# 
15
16
sub usage()
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
17
18
19
20
21
    print STDOUT "Usage: os_load [-s | -w] [-r] [-i <imageid>] ".
	         "<node> [node ...]\n".
	"Use -i to specify an imageid. Use node default otherwise.\n".
	"Use -s to setup reload only, but do not issue a reboot.\n".
	"Use -w to block waiting for nodes to finish reloading.\n".
22
	"    (-s and -w are mutually exclusive)\n".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
23
24
	"Use -l to get a list of images you are permitted to load.\n" .
	"Use -r to use Frisbee support instead of netdisk (experimental).\n";
25
26
    exit(-1);
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
27
my  $optlist = "sldwri:";
28
29
30
31
32

#
# Configure variables
#
my $TB		= "@prefix@";
33
my $TESTMODE    = @TESTMODE@;
34
my $TBOPS       = "@TBOPSEMAIL@";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
35
36
37
my $BOSSADDR	= "@BOSSNODE@";
my $USERADDR	= "@USERNODE@";

38
39
40
41
42

#
# Max number of simultaneous loads. Will be better with Frisbee.
# 
my $MAXLOADS	= 2;
43
44
45
46

#
# Load the Testbed support stuff. 
#
47
48
49
use lib "@prefix@/lib";
use libdb;
use libtestbed;
50

51
my $NETDISKOSID = "NETDISK-STD";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
52
my $FRISBEEPATH = "$BOSSADDR:/tftpboot/pxeboot.frisbee";
53
my $FRISBEELAUNCHER = "$TB/sbin/frisbeelauncher";
54
my $nodereboot	= "$TB/bin/node_reboot";
55
my $schedreload	= "$TB/bin/sched_reload";
56
my $ping	= "/sbin/ping";
57
my $dbg		= 0;
58
my @row;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
59
60
61
my $usedefault  = 1;
my $imageid;
my %imageid_row;
62
my @nodes       = ();
63
my $mereuser    = 0;
64
my $setuponly   = 0;
65
my $waitmode    = 0;
66
my $failures    = 0;
67
my $startwait   = 0;
68
my $frisbee     = 0;
69
70
71
72
73
74
75
76

# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

$| = 1; #Turn off line buffering on output

#
77
78
79
80
81
82
83
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
84
85
86
87
if (defined($options{"l"})) {
    dolisting();
    exit(0);
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
88
if (@ARGV < 1) {
89
90
91
92
93
    usage();
}
if (defined($options{"s"})) {
    $setuponly = 1;
}
94
95
96
if (defined($options{"w"})) {
    $waitmode = 1;
}
97
if (defined($options{"r"})) {
98
99
    $frisbee = 1;
}
100
101
102
if ($waitmode && $setuponly) {
    usage();
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
103
104
105
if (defined($options{"i"})) {
    $imageid    = $options{"i"};
    $usedefault = 0;
106

Leigh B. Stoller's avatar
Leigh B. Stoller committed
107
108
109
110
111
112
    if ($imageid =~ /^([-\@\w.\+]+)$/) {
	$imageid = $1;
    }
    else {
	die("*** Bad $imageid name.\n");
    }
113
}
114

Leigh B. Stoller's avatar
Leigh B. Stoller committed
115
116
117
#
# Untaint nodes.
#
118
119
120
121
122
foreach my $node ( @ARGV ) {
    if ($node =~ /^([-\@\w]+)$/) {
	$node = $1;
    }
    else {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
123
	die("*** Bad node name: $node.\n");
124
125
126
127
    }
    
    push(@nodes, $node);
}
128
129
130
131
132
133

#
# Figure out who called us. Root and admin types can do whatever they
# want. Normal users can only change nodes in experiments in their
# own projects.
#
134
135
if ($UID && !TBAdmin($UID)) {
    $mereuser = 1;
136

Leigh B. Stoller's avatar
Leigh B. Stoller committed
137
    if (! TBNodeAccessCheck($UID, TB_NODEACCESS_LOADIMAGE, @nodes)) {
138
	die("*** You do not have permission to load images on one (or more) ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
139
	    "of the nodes!\n");
140
141
142
143
    }
}

#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
144
# See if allowed to load this image!
145
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
146
if (defined($imageid) && $mereuser &&
147
    ! TBImageIDAccessCheck($UID, $imageid, TB_IMAGEID_READINFO)) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
148
    die("*** You do not have permission to load imageid $imageid!\n");
149
150
151
}

#
152
# Loop for each node.
153
154
# 
foreach my $node (@nodes) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
155
156
157
158
159
160
    #
    # Oh, I suppose it would be nice to do this just once per imageid,
    # but that would be a pain. Not in the mood.
    #
    
    #
161
    # Get default imageid for this node.
Leigh B. Stoller's avatar
Leigh B. Stoller committed
162
    #
163
164
165
166
167
    my $default_imageid;
    if (! ($default_imageid = DefaultImageID($node))) {
	die("*** $0:\n".
	    "    No default imageid is defined for $node!\n");
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
168
    if ($usedefault) {
169
	$imageid = $default_imageid;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
170
    }
171
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
172
173
174
175
    my $db_result =
	DBQueryFatal("select * from images where imageid='$imageid'");
    
    if ($db_result->numrows < 1) {
176
177
	die("*** $0:\n".
	    "    No such imageid $imageid is defined in the DB!\n");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
    }
    %imageid_row = $db_result->fetchhash();
    
    my $loadpart  = $imageid_row{'loadpart'};
    my $loadlen   = $imageid_row{'loadlength'};
    my $imagepath = $imageid_row{'path'};
    my $defosid   = $imageid_row{'default_osid'};

    #
    # 0 means load the entire disk.
    #
    my $diskpart = "";
    if ($loadpart) {
	$diskpart = "wd0:s${loadpart}";
    }
    else {
	$diskpart = "wd0";
    }

    #
    # For now, all testbed default images come from paper and all pid specific
    # images come from plastic:/proj.
    # 
    my $cmdline = "";
    if (defined($imageid_row{'pid'})) {
	if (! ($imagepath =~ /^\/proj\//)) {
204
205
	    die("*** $0:\n".
		"    Your image must reside in /proj\n");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
206
207
208
209
210
211
212
213
	}
	$cmdline = "${USERADDR}:$imagepath $diskpart";    
    }
    else {
	$cmdline = "${BOSSADDR}:$imagepath $diskpart";
    }
    
    print STDOUT "Changing default OS for $node to $defosid\n";
214
215
216
    if (!$TESTMODE) {
      DBQueryFatal("update nodes set ".
		   "def_boot_osid='$defosid',def_boot_path='' ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
217
		   "where node_id='$node'");
218
    }
219

220
    # Put it in the current_reloads table so that nodes can find out which
Leigh B. Stoller's avatar
Leigh B. Stoller committed
221
    # OS to load
222
    DBQueryFatal("replace into current_reloads ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
223
		 "(node_id, image_id) values ('$node', '$imageid')");
224

225
226
227
228
229
230
231
232
233
234
235
236
237
    #
    # If a mereuser is loading an image (which is not the default) then
    # schedule a reload for it so that when the experiment is terminated
    # it will get a fresh default image before getting reallocated to
    # another experiment.
    #
    if ($mereuser && $imageid ne $default_imageid) {
	if (! TBSetSchedReload($node, $default_imageid)) {
	    print "*** $0:\n".
		  "    WARNING: Could not schedule default reload for $node!";
	}
    }

238
    #
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
    # Assign partition table entries for each partition in the image.
    # This is complicated by the fact that an image that covers only
    # part of the slices, should only change the partition table entries
    # for the subset of slices that are written to disk. In reality, this
    # is silly since there is no telling what the disk is going to end
    # up looking like after a partial image is written, especially if its
    # a user generated image. Not sure how to handle this yet. For now
    # lets just say that a user defined images essentially wipe the disk
    # except for the stuff they write. 
    #
    for ($i = 1; $i <= 4; $i++) {
	my $partname = "part${i}_osid";
	
	if (defined($imageid_row{$partname})) {
	    my $osid = $imageid_row{$partname};

255
256
	    DBQueryFatal("replace into partitions ".
			 "(partition, osid, node_id) ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
257
			 "values('$i', '$osid', '$node')");
258
259
	}
	else {
260
	    DBQueryFatal("delete from partitions ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
261
			 "where node_id='$node' and partition='$i'");
262
263
	}
    }
264
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
265
    print STDOUT "Setting up reload for $node\n";
266
    if (!$TESTMODE) {
267
268
269
         if ($frisbee) {
	     DBQueryFatal("update nodes set ".
	                  "next_pxe_boot_path='$FRISBEEPATH'" .
Leigh B. Stoller's avatar
Leigh B. Stoller committed
270
271
272
	 		  "where node_id='$node'");
	     system "$FRISBEELAUNCHER $imageid" and
		 die "*** Unable to launch frisbee daemon\n";
273
274
275
276
	 } else {
	     DBQueryFatal("update nodes set ".
	                  "next_boot_osid='$NETDISKOSID',".
	 		  "next_boot_cmd_line='$cmdline' ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
277
	 		  "where node_id='$node'");
278
	}
279
    }
280
}
281

282
#
283
# Exit if not doing about actual reload.
284
#
285
286
287
288
289
290
291
292
293
if ($setuponly || $TESTMODE) {
    print STDOUT "OS Reload (Setup/Testmode) Done!\n";
    exit 0;
}

#
# Fire off a mass reboot if not in waitmode.
#
if (! $waitmode) {
294
295
    system("$nodereboot @nodes");
    $failures = $? >> 8;
296
297
298

    print STDOUT "OS Reload (no waiting) Done!\n";
    exit $failures;
299
300
}

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
#
# Okay, in waitmode we do a couple at a time and wait for them to come
# back alive before proceeding to the next one.
#
while (@nodes) {
    my @batch = ();
    my $i;

    for ($i = 0; $i < $MAXLOADS && @nodes > 0; $i++) {
	push(@batch, shift(@nodes));
    }
    
    print "Issuing reload/reboot for @batch and then waiting ...\n";

    system("$nodereboot @batch");
    $failures = $? >> 8;
    if ($failures) {
	print "Reboot failed for (some of) @batch. Quitting!\n";
	exit $failures;
    }

    #
    # Now wait for them.
    #
    $startwait = time;
    foreach my $node (@batch) {
	if (WaitTillReloadDone($node) == 0) {
	    print "$node appears have reloaded okay.\n";
	    next;
	}

	print "$node may be down.\n".
	      "Please contact $TBOPS for assistance.\n";
    }
}
336

337
338
339
340
341
342
343
344
345
346
347
348
#
# Print a listing of imageids.
#
sub dolisting()
{
    my($query_result);
    
    if ($UID && !TBAdmin($UID)) {
	my ($me) = getpwuid($UID);
	
	$query_result =
	    DBQueryFatal("select distinct i.* from images as i ".
Leigh B. Stoller's avatar
Leigh B. Stoller committed
349
350
			 "left join group_membership as p on ".
			 " i.pid IS NULL or (p.pid=i.pid and p.gid=p.pid) ".
351
			 "where p.uid='$me' order by i.pid,i.imageid");
352
353
354
355
356
357
358
359
360
361
362
363
364
365
    }
    else {
	$query_result =
	    DBQueryFatal("SELECT * FROM images order by imageid");
    }
    
    for ($i = 0; $i < $query_result->numrows; $i++) {
	my %row  = $query_result->fetchhash();
	my $id   = $row{'imageid'};
	my $desc = $row{'description'};

	printf "%-20s %s\n", $id, $desc;
    }
}
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390

#
# Wait for a reload to finish. We do this in a rather kludgey manner,
# by waiting for bootinfo to clear the DB state (next_boot_osid).
# 
sub WaitTillReloadDone {
    my ($pc) = @_;

    print STDERR "Waiting for $pc to finish reloading\n" if $dbg;
    
    #
    # Seven minutes seems like a long time to wait, but it ain't!
    # 
    my $maxwait = (60 * 7);

    #
    # Start a counter going, relative to the time we rebooted the first
    # node. 
    # 
    my $waittime  = 0;
    my $minutes   = 0;

    while (1) {
	my ($query_result, @row);

391
392
393
394
395
396
397
398
399
	if ($frisbee) {
	    $query_result =
		DBQueryFatal("SELECT next_pxe_boot_path FROM nodes ".
			     "where node_id='$pc'");
	} else {
	    $query_result =
		DBQueryFatal("SELECT next_boot_osid FROM nodes ".
			     "where node_id='$pc'");
	}
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420

        @row = $query_result->fetchrow_array();
	if (! $row[0]) {
	    print STDERR "$pc alive and well\n" if $dbg;
	    return 0;
	}
	
	$waittime = time - $startwait;
	if ($waittime > $maxwait) {
	    print "$pc appears unresponsive; its been ",
	    (int ($waittime / 60))," minutes since reload started.\n";
	    return 1;
	}
	if (int($waittime / 60) > $minutes) {
	    $minutes = int($waittime / 60);
	    print "Still waiting for $pc - its been $minutes minute(s)\n";
	}
	sleep(5);
    }
}