2 # Copyright (c) 2008-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 # This is a tool to examine a device and generate a reasonable tapetype
24 use lib '@amperldir@';
31 use Amanda::BigIntCompat;
33 use Amanda::Device qw( :constants );
34 use Amanda::Debug qw( :logging );
35 use Amanda::Util qw( :constants );
36 use Amanda::Config qw( :init :getconf config_dir_relative );
39 use Amanda::Constants;
42 # command-line options
43 my $opt_only_compression = 0;
45 my $opt_tapetype_name = 'unknown-tapetype';
47 my $opt_label = "amtapetype-".(int rand 2**31);
52 # global "hint" from the compression heuristic as to how fast this
54 my $device_speed_estimate;
56 # open up a device, optionally check its label on the first invocation,
57 # and start it in ACCESS_WRITE.
58 my $_label_checked = 0;
60 my $device = Amanda::Device->new($opt_device_name);
61 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
62 die("Could not open device $opt_device_name: ".$device->error()."\n");
65 if (!$device->configure(0)) {
66 die("Errors configuring $opt_device_name: " . $device->error_or_status());
69 if (defined $opt_blocksize) {
70 $device->property_set('BLOCK_SIZE', $opt_blocksize)
71 or die "Error setting blocksize: " . $device->error_or_status();
74 if (!$opt_force and !$_label_checked) {
75 my $read_label_status = $device->read_label();
76 if ($read_label_status & $DEVICE_STATUS_VOLUME_UNLABELED) {
78 } elsif ($read_label_status != $DEVICE_STATUS_SUCCESS) {
79 die "Error reading label: " . $device->error_or_status();
80 } elsif ($device->volume_label) {
81 die "Volume in device $opt_device_name has Amanda label '" .
82 $device->volume_label . "'. Giving up.";
87 my $start_time = time;
91 last if ($device->start($ACCESS_WRITE, $opt_label, undef));
92 if (!($device->status & $DEVICE_STATUS_DEVICE_BUSY)) {
93 die("Error writing label '$opt_label': ". $device->error_or_status());
97 print STDERR "Device is busy. Amtapetype will retry forever; hit ctrl-C to quit.\n";
102 $backoff = 120 if $backoff > 120;
107 my $elapsed = time - $start_time;
108 print STDERR "Drive was busy for $elapsed seconds.\n";
109 print STDERR "If this device is used in a changer, you may want to set timeouts appropriately.\n";
115 # Write a single file to the device, and record the results in STATS.
117 # STATS => $stats_hashref, (see below)
118 # DEVICE => $dev, (device to write to)
119 # PATTERN => RANDOM or FIXED, (data pattern to write)
120 # BYTES => nn, (number of bytes; optional)
121 # MAX_TIME => secs); (cancel write after this time; optional)
123 # Returns 0 on success (including EOM), "TIMEOUT" on timeout, or an error message
126 # STATS is a multi-level hashref; write_one_file adds to any values
127 # already in the data structure.
128 # $stats->{$pattern}->{TIME} - number of seconds spent writing
129 # $stats->{$pattern}->{FILES} - number of files written
130 # $stats->{$pattern}->{BYTES} - number of bytes written (approximate)
132 sub write_one_file(%) {
134 my $stats = $options{'STATS'} || { };
135 my $device = $options{'DEVICE'};
136 my $bytes = $options{'MAX_BYTES'} || 0;
137 my $pattern = $options{'PATTERN'} || 'FIXED';
138 my $max_time = $options{'MAX_TIME'} || 0;
140 # get the block size now, while the device is still working
141 my $block_size = $device->property_get("block_size");
144 my $hdr = Amanda::Header->new();
145 $hdr->{type} = $Amanda::Header::F_DUMPFILE;
146 $hdr->{name} = "amtapetype";
147 $hdr->{disk} = "/test";
148 $hdr->{datestamp} = "X";
149 $hdr->{program} = "AMTAPETYPE";
150 $device->start_file($hdr)
151 or return $device->error_or_status();
153 # set up the transfer
154 my ($source, $dest, $xfer);
155 if ($pattern eq 'FIXED') {
156 # a simple 256-byte pattern to dodge run length encoding.
157 my $non_random_pattern = pack("C*", 0..255);
158 $source = Amanda::Xfer::Source::Pattern->new($bytes, $non_random_pattern);
159 } elsif ($pattern eq 'RANDOM') {
160 $source = Amanda::Xfer::Source::Random->new($bytes, 1 + int rand 100);
162 die "Unknown PATTERN $pattern";
164 $dest = Amanda::Xfer::Dest::Device->new($device, 1);
165 $xfer = Amanda::Xfer->new([$source, $dest]);
167 # set up the relevant callbacks
168 my ($timeout_src, $spinner_src);
173 $timeout_src = Amanda::MainLoop::timeout_source($max_time * 1000);
174 $timeout_src->set_callback(sub {
177 $xfer->cancel(); # will result in an XFER_DONE
181 $spinner_src = Amanda::MainLoop::timeout_source(1000);
182 $spinner_src->set_callback(sub {
184 my ($file, $block) = ($device->file(), $device->block());
185 print STDERR "File $file, block $block \r";
188 my $start_time = time();
191 my ($src, $xmsg, $xfer) = @_;
192 if ($xmsg->{type} == $Amanda::Xfer::XMSG_ERROR) {
193 $got_error = $xmsg->{message};
194 } elsif ($xmsg->{'type'} == $Amanda::Xfer::XMSG_DONE) {
195 Amanda::MainLoop::quit();
199 Amanda::MainLoop::run();
200 $spinner_src->remove();
201 $timeout_src->remove() if ($timeout_src);
202 print STDERR " " x 60, "\r";
204 my $duration = time() - $start_time;
206 # OK, we finished, update statistics (even if we saw an error)
207 my $blocks_written = $device->block();
208 $stats->{$pattern}->{BYTES} += $blocks_written * $block_size;
209 $stats->{$pattern}->{FILES} += 1;
210 $stats->{$pattern}->{TIME} += $duration;
212 # make sure the time is nonzero
213 if ($stats->{$pattern}->{TIME} == 0) {
214 $stats->{$pattern}->{TIME}++;
217 if ($device->status() != $Amanda::Device::DEVICE_STATUS_SUCCESS) {
218 return $device->error_or_status();
221 if ($got_error && $got_error =~ /LEOM detected/) {
236 sub check_compression {
237 my $device = open_device();
239 # Check compression status here by property query. If the device can answer
240 # the question, there's no reason to investigate further.
241 my $compression_enabled = $device->property_get("compression");
243 if (defined $compression_enabled) {
244 return $compression_enabled;
247 # Need to use heuristic to find out if compression is enabled. Also, we
248 # rewind between passes so that the second pass doesn't get some kind of
249 # buffering advantage.
251 print STDERR "Applying heuristic check for compression.\n";
253 # We base our determination on whether it's faster to write random data or
254 # patterned data. That starts by writing random data for a short length of
255 # time, then measuring the elapsed time and total data written. Due to
256 # potential delay in cancelling a transfer, the elapsed time will be a bit
257 # longer than the intended time. We then write the same amount of
258 # patterned data, and again measure the elapsed time. We can then
259 # calculate the speeds of the two operations. If the compressible speed
260 # was faster by more than min_ratio, then we assume compression is enabled.
262 my $compression_check_time = 60;
263 my $compression_check_min_ratio = 1.2;
267 my $err = write_one_file(
270 MAX_TIME => $compression_check_time,
271 PATTERN => 'RANDOM');
273 if ($err != 'TIMEOUT') {
278 # speed calculations are a little tricky: BigInt * float comes out to NaN, so we
279 # cast the BigInts to float first
280 my $random_speed = ($stats->{RANDOM}->{BYTES} . "") / $stats->{RANDOM}->{TIME};
281 print STDERR "Wrote random (uncompressible) data at $random_speed bytes/sec\n";
283 # sock this away for make_tapetype's use
284 $device_speed_estimate = $random_speed;
286 # restart the device to clear any errors and rewind it
287 $device = open_device();
289 $err = write_one_file(
292 MAX_BYTES => $stats->{'RANDOM'}->{'BYTES'},
299 my $fixed_speed = ($stats->{FIXED}->{BYTES} . "") / $stats->{FIXED}->{TIME};
300 print STDERR "Wrote fixed (compressible) data at $fixed_speed bytes/sec\n";
302 $compression_enabled =
303 ($fixed_speed / $random_speed > $compression_check_min_ratio);
304 return $compression_enabled;
311 my $xfer = Amanda::Xfer->new([
312 Amanda::Xfer::Source::Device->new($device),
313 Amanda::Xfer::Dest::Null->new(0),
317 my ($src, $xmsg, $xfer) = @_;
318 if ($xmsg->{type} == $Amanda::Xfer::XMSG_ERROR) {
319 $got_error = $xmsg->{message};
320 } elsif ($xmsg->{'type'} == $Amanda::Xfer::XMSG_DONE) {
321 Amanda::MainLoop::quit();
325 Amanda::MainLoop::run();
329 my $device = open_device();
331 print STDERR "Checking for FSF_AFTER_FILEMARK requirement\n";
332 my $fsf_after_filemark = $device->property_get("FSF_AFTER_FILEMARK");
334 # not a 'tape:' device
335 return if !defined $fsf_after_filemark;
337 $device->start($ACCESS_WRITE, "TEST-001", "20080706050403");
339 my $hdr = new Amanda::Header;
341 $hdr->{type} = $Amanda::Header::F_DUMPFILE;
342 $hdr->{name} = "localhost";
343 $hdr->{disk} = "/test1";
344 $hdr->{datestamp} = "20080706050403";
345 $hdr->{program} = "AMTAPETYPE";
346 $device->start_file($hdr);
347 $device->finish_file();
349 $hdr->{type} = $Amanda::Header::F_DUMPFILE;
350 $hdr->{name} = "localhost";
351 $hdr->{disk} = "/test2";
352 $hdr->{datestamp} = "20080706050403";
353 $hdr->{program} = "AMTAPETYPE";
354 $device->start_file($hdr);
355 $device->finish_file();
357 $hdr->{type} = $Amanda::Header::F_DUMPFILE;
358 $hdr->{name} = "localhost";
359 $hdr->{disk} = "/test3";
360 $hdr->{datestamp} = "20080706050403";
361 $hdr->{program} = "AMTAPETYPE";
362 $device->start_file($hdr);
363 $device->finish_file();
367 #set fsf_after_filemark to false
368 $device->property_set('FSF_AFTER_FILEMARK', 0)
369 or die "Error setting FSF_AFTER_FILEMARK: " . $device->error_or_status();
371 my $need_fsf_after_filemark = 0;
373 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
374 die ("Could not read label from: " . $device->error_or_status());
376 if ($device->volume_label != "TEST-001") {
377 die ("wrong label: ", $device->volume_label);
379 $device->start($ACCESS_READ, undef, undef)
380 or die ("Could not start device: " . $device->error_or_status());
382 $hdr = $device->seek_file(1);
383 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
384 die ("seek_file(1) failed");
386 if ($hdr->{disk} ne "/test1") {
387 die ("Wrong disk: " . $hdr->{disk} . " expected /test1");
389 data_to_null($device);
391 $hdr = $device->seek_file(2);
392 if ($device->status() == $DEVICE_STATUS_SUCCESS) {
393 if ($hdr->{disk} ne "/test2") {
394 die ("Wrong disk: " . $hdr->{disk} . " expected /test2");
396 data_to_null($device);
398 $hdr = $device->seek_file(3);
399 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
400 die("seek_file(3) failed");
402 if ($hdr->{disk} ne "/test3") {
403 die ("Wrong disk: " . $hdr->{disk} . " expected /test3");
405 data_to_null($device);
409 $need_fsf_after_filemark = 1;
411 # $device is in error, so open a new one
413 $device = open_device();
416 #verify need_fsf_after_filemark
417 my $fsf_after_filemark_works = 0;
418 if ($need_fsf_after_filemark) {
419 #set fsf_after_filemark to true
420 $device->property_set('FSF_AFTER_FILEMARK', 1)
421 or die "Error setting FSF_AFTER_FILEMARK: " . $device->error_or_status();
423 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
424 die ("Could not read label from: " . $device->error_or_status());
426 if ($device->volume_label != "TEST-001") {
427 die ("wrong label: ", $device->volume_label);
429 $device->start($ACCESS_READ, undef, undef)
430 or die ("Could not start device: " . $device->error_or_status());
432 $hdr = $device->seek_file(1);
433 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
434 die ("seek_file(1) failed");
436 if ($hdr->{disk} ne "/test1") {
437 die ("Wrong disk: " . $hdr->{disk} . " expected /test1");
439 data_to_null($device);
441 $hdr = $device->seek_file(2);
442 if ($device->status() == $DEVICE_STATUS_SUCCESS) {
443 if ($hdr->{disk} ne "/test2") {
444 die ("Wrong disk: " . $hdr->{disk} . " expected /test2");
446 data_to_null($device);
448 $hdr = $device->seek_file(3);
449 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
450 die("seek_file(3) failed");
452 if ($hdr->{disk} ne "/test3") {
453 die ("Wrong disk: " . $hdr->{disk} . " expected /test3");
455 data_to_null($device);
456 $fsf_after_filemark_works = 1;
458 die("seek_file(2) failed");
463 if ($need_fsf_after_filemark == 0 && $fsf_after_filemark_works == 0) {
464 if (defined $opt_property || $fsf_after_filemark) {
465 print STDOUT "device-property \"FSF_AFTER_FILEMARK\" \"false\"\n";
467 $device->property_set('FSF_AFTER_FILEMARK', 0);
468 } elsif ($need_fsf_after_filemark == 1 && $fsf_after_filemark_works == 1) {
469 if (defined $opt_property || !$fsf_after_filemark) {
470 print STDOUT "device-property \"FSF_AFTER_FILEMARK\" \"true\"\n";
472 $device->property_set('FSF_AFTER_FILEMARK', 1);
474 die ("Broken seek_file");
477 #Check seek to file 1 from header
478 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
479 die ("Could not read label from: " . $device->error_or_status());
481 if ($device->volume_label != "TEST-001") {
482 die ("wrong label: ", $device->volume_label);
484 $device->start($ACCESS_READ, undef, undef)
485 or die ("Could not start device: " . $device->error_or_status());
487 $hdr = $device->seek_file(1);
488 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
489 die ("seek_file(1) failed");
491 if ($hdr->{disk} ne "/test1") {
492 die ("Wrong disk: " . $hdr->{disk} . " expected /test1");
496 #Check seek to file 2 from header
497 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
498 die ("Could not read label from: " . $device->error_or_status());
500 if ($device->volume_label != "TEST-001") {
501 die ("wrong label: ", $device->volume_label);
503 $device->start($ACCESS_READ, undef, undef)
504 or die ("Could not start device: " . $device->error_or_status());
506 $hdr = $device->seek_file(2);
507 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
508 die ("seek_file(2) failed");
510 if ($hdr->{disk} ne "/test2") {
511 die ("Wrong disk: " . $hdr->{disk} . " expected /test1");
515 #Check seek to file 3 from header
516 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
517 die ("Could not read label from: " . $device->error_or_status());
519 if ($device->volume_label != "TEST-001") {
520 die ("wrong label: ", $device->volume_label);
522 $device->start($ACCESS_READ, undef, undef)
523 or die ("Could not start device: " . $device->error_or_status());
525 $hdr = $device->seek_file(3);
526 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
527 die ("seek_file(3) failed");
529 if ($hdr->{disk} ne "/test3") {
530 die ("Wrong disk: " . $hdr->{disk} . " expected /test1");
534 #Check seek to file 3 from eof of 1
535 if ($device->read_label() != $DEVICE_STATUS_SUCCESS) {
536 die ("Could not read label from: " . $device->error_or_status());
538 if ($device->volume_label != "TEST-001") {
539 die ("wrong label: ", $device->volume_label);
541 $device->start($ACCESS_READ, undef, undef)
542 or die ("Could not start device: " . $device->error_or_status());
544 $hdr = $device->seek_file(1);
545 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
546 die ("seek_file(1) failed");
548 data_to_null($device);
549 $hdr = $device->seek_file(3);
550 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
551 die ("seek_file(3) failed");
553 if ($hdr->{disk} ne "/test3") {
554 die ("Wrong disk: " . $hdr->{disk} . " expected /test3");
560 my ($compression_enabled) = @_;
562 my $device = open_device();
563 my $blocksize = $device->property_get("BLOCK_SIZE");
565 # First, write one very long file to get the total tape length
566 print STDERR "Writing one file to fill the volume.\n";
568 my $err = write_one_file(
571 PATTERN => 'RANDOM');
573 # if we wrote almost no data, then there's probably a problem
574 # with the device, so error out
575 if ($stats->{RANDOM}->{BYTES} < 1024 * 1024) {
576 die "Wrote less than 1MB to the device: $err\n";
578 my $volume_size_estimate = $stats->{RANDOM}->{BYTES};
579 my $speed_estimate = (($stats->{RANDOM}->{BYTES}."") / 1024)
580 / $stats->{RANDOM}->{TIME};
581 $speed_estimate = int $speed_estimate;
582 print STDERR "Wrote $volume_size_estimate bytes at $speed_estimate kb/sec\n";
585 if ($err eq 'LEOM') {
586 print STDERR "Got LEOM indication, so drive and kernel together support LEOM\n";
590 # now we want to write about 100 filemarks; round down to the blocksize
591 # to avoid counting padding as part of the filemark
592 my $file_size = $volume_size_estimate / 100;
593 $file_size -= $file_size % $blocksize;
595 print STDERR "Writing smaller files ($file_size bytes) to determine filemark.\n";
597 $device = open_device(); # re-open to rewind and clear errors
599 while (!write_one_file(
602 MAX_BYTES => $file_size,
603 PATTERN => 'RANDOM')) { }
605 my $filemark_estimate = ($volume_size_estimate - $stats->{RANDOM}->{BYTES})
606 / ($stats->{RANDOM}->{FILES} - 1);
607 if ($filemark_estimate < 0) {
608 $filemark_estimate = 0;
611 my $comment = "Created by amtapetype; compression "
612 . ($compression_enabled? "enabled" : "disabled");
614 # round these parameters to the nearest kb, since the parameters' units
616 my $volume_size_estimate_kb = $volume_size_estimate/1024;
617 my $filemark_kb = $filemark_estimate/1024;
619 # and suggest using device-property for blocksize if it's not an even multiple
622 if ($blocksize % 1024 == 0) {
623 $blocksize_line = "blocksize " . $blocksize/1024 . " kbytes";
625 $blocksize_line = "# add device-property \"BLOCK_SIZE\" \"$blocksize\" to the device";
629 define tapetype $opt_tapetype_name {
631 length $volume_size_estimate_kb kbytes
632 filemark $filemark_kb kbytes
633 speed $speed_estimate kps
639 print "# for this drive and kernel, LEOM is supported; add\n";
640 print "# device-property \"LEOM\" \"TRUE\"\n";
641 print "# for this device.\n";
643 print "# LEOM is not supported for this drive and kernel\n";
649 Usage: amtapetype [-h] [-c] [-f] [-b blocksize] [-t typename] [-l label]
650 [ [-o config_override] ... ] [config] device
651 -h Display this message
652 -c Only check hardware compression state
653 -f Run amtapetype even if the loaded volume is already in use
654 or compression is enabled.
655 -b Blocksize to use (default 32k)
656 -t Name to give to the new tapetype definition
657 -l Label to write to the tape (default is randomly generated)
658 -p Check property of the device.
659 -o Overwrite configuration parameter (such as device properties)
660 Blocksize can include an optional suffix (k, m, or g)
662 If CONFIG is specified, the device and its configuration are loaded
663 from the correspnding amanda.conf.
668 ## Application initialization
670 Amanda::Util::setup_application("amtapetype", "server", $CONTEXT_CMDLINE);
671 config_init(0, undef);
673 my $config_overrides = new_config_overrides($#ARGV+1);
675 debug("Arguments: " . join(' ', @ARGV));
676 Getopt::Long::Configure(qw(bundling));
678 'version' => \&Amanda::Util::version_opt,
679 'help|usage|?|h' => \&usage,
680 'c' => \$opt_only_compression,
682 my ($num, $suff) = ($_[1] =~ /^([0-9]+)\s*(.*)$/);
683 die "Invalid blocksize '$_[1]'" unless (defined $num);
684 my $mult = (defined $suff)?
685 Amanda::Config::find_multiplier($suff) : 1;
686 die "Invalid suffix '$suff'" unless ($mult);
687 $opt_blocksize = $num * $mult;
689 't=s' => \$opt_tapetype_name,
692 'p' => \$opt_property,
693 'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
695 usage() if (@ARGV < 1 or @ARGV > 2);
697 set_config_overrides($config_overrides);
699 $opt_config = shift @ARGV;
700 config_init($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
702 config_init(0, undef);
704 $opt_device_name= shift @ARGV;
706 my ($cfgerr_level, @cfgerr_errors) = config_errors();
707 if ($cfgerr_level >= $CFGERR_WARNINGS) {
708 config_print_errors();
709 if ($cfgerr_level >= $CFGERR_ERRORS) {
710 die("errors processing config file");
714 Amanda::Util::finish_setup($RUNNING_AS_ANY);
716 # Find property of the device.
719 if (!defined $opt_property) {
720 my $compression_enabled = check_compression();
721 print STDERR "Compression: ",
722 $compression_enabled? "enabled" : "disabled",
725 if ($compression_enabled and !$opt_force) {
726 print STDERR "Turn off compression or run amtapetype with the -f option\n";
730 if (!$opt_only_compression) {
731 make_tapetype($compression_enabled);
735 Amanda::Util::finish_application();