libdb.pm.in 13.1 KB
Newer Older
1
2
#!/usr/bin/perl -w

3
#
4
5
# A library of useful DB stuff. Mostly things that get done a lot.
# Saves typing.
6
7
8
9
10
#
# XXX: The notion of "uid" is a tad confused. A unix uid is a number,
#      while in the DB a user uid is a string (equiv to unix login).
#      Needs to be cleaned up.
#
11

12
13
14
15
package libdb;
use Exporter;
@ISA = "Exporter";
@EXPORT =
16
17
18
    qw ( NODERELOADING_PID NODERELOADING_EID NODEDEAD_PID NODEDEAD_EID
	 NODEBOOTSTATUS_OKAY NODEBOOTSTATUS_FAILED NODEBOOTSTATUS_UNKNOWN
	 NODESTARTSTATUS_NOSTATUS PROJMEMBERTRUST_NONE PROJMEMBERTRUST_USER
19
	 PROJMEMBERTRUST_ROOT DBLIMIT_NSFILESIZE NODERELOADPENDING_EID
20

21
22
23
24
	 EXPTSTATE_NEW EXPTSTATE_PRERUN EXPTSTATE_SWAPPED EXPTSTATE_SWAPPING
	 EXPTSTATE_ACTIVATING EXPTSTATE_ACTIVE EXPTSTATE_TESTING
	 EXPTSTATE_TERMINATING EXPTSTATE_TERMINATED

25
26
	 TBAdmin NodeAccessCheck ProjMember ExpLeader MarkNodeDown
	 SetNodeBootStatus OSFeatureSupported IsShelved NodeidToExp
27
28
	 UserDBInfo DBQuery DBQueryFatal DBQueryWarn DBWarn DBFatal
	 DBQuoteSpecial UNIX2DBUID ExpState SetExpState
29
30

	 );
31

32
33
34
35
# Must come after package declaration!
use English;
require Mysql;

36
37
38
# Configure variables
my $TB		= "@prefix@";
my $DBNAME	= "@TBDBNAME@";
39
my $TBOPS       = "@TBOPSEMAIL@";
40
41

#
42
43
# Set up for querying the database. Note that fork causes a reconnect
# to the DB in the child. 
44
45
46
# 
my $DB = Mysql->connect("localhost", $DBNAME, "script", "none");

47
48
49
50
51
#
# Record last DB error string.
#
my $DBErrorString = "";

52
53
54
55
56
#
# Define exported "constants". Basically, these are just perl subroutines
# that look like constants cause you do not need to call a perl subroutine
# with parens. That is, FOO and FOO() are the same thing.
#
57
sub NODERELOADING_PID()		{ "emulab-ops"; }
58
sub NODERELOADING_EID()		{ "reloading"; }
59
sub NODERELOADPENDING_EID()	{ "reloadpending"; }
60
61
62
63
64
65
66
67
sub NODEDEAD_PID()		{ "emulab-ops"; }
sub NODEDEAD_EID()		{ "hwdown"; }

sub NODEBOOTSTATUS_OKAY()	{ "okay" ; }
sub NODEBOOTSTATUS_FAILED()	{ "failed"; }
sub NODEBOOTSTATUS_UNKNOWN()	{ "unknown"; }
sub NODESTARTSTATUS_NOSTATUS()	{ "none"; }

68
69
70
71
72
73
74
75
76
77
sub EXPTSTATE_NEW()		{ "new"; }
sub EXPTSTATE_PRERUN()		{ "prerunning"; }
sub EXPTSTATE_SWAPPED()		{ "swapped"; }
sub EXPTSTATE_SWAPPING()	{ "swapping"; }
sub EXPTSTATE_ACTIVATING()	{ "activating"; }
sub EXPTSTATE_ACTIVE()		{ "active"; }
sub EXPTSTATE_TESTING()		{ "testing"; }
sub EXPTSTATE_TERMINATING()	{ "terminating"; }
sub EXPTSTATE_TERMINATED()	{ "ended"; }

78
79
80
81
82
83
#
# We want valid project membership to be non-zero for easy membership
# testing. Specific trust levels are encoded thusly.
# 
sub PROJMEMBERTRUST_NONE()	{ 0; }
sub PROJMEMBERTRUST_USER()	{ 1; }
84
sub PROJMEMBERTRUST_ROOT()	{ 2; }
85

86
87
88
89
90
#
# We should list all of the DB limits.
#
sub DBLIMIT_NSFILESIZE()	{ (1024 * 16); }

91
#
92
93
# Test admin status. Optional argument is the UID or Name to test. If not
# provided, then test the current UID.
94
#
95
96
97
# XXX Argument is *either* a numeric UID, or a string name.
#
# usage: TBAdmin([int or char* uid]);
98
99
100
101
102
103
#        returns 1 if an admin type.
#        returns 0 if a mere user.
# 
sub TBAdmin(;$)
{
    my($uid) = @_;
104
    my($name);
105
106
107
108
109

    if (!defined($uid)) {
	$uid = $UID;
    }

110
111
112
113
114
115
116
117
118
119
    #
    # Test if numeric. Map to name if it is.
    # 
    if ($uid =~ /^[0-9]+$/) {
	($name) = getpwuid($uid)
	    or die "$uid not in passwd file\n";
    }
    else {
	$name = $uid;
    }
120
121

    my $query_result =
122
	DBQueryFatal("select admin from users where uid='$name'");
123
124
125
126
127
128
129
130
131

    my @row = $query_result->fetchrow_array();
    if ($row[0] == 1) {
	return 1;
    }
    return 0;
}

#
132
133
134
# Check access permission to a list of nodes. First argument is a *reference*
# to a single node, or a list of nodes. Second argument is optional uid,
# defaults to the current uid.
135
136
137
#
# usage: NodeAccessCheck(array or scalar \@nodelist, [int uid])
#        returns 1 if the uid is allowed to muck with all the nodes.
138
139
#        returns 0 if the uid is not allowed to muck with at least one of the
#                  nodes.
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#
sub NodeAccessCheck($;$)
{
    my($list, $uid) = @_;
    my(@nodelist);

    if (!defined($uid)) {
	$uid = $UID;
    }

    if (ref($list) eq "ARRAY") {
	@nodelist = @$list;
    }
    elsif (ref($list) eq "SCALAR") {
	@nodelist = ($$list);
    }

    if (!defined(@nodelist) ||
	scalar(@nodelist) == 0) {
	die("NodeAccessCheck:\n".
	    "  First parameter should be a reference to a node (scalar), ".
	    "or a list of nodes!\n");
    }

    #
    # Admin types can do anything to any node. So can Root.
    #
    if ($uid == 0 || TBAdmin($uid)) {
	return 1;
    }

    my ($name) = getpwuid($uid)
	or die "$uid not in passwd file\n";

    #
    # Check to make sure that mere user is allowed to muck with nodes.
    #
    foreach my $node (@$nodelist) {
	my $query_result =
179
180
181
182
183
	    DBQueryFatal("select reserved.node_id from reserved ".
			 "left join proj_memb on ".
			 "reserved.pid=proj_memb.pid and ".
			 "reserved.node_id='$node' ".
			 "where proj_memb.uid='$name'");
184

185
	if ($query_result->numrows == 0) {
186
187
188
189
190
191
192
193
	    return 0;
	}
    }
    return 1;
}

#
# Check project membership. First argument is the project to check.
194
# Second argument is optional DB uid, defaults to the current uid.
195
# The return argument encodes the trust membership for members. 
196
#
197
# usage: ProjMember(char *pid, [char *dbuid])
198
199
200
#        returns PROJMEMBERTRUST_NONE if uid is not a member or trust=none.
#        returns PROJMEMBERTRUST_USER if uid is a mere user in pid.
#        returns PROJMEMBERTRUST_ROOT if uid is a root user in pid.
201
202
203
# 
sub ProjMember($;$)
{
204
    my($pid, $dbuid) = @_;
205

206
207
    if (!defined($dbuid) && !UNIX2DBUID($UID, \$dbuid)) {
	die("$UID is not in the Emulab DB.\n");
208
209
210
    }

    my $query_result =
211
	DBQueryFatal("select trust from proj_memb where ".
212
		     "uid='$dbuid' and pid='$pid'");
213

214
    if ($query_result->numrows == 0) {
215
	return PROJMEMBERTRUST_NONE;
216
    }
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231

    my @row = $query_result->fetchrow_array();
    if ($row[0] eq "none") {
	return PROJMEMBERTRUST_NONE;
    }
    if ($row[0] eq "user") {
	return PROJMEMBERTRUST_USER;
    }
    if ($row[0] eq "group_root" || $row[0] eq "local_root") {
	return PROJMEMBERTRUST_ROOT;
    }
    #
    # Should never happen.
    #
    DBFatal("Improper response in ProjMember()");
232
233
}

234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#
# Return Experiment leader. First argument pid. Second argument is eid.
#
# usage: ExpLeader(char *pid, char *eid)
#        returns char *leader if a valid pid/eid.
#        returns 0 if an invalid pid/eid.
# 
sub ExpLeader($$)
{
    my($pid, $eid) = @_;

    my $query_result =
	DBQueryFatal("select expt_head_uid from experiments ".
		     "where eid='$eid' and pid='$pid'");

    if ($query_result->numrows == 0) {
	return 0;
    }

    my @row = $query_result->fetchrow_array();
    return $row[0];
}

257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#
# Return Experiment state.
#
# usage: ExpState(char *pid, char *eid)
#        returns state if a valid pid/eid.
#        returns 0 if an invalid pid/eid or if an error.
# 
sub ExpState($$)
{
    my($pid, $eid) = @_;

    my $query_result =
	DBQueryWarn("select state from experiments ".
		    "where eid='$eid' and pid='$pid'");

    if (! $query_result ||
	$query_result->numrows == 0) {
	return 0;
    }

    my @row = $query_result->fetchrow_array();
    return $row[0];
}

#
# Set Experiment state.
#
# usage: SetExpState(char *pid, char *eid, char *state)
#        returns 1 if okay.
#        returns 0 if an invalid pid/eid or if an error.
# 
sub SetExpState($$$)
{
    my($pid, $eid, $state) = @_;

    my $query_result =
	DBQueryWarn("update experiments set state='$state' ".
		    "where eid='$eid' and pid='$pid'");

    if (! $query_result ||
	$query_result->numrows == 0) {
	return 0;
    }
    return 1;
}

303
304
305
306
307
308
309
310
#
# Mark a node as down, moving it to special pid/eid. First argument is nodeid.
#
# usage: MarkNodeDown(char *nodeid)
#
sub MarkNodeDown($)
{
    my($node) = $_[0];
311
312
    my($pid, $eid);

313
314
    $pid = NODEDEAD_PID;
    $eid = NODEDEAD_EID;
315
316

    my $query_result =
317
	DBQueryFatal("update reserved set pid='$pid', eid='$eid' ".
318
319
320
321
322
323
324
		     "where node_id='$node'");

    if ($query_result->num_rows < 1) {
	DBWarn("WARNING: Could not mark $node down");
    }
}

325
326
327
328
329
330
331
332
333
334
335
336
337
#
# Set the boot status for a node.
#
# usage: SetNodeBootStatus(char *status)
#
sub SetNodeBootStatus($$)
{
    my($node, $bstat) = @_;

    DBQueryFatal("update nodes set bootstatus='$bstat' ".
		 "where node_id='$node'");
}

338
339
340
341
342
343
#
# Check if a particular feature is supported by an OSID. 
#
# usage: OSFeatureSupported(char *osid, char *feature)
#        returns 1 if supported, 0 if not.
#
344
sub OSFeatureSupported($$) {
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
    my($osid, $feature) = @_;

    my $query_result =
	DBQueryFatal("select osfeatures from os_info where osid='$osid'");

    # Invalid OSID?
    if ($query_result->numrows < 1) {
	return 0;
    }
    
    foreach my $osfeature (split(',', $query_result->fetchrow_array())) {
	if ($feature eq $osfeature) {
	    return 1;
	}
    }
    return 0;
}

363
364
365
366
367
368
#
# Ah, what a hack! I'm tired of seeing regexs for sharks scattered around
# the code. Anyway, this checks to see if a node is a shelf, and fills
# in the shelf/node, return 1 if it is. The shelf/node arguments are
# optional, if all you want to do is see if its a shelf type thing.
#
369
# usage: IsShelved(char *nodeid, [\$shelf], [\$node])
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#        returns 1 if the node is a shelf type thing. Optionally fills in info.
#        returns 0 if the node is just a normal kind of node.
#
sub IsShelved ($;$$) {
    my($nodeid, $shelf, $node) = @_;

    if ($nodeid =~ /sh(\d+)-(\d+)/) {
	if (defined($shelf)) {
	    $$shelf = $1;
	}
	if (defined($node)) {
	    $$node = $2;
	}
	return 1;
    }
    return 0;
}

388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
#
# Map nodeid to its pid/eid.
#
# usage: NodeToExp(char *nodeid, \$pid, \$eid)
#        returns 1 if the node is reserved.
#        returns 0 if the node is not reserved.
#
sub NodeidToExp ($$$) {
    my($nodeid, $pid, $eid) = @_;

    my $query_result =
	DBQueryFatal("select pid,eid from reserved where node_id='$nodeid'");

    if ($query_result->num_rows < 1) {
	return 0;
    }
    
    my @row = $query_result->fetchrow_array();
    $$pid = $row[0];
    $$eid = $row[1];
    return 1;
}

#
412
# Map login (db uid) to a user_name and user_email.
413
#
414
# usage: UserDBInfo(char *dbuid, \$name, \$email)
415
416
417
#        returns 1 if the UID is okay.
#        returns 0 if the UID is bogus.
#
418
419
sub UserDBInfo ($$$) {
    my($dbuid, $username, $useremail) = @_;
420

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
    my $query_result =
	DBQueryFatal("select usr_name,usr_email from users ".
		     "where uid='$dbuid'");

    if ($query_result->num_rows < 1) {
	return 0;
    }

    my @row = $query_result->fetchrow_array();
    $$username  = $row[0];
    $$useremail = $row[1];
    return 1;
}

#
# Map UID to DB UID (login). Does a DB check to make sure user is known to
# the DB (user obviously has a regular account), and that account will
# always match what the DB says. Redundant, I know. But consider it a
# sanity (or consistency) check. 
#
# usage: UNIX2DBUID(int uid, \$login)
#        returns 1 if the UID is okay.
#        returns 0 if the UID is bogus.
#
sub UNIX2DBUID ($$) {
    my($unix_uid, $userlogin) = @_;
447
448

    my $query_result =
449
	DBQueryFatal("select uid from users where unix_uid='$unix_uid'");
450
451
452
453
454
455
456
457
458
459

    if ($query_result->num_rows < 1) {
	return 0;
    }

    my @row = $query_result->fetchrow_array();
    $$userlogin = $row[0];
    return 1;
}

460
461
462
#
# Issue a DB query. Argument is a string. Returns the actual query object, so
# it is up to the caller to test it. I would not for one moment view this
463
464
# as encapsulation of the DB interface. I'm just tired of typing the same
# silly stuff over and over. 
465
466
467
468
# 
# usage: DBQuery(char *str)
#        returns the query object result.
#
469
470
471
472
# Sets $DBErrorString is case of error; saving the original query string and
# the error string from the DB module. Use DBFatal (below) to print/email
# that string, and then exit.
#
473
474
475
476
477
478
479
480
sub DBQuery($)
{
    my($query) = $_[0];
    my($result);

    $result = $DB->query($query);

    if (! $result) {
481
482
	$DBErrorString =
	    "  Query: $query\n".
483
	    "  Error: " . $DB->errstr;
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
    }
    return $result;
}

#
# Same as above, but die on error. 
# 
sub DBQueryFatal($)
{
    my($query) = $_[0];
    my($result);

    $result = DBQuery($query);

    if (! $result) {
499
	DBFatal("DB Query failed");
500
501
502
503
    }
    return $result;
}

504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
#
# Same as above, but just send email on error. This info is useful
# to the TB system, but the caller has to retain control.
# 
sub DBQueryWarn($)
{
    my($query) = $_[0];
    my($result);

    $result = DBQuery($query);

    if (! $result) {
	DBWarn("DB Query failed");
    }
    return $result;
}

521
#
522
# Warn and send email after a failed DB query. First argument is the error
523
524
# message to display. The contents of $DBErrorString is also printed.
# 
525
# usage: DBWarn(char *message)
526
#
527
sub DBWarn($)
528
529
{
    my($message) = $_[0];
530
    my($text, $progname);
531

532
    #
533
    # Must taint check $PROGRAM_NAME cause it comes from outside. Silly!
534
535
536
537
538
539
540
541
    #
    if ($PROGRAM_NAME =~ /^([-\w.\/]+)$/) {
	$progname = $1;
    }
    else {
	$progname = "Tainted";
    }

542
543
    $text = "$message - In $progname\n" .
  	    "$DBErrorString\n";
544

545
    print STDERR "*** $text\n";
546
547

    system("echo \"$text\" | /usr/bin/mail ".
548
	   "-s 'TESTBED: DBError - $message' \"$TBOPS\"");
549
550
551
552
553
}

#
# Same as above, but die after the warning.
# 
554
# usage: DBFatal(char *message);
555
556
557
558
559
560
#
sub DBFatal($)
{
    my($message) = $_[0];

    DBWarn($message);
561

562
    exit(1);
563
564
}

565
566
567
568
569
570
571
572
573
574
575
576
577
578
#
# Quote a string for DB insertion.
#
# usage: DBQuoteSpecial(char *string);
#
sub DBQuoteSpecial($)
{
    my($string) = $_[0];

    $string = $DB->quote($string);

    return $string;
}

579
580
1;