delete_image.in 12.1 KB
Newer Older
1 2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2000-2017 University of Utah and the Flux Group.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
#
use English;
use strict;
use Getopt::Std;
27
use File::Basename;
28 29 30 31 32 33 34
use Data::Dumper;

#
# Delete an image (descriptor) 
#
sub usage()
{
35
    print("Usage: delete_image [[-f | -F] -p | -r] <imagename>\n".
36
	  "Options:\n".
37
	  "       -d     Enable debug messages\n".
38
	  "       -p     Purge the disk image file(s)\n".
39
	  "       -r     Rename the disk image file(s) instead (default)\n".
40
	  "       -n     Impotent mode, show what would be done.\n".
41
	  "       -f     Force deletion of system image\n".
42
	  "       -V     Delete only the version\n".
43
	  "       -F     Force deletion of global system image\n");
44 45
    exit(-1);
}
46
my $optlist     = "dFprnfV";
47 48
my $debug       = 0;
my $purge       = 0;
49
my $rename      = 1;
50
my $force       = 0;
51
my $FORCE       = 0;
52 53
my $impotent    = 0;
my $needunlock  = 0;
54
my $versonly    = 0;
55 56 57 58

#
# Configure variables
#
59 60 61 62
my $TB             = "@prefix@";
my $PROJROOT       = "@PROJROOT_DIR@";
my $TBOPS          = "@TBOPSEMAIL@";
my $friskiller     = "$TB/sbin/frisbeehelper";
63
my $PGENISUPPORT   = @PROTOGENI_SUPPORT@;
64 65
my $WITHPROVENANCE = @IMAGEPROVENANCE@;
my $doprovenance   = 0;
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

#
# Untaint the path
#
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

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

#
# We don't want to run this script unless its the real version.
#
if ($EUID != 0) {
    die("*** $0:\n".
	"    Must be setuid! Maybe its a development version?\n");
}

#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use EmulabConstants;
91
use EmulabFeatures;
92
use libEmulab;
93 94
use libtestbed;
use User;
95
use OSImage;
96 97 98 99 100 101
if ($PGENISUPPORT) {
    use vars qw($GENI_DBNAME);
    $GENI_DBNAME = "geni-cm";
    require GeniHRN;
    require GeniUser;
}
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

# Protos
sub fatal($);

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
my %options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"d"})) {
    $debug = 1;
}
if (defined($options{"p"})) {
118 119
    $purge  = 1;
    $rename = 0;
120
}
121 122 123 124 125 126
if (defined($options{"r"})) {
    $rename = 1;
}
if (defined($options{"n"})) {
    $impotent = 1;
}
127 128 129
if (defined($options{"V"})) {
    $versonly = 1;
}
130
if (defined($options{"F"})) {
131 132 133
    $FORCE = 1;
}
if (defined($options{"f"})) {
134 135
    $force = 1;
}
136 137
usage()
    if (@ARGV != 1);
138
usage()
139
    if (! ($purge || $rename));
140 141

my $imageid = shift(@ARGV);
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
my $image = OSImage->Lookup($imageid);
if (!defined($image)) {
    fatal("Image does not exist in the DB!");
}

#
# See if we should use libimageops instead.  Eventually it will be
# a feature as well as a few special cases; but certain node types may
# require it (i.e. Docker).
#
my $usenew = 0;
if ($image->format() eq 'docker') {
    $usenew = 1;
}
if ($usenew) {
    use libimageops;

    libimageops::setDebug(1)
	if (1 || $debug);
    my %args = ( 'purge' => $purge,'rename' => $rename,
		 'impotent' => $impotent,'versonly' => $versonly,
		 'force' => $force,'force_global' => $FORCE );

    my $iops = libimageops::Factory("image" => $image);
    my ($rc,$msg) = $iops->DeleteImage($image,\%args);
    if ($rc) {
	print STDERR "Error: $msg\n";
    }
    exit($rc);
}
172 173 174 175 176 177 178 179 180 181 182

#
# Map invoking user to object. 
#
my $this_user = User->ThisUser();
if (! defined($this_user)) {
    fatal("You ($UID) do not exist!");
}
if (!$image->AccessCheck($this_user, TB_IMAGEID_DESTROY())) {
    fatal("You do not have permission to delete this image!");
}
183

184
if ($image->pid() eq TBOPSPID() && $image->global() && !$FORCE) {
185 186 187 188
    fatal("Refusing to delete global system image $image. ".
	  "Use -F if you are sure.\n");
}

189 190 191
#
# Before we do anything destructive, we lock the descriptor.
#
192 193 194 195 196
if (!$impotent) {
    if ($image->Lock()) {
	fatal("Image is locked, please try again later!\n");
    }
    $needunlock = 1;
197
}
198 199 200
$imageid      = $image->imageid();
my $imagename = $image->imagename();
my $imagepid  = $image->pid();
201 202 203 204 205 206
my $imagevers = $image->version();

# Sanity check; cannot delete a deleted version.
if ($versonly && defined($image->deleted())) {
    fatal("Image version is already deleted");
}
207

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
#
# We want to send email to the creator. Also, watch for an image created
# on the Geni path; the creator urn tells us who the creator is, rather
# then who is calling the script. When PROTOGENI_LOCALUSER=0 there is no
# local creator, but when set there is a local shadow user we can use.
#
my $notifyuser = $image->GetCreator();
if (!defined($notifyuser)) {
    $notifyuser = $this_user;
}
if (defined($image->creator_urn())) {
    my $geniuser = GeniUser->Lookup($image->creator_urn(), 1);
    if (defined($geniuser) && $geniuser->IsLocal()) {
	$notifyuser = $geniuser->emulab_user();
    }
    else {
	# This is okay, it is just for email below.
	$notifyuser = $geniuser;
    }
}
if ($debug) {
    print STDERR "Will send email to $notifyuser\n";
}

232 233 234 235 236 237 238 239 240
#
# Need root to delete the image file later.
#
$EUID = $UID;

#
# Be sure to kill off running frisbee. If a node is trying to load that
# image, well tough. 
#
241 242 243 244 245
if (!$impotent) {
    system("$friskiller -k $imageid");
    if ($?) {
	fatal("Could not kill running frisbee for $imageid!");
    }
246 247
}

248 249 250 251
#
# When IMAGEPROVENANCE is on, we never delete system images, we
# rename them. 
#
252
if ($image->pid() eq TBOPSPID() && !$force) {
253 254 255 256 257 258 259 260
    if ($purge) {
	$purge  = 0;
	print STDERR "Ignoring purge option for system image. \n";
    }
    if ($WITHPROVENANCE) {
	print STDERR "Turning on rename option for system image. \n";
	$rename = 1;
    }
261 262
}

263 264 265 266
#
# Since admins will often delete image descriptors for users, we are
# setuid root. Flip for deleting the image file.
#
267
if ($purge || $rename) {
268 269
    my $isdirpath = $image->IsDirPath();
    
270 271
    #
    # When doing image provenance, we have to deal with all versions
272
    # of the image. This will not return deleted versions.
273 274 275 276
    #
    my @images = ();
    if ($image->AllVersions(\@images)) {
	fatal("Could not get list of image (versions)");
277
    }
278 279 280 281 282 283 284 285 286 287 288
    #
    # When deleting just a single version, if this is the last or only
    # version, then turn off version only. Makes no sense to have a
    # descriptor with no non-deleted versions.
    #
    if ($versonly && scalar(@images) == 1) {
	$versonly = 0;
    }
    if ($versonly) {
	@images = ($image);
    }
289 290 291

    #
    # If the path is a directory, we can just do a rename on it.
292
    # But not if deleting just a single image version.
293
    #
294
    if ($isdirpath && !$versonly) {
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
	my $dirname = $image->path();
	
	if ($purge) {
	    if ($impotent) {
		print "Would remove directory $dirname\n" if (-e $dirname);
	    }
	    else {
		$EUID = 0;
		system("/bin/rm -rf $dirname");
		if ($?) {
		    fatal("Could not remove $dirname");
		}
		$EUID = $UID;
	    }
	}
	else {
Leigh Stoller's avatar
Leigh Stoller committed
311
	    my $newname = dirname($dirname) . "/" . basename($dirname) .
312
		"," . $image->imageid();
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327

	    if ($impotent) {
		print "Would rename $dirname to $newname\n" if (-e $dirname);
	    }
	    else {
		if (-e $dirname) {
		    $EUID = 0;
		    system("/bin/mv -fv $dirname $newname");
		    if ($?) {
			fatal("Could not rename $dirname to $newname");
		    }
		    $EUID = $UID;
		}
		# Hmm, need an update all versions method.
		foreach my $imageversion (@images) {
328 329
		    # Need trailing slash!
		    $imageversion->Update({"path" => $newname . "/"});
330 331 332 333 334 335 336 337
		}
	    }
	}
	#
	# Fall into the loop below to clean up stale image versions and
	# backup files.
	#
    }
338 339 340
    foreach my $imageversion (@images) {
	my @todelete = ();
	my @torename = ();
341
	my $filename = $imageversion->FullImageFile();
342 343 344

	push(@torename, $filename);
	push(@todelete, "$filename.bak");
345 346 347 348 349 350 351 352 353 354 355
	push(@todelete, "$filename.tmp");
	push(@torename, $imageversion->FullImageSHA1File());
	push(@torename, $imageversion->FullImageSigFile());

	# Backwards compat with non-directory image paths.
	if ($filename ne $imageversion->DeltaImageFile()) {
	    $filename = $imageversion->DeltaImageFile();
	    push(@torename, $filename);
	    push(@todelete, "$filename.bak");
	    push(@todelete, "$filename.tmp");
	    push(@torename, $imageversion->DeltaImageSHA1File());
356 357 358 359 360 361 362 363 364
	}

	# We throw away versions that never came ready or released.
	if ($purge ||
	    !($imageversion->ready() && $imageversion->released())) {
	    @todelete = (@todelete, @torename);
	    @torename = ();
	}
	# Throw away the slot if it never came ready or released.
Leigh Stoller's avatar
Leigh Stoller committed
365 366 367
	# Only if the highest numbered version, no holes please.
	if ($imageversion->IsNewest() &&
	    !($imageversion->ready() && $imageversion->released())) {
368 369
	    if ($impotent) {
		my $vers = $imageversion->version();
Leigh Stoller's avatar
Leigh Stoller committed
370 371 372 373 374
		print "Would kill version $vers DB state since it was ".
		    "not ready/released\n";
	    }
	    else {
		$imageversion->PurgeVersion();
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
	    }
	}

	$EUID = 0;
	foreach my $file (@todelete) {
	    if (-e $file) {
		if ($impotent) {
		    print "Would delete $file\n";
		    next;
		}
		if (! unlink($file)) {
		    SENDMAIL($TBOPS,
			     "delete_image: Could not remove image file",
			     "Could not remove $file\n".
			     "Someone will need to do this by hand.\n");
		}
391 392
	    }
	}
393 394 395
	$EUID = $UID;
	
	#
396 397 398 399
	# Skip renames for directory based images. 
	# Note that when deleting a single version in an image directory,
	# we do not want to do a rename. That would be confusing if some
	# versions were deleted and then the entire image deleted later.
400 401 402 403
	#
	next
	    if ($isdirpath);
	
404 405 406 407 408
	#
	# Delete with rename; move the current files out of the way
	# so that they do not conflict with a later image of the same name.
	# We do this by creating a subdir for the files.
	#
409
	$EUID = 0;
410 411
	if (@torename) {
	    my $dirname = dirname($imageversion->path()) .
412
		"/" . $image->imagename() . "," . $image->imageid();
413

414 415 416 417 418
	    if (! -e $dirname) {
		if ($impotent) {
		    print "Would mkdir($dirname)\n";
		}
		elsif (! mkdir("$dirname", 0775)) {
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
		    fatal("Could not mkdir $dirname");
		}
	    }
	    foreach my $file (@torename) {
		my $newname = $dirname . "/" . basename($file);
		
		if ($impotent) {
		    print "Would rename $file to $newname\n" if (-e $file);
		    next;
		}
		if (-e $file) {
		    system("/bin/mv -fv $file $newname");
		    if ($?) {
			fatal("Could not rename $file to $dirname");
		    }
		}
		if ($file eq $filename &&
		    $imageversion->Update({"path" => $newname})) {
		    fatal("Could not update path for $imageversion");
		}
	    }
	}
	$EUID = $UID;
442 443
    }
}
444 445 446
exit(0)
    if ($impotent);

447 448 449
#
# If using the image tracker, have to notify the IMS.
#
450 451 452
if (!$versonly) {
    # Do this before delete().
    if (GetSiteVar("protogeni/use_imagetracker")) {
453
	$image->SchedIMSDeletion(1) == 0
454 455 456 457 458
	    or fatal("Could not schedule IMS deletion");
    }
    if ($image->Delete() != 0) {
	fatal("Could not delete image!");
    }
459 460 461
    $notifyuser->SendEmail("delete_image: Image has been deleted",
			   "Image $imagepid,$imagename ($imageid) has ".
			   "been deleted by $this_user\n");
462
}
463
else {
464 465 466 467 468
    # Do this before delete().
    if (GetSiteVar("protogeni/use_imagetracker")) {
	$image->SchedIMSDeletion(0) == 0
	    or fatal("Could not schedule IMS deletion");
    }
469 470 471 472 473
    if ($image->DeleteVersion() != 0) {
	fatal("Could not delete image version!");
    }
    # I know, we are unlocking something we just deleted. Its okay, relax.
    $image->Unlock();
474 475 476
    $notifyuser->SendEmail("delete_image: Image Version has been deleted",
			   "Version $imagevers of image $imagepid,$imagename".
			   "($imageid)\nhas been deleted by $this_user\n");
477 478
}
exit(0);
479

480 481 482 483
sub fatal($)
{
    my ($mesg) = @_;

484 485 486
    $image->Unlock()
	if ($needunlock);

487 488 489
    die("*** $0:\n".
	"    $mesg\n");
}