summaryrefslogtreecommitdiffstats
path: root/perl-install/fsedit.pm
blob: 14b15aaef4a97bd7c4b42e96b5077d1efc7f4f5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
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
204
205
206
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
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
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
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
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
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
package fsedit;

use diagnostics;
use strict;
use vars qw(%suggestions);
use feature 'state';

#-######################################################################################
#- misc imports
#-######################################################################################
use common;
use partition_table;
use partition_table::raw;
use fs::get;
use fs::type;
use fs::loopback;
use fs::proc_partitions;
use detect_devices;
use devices;
use log;
use fs;

# min_hd_size: only suggest this partition if the hd size is bigger than that
%suggestions = (
  N_("simple") => [
    { mntpoint => "/",     size => MB(300), fs_type => defaultFS(), ratio => 6, maxsize => MB(51500) },
    { mntpoint => "swap",  size => MB(256), fs_type => 'swap', ratio => 1, maxsize => MB(4096) },
    { mntpoint => "/home", size => MB(300), fs_type => defaultFS(), ratio => 12, min_hd_size => MB(51200) },
  ], N_("with /usr") => [
    { mntpoint => "/",     size => MB(250), fs_type => defaultFS(), ratio => 1, maxsize => MB(8000) },
    { mntpoint => "swap",  size =>  MB(64), fs_type => 'swap', ratio => 1, maxsize => MB(4000) },
    { mntpoint => "/usr",  size => MB(300), fs_type => defaultFS(), ratio => 4, maxsize => MB(8000) },
    { mntpoint => "/home", size => MB(100), fs_type => defaultFS(), ratio => 3, min_hd_size => MB(10000) },
  ], N_("server") => [
    { mntpoint => "/",     size => MB(150), fs_type => defaultFS(), ratio => 1, maxsize => MB(8000) },
    { mntpoint => "swap",  size =>  MB(64), fs_type => 'swap', ratio => 2, maxsize => MB(4000) },
    { mntpoint => "/usr",  size => MB(300), fs_type => defaultFS(), ratio => 4, maxsize => MB(8000) },
    { mntpoint => "/var",  size => MB(200), fs_type => defaultFS(), ratio => 3 },
    { mntpoint => "/home", size => MB(150), fs_type => defaultFS(), ratio => 3, min_hd_size => MB(10000) },
    { mntpoint => "/tmp",  size => MB(150), fs_type => defaultFS(), ratio => 2, maxsize => MB(4000) },
  ],
);

sub init_efi_suggestions {
    my ($fstab, $o_force) = @_;
    state $done;
    return if $done && !$o_force;
    $done++;

    # only suggests /boot/EFI if there's not already one:
    return if !is_uefi() || grep { isESP($_) } @$fstab;

    foreach (values %suggestions) {
	@$_ = ({ mntpoint => "/boot/EFI", size => MB(100), pt_type => 0xef, ratio => 1, maxsize => MB(300) }, @$_);
    }
}

my @suggestions_mntpoints = (
    "/var/ftp", "/var/www", "/boot", '/usr/local', '/opt',
   "/mnt/windows",
);

#-######################################################################################
#- Functions
#-######################################################################################
sub recompute_loopbacks {
    my ($all_hds) = @_;
    my @fstab = fs::get::fstab($all_hds);
    @{$all_hds->{loopbacks}} = map { isPartOfLoopback($_) ? @{$_->{loopback}} : () } @fstab;
}

sub raids {
    my ($hds) = @_;

    my @parts = fs::get::hds_fstab(@$hds);

    my @l = grep { isRawRAID($_) } @parts or return [];

    log::l("looking for raids in " . join(' ', map { $_->{device} } @l));
    
    require raid;
    raid::detect_during_install(@l) if $::isInstall;
    raid::get_existing(@l);
}

sub dmcrypts {
    my ($all_hds) = @_;

    my @parts = fs::get::fstab($all_hds);

    my @l = grep { fs::type::isRawLUKS($_) } @parts or return;

    log::l("using dm-crypt from " . join(' ', map { $_->{device} } @l));
    
    require fs::dmcrypt;
    fs::dmcrypt::read_crypttab($all_hds);

    fs::dmcrypt::get_existing(@l);
}

sub lvms {
    my ($all_hds) = @_;
    my @pvs = grep { isRawLVM($_) } fs::get::fstab($all_hds) or return;
    scan_pvs(@pvs);
}

sub scan_pvs {
    my (@pvs) = @_;

    log::l("looking for vgs in " . join(' ', map { $_->{device} } @pvs));

    #- otherwise vgscan will not find them
    devices::make($_->{device}) foreach @pvs; 
    require lvm;

    my @lvms;
    foreach (@pvs) {
	my $name = lvm::pv_to_vg($_) or next;
	my $lvm = find { $_->{VG_name} eq $name } @lvms;
	if (!$lvm) {
	    $lvm = new lvm($name);
	    lvm::update_size($lvm);
	    lvm::get_lvs($lvm);
	    push @lvms, $lvm;
	}
	$_->{lvm} = $name;
	push @{$lvm->{disks}}, $_;
    }
    @lvms;
}

sub handle_dmraid {
    my ($drives, $o_in) = @_;

    @$drives > 1 or return;

    devices::make($_->{device}) foreach @$drives;

    require fs::dmraid; 
    eval { fs::dmraid::init() } or log::l("dmraid::init failed"), return;

    my @vgs = fs::dmraid::vgs();
    log::l(sprintf('dmraid: ' . join(' ', map { "$_->{device} [" . join(' ', @{$_->{disks}}) . "]" } @vgs)));

    if ($o_in && @vgs && $::isInstall) {
	@vgs = grep {
	    $o_in->ask_yesorno('', N("BIOS software RAID detected on disks %s. Activate it?", join(' ', @{$_->{disks}})), 1);
	} @vgs or do {
	    fs::dmraid::call_dmraid('-an');
	    return;
	};
    }
    if (!$::isInstall) {
	fs::dmraid::migrate_device_names($_) foreach @vgs;
    }
    log::l("using dmraid on " . join(' ', map { $_->{device} } @vgs));

    my @used_hds = map {
	my $part = fs::get::device2part($_, $drives) or log::l("handle_dmraid: can't find $_ in known drives");
	if_($part, $part);
    } map { @{$_->{disks}} } @vgs;

    @$drives = difference2($drives, \@used_hds);

    push @$drives, @vgs;
}

sub get_hds {
    my ($o_flags, $o_in) = @_;
    my $flags = $o_flags || {};
    $flags->{readonly} && ($flags->{clearall} || $flags->{clear}) and die "conflicting flags readonly and clear/clearall";

    my @drives = detect_devices::hds();

    #- replace drives used in dmraid by the merged name
    handle_dmraid(\@drives, $o_in) if !$flags->{nodmraid};

    foreach my $hd (@drives) {
	$hd->{file} = devices::make($hd->{device});
    }

    @drives = partition_table::raw::get_geometries(@drives);

    my (@hds, @raw_hds);
    foreach my $hd (@drives) {
	$hd->{readonly} = $flags->{readonly};

	eval { partition_table::raw::test_for_bad_drives($hd) if !$flags->{no_bad_drives} };
	if (my $err = $@) {
	    log::l("test_for_bad_drives returned $err");
	    if ($err =~ /write error:/) { 
		log::l("setting $hd->{device} readonly");
		$hd->{readonly} = 1;
	    } elsif ($err =~ /read error:/) {
		next;
	    } else {
		$o_in and $o_in->ask_warn('', $err);
		next;
	    }
	}

	if ($flags->{clearall} || member($hd->{device}, @{$flags->{clear} || []})) {
	    my $lvms = []; #- temporary one, will be re-created later in get_hds()
	    partition_table_initialize($lvms, $hd, $o_in);
	} else {
	    my $handle_die_and_cdie = sub {
		if (my $type = fs::type::type_subpart_from_magic($hd)) {
		    #- non partitioned drive?
		    if (exists $hd->{usb_description} && $type->{fs_type}) {
			#- USB keys
			put_in_hash($hd, $type);
			push @raw_hds, $hd;
			$hd = '';
			1;
		    } else {
			0;
		    }
		} elsif ($hd->{readonly}) {
		    log::l("using /proc/partitions since diskdrake failed :(");
		    fs::proc_partitions::use_($hd);
		    1;
		} else {
		    0;
		}
	    };
	    my $handled;
	    eval {
		catch_cdie {
		    partition_table::read($hd);
		    if (listlength(partition_table::get_normal_parts($hd)) == 0) {
			$handled = 1 if $handle_die_and_cdie->();
		    } elsif ($::isInstall) {
			if (fs::type::is_dmraid($hd)) {
			    if (my $p = find { ! -e "/dev/$_->{device}" } partition_table::get_normal_parts($hd)) {
				#- dmraid should have created the device, so it means we don't agree
				die sprintf(q(bad dmraid (missing partition %s), you may try rebooting install with option "nodmraid"), $p->{device});
			    }
			} else {
			    fs::proc_partitions::compare($hd) if !detect_devices::is_xbox();
			}
		    }
		} sub {
		    my $err = $@;
		    if ($handle_die_and_cdie->()) {
			$handled = 1;
			0; #- do not continue, transform cdie into die
		    } else {
			!$o_in || $o_in->ask_okcancel('', formatError($err));
		    }
		};
	    };
	    if (my $err = $@) {
		if ($handled) {
		    #- already handled in cdie handler above
		} elsif ($handle_die_and_cdie->()) {
		} elsif ($o_in && $o_in->ask_yesorno(N("Error"), 
N("I cannot read the partition table of device %s, it's too corrupted for me :(
I can try to go on, erasing over bad partitions (ALL DATA will be lost!).
The other solution is to not allow DrakX to modify the partition table.
(the error is %s)

Do you agree to lose all the partitions?
", $hd->{device}, formatError($err)))) {
		    partition_table::raw::zero_MBR($hd);
		} else {
		    #- using it readonly
		    log::l("using /proc/partitions since diskdrake failed :(");
		    fs::proc_partitions::use_($hd);
		}
	    }
	    $hd or next;

	    member($_->{device}, @{$flags->{clear} || []}) and partition_table::remove($hd, $_)
	      foreach partition_table::get_normal_parts($hd);
	}

	my @parts = partition_table::get_normal_parts($hd);

	# checking the magic of the filesystem, do not rely on pt_type
	foreach (@parts) {
	    if (my $type = fs::type::type_subpart_from_magic($_)) {
                $type->{pt_type} = $_->{pt_type}; #- keep {pt_type}
                put_in_hash($_, $type); 
	    } else {
		$_->{bad_fs_type_magic} = 1;
	    }
	}

	if ($hd->{usb_media_type}) {
	    $hd->{is_removable} = 1;
	    $_->{is_removable} = 1 foreach @parts;
	}

	push @hds, $hd;
    }

    #- detect raids before LVM allowing LVM on raid
    my $raids = raids(\@hds);
    my $all_hds = { %{ fs::get::empty_all_hds() }, hds => \@hds, raw_hds => \@raw_hds, lvms => [], raids => $raids };

    $all_hds->{lvms} = [ lvms($all_hds) ];

    fs::get_major_minor([ fs::get::fstab($all_hds) ]);

    # must be done after getting major/minor
    $all_hds->{dmcrypts} = [ dmcrypts($all_hds) ];
    # allow lvm on dmcrypt
    $all_hds->{lvms} = [ lvms($all_hds) ];

    $_->{faked_device} = 0 foreach fs::get::fstab($all_hds);

    $all_hds;
}

#- are_same_partitions() do not look at the device name since things may have changed
sub are_same_partitions {
    my ($part1, $part2) = @_;
    foreach ('start', 'size', 'pt_type', 'fs_type', 'rootDevice') {
	$part1->{$_} eq $part2->{$_} or return 0;
    }
    1;
}

sub is_one_big_fat_or_NT {
    my ($hds) = @_;
    @$hds == 1 or return 0;

    my @l = fs::get::hds_fstab(@$hds);
    @l == 1 && isFat_or_NTFS($l[0]) && fs::get::hds_free_space(@$hds) < MB(10);
}


sub computeSize {
    my ($part, $best, $all_hds, $suggestions) = @_;
    my $max = $part->{maxsize} || $part->{size};
    return min($max, $best->{size}) unless $best->{ratio};

    my %free_space;
    $free_space{$_->{rootDevice}} += $_->{size} foreach fs::get::holes($all_hds);

    my @l = my @L = grep {
	my @possible = $_->{hd} ? $_->{hd} : keys %free_space;
	my $size = $_->{size};
	if (my $dev = find { $free_space{$_} >= $size } @possible) {
	    $free_space{$dev} -= $size;
	    1;
	} else { 0 } } @$suggestions;

    my $free_space = $best->{hd} && $free_space{$best->{hd}} || sum(values %free_space);

    my $cylinder_size_maxsize_adjusted;
    my $tot_ratios = 0;
    while (1) {
	my $old_free_space = $free_space;
	my $old_tot_ratios = $tot_ratios;

	$tot_ratios = sum(map { $_->{ratio} } @l);
	last if $tot_ratios == $old_tot_ratios;

	@l = grep { 
	    if ($_->{ratio} && $_->{maxsize} && $tot_ratios &&
		$_->{size} + $_->{ratio} / $tot_ratios * $old_free_space >= $_->{maxsize}) {
		return min($max, $best->{maxsize}) if $best->{mntpoint} eq $_->{mntpoint};
		$free_space -= $_->{maxsize} - $_->{size};
		if (!$cylinder_size_maxsize_adjusted++) {
		    eval { $free_space += fs::get::part2hd($part, $all_hds)->cylinder_size - 1 };
		}
		0;
	    } else {
		$_->{ratio};
	    } 
	} @l;
    }
    my $size = int min($max, $best->{size} + $free_space * ($tot_ratios && $best->{ratio} / $tot_ratios));
    #- verify other entry can fill the hole
    (any { $_->{size} <= $max - $size } @L) ? $size : $max;
}

sub suggest_part {
    my ($part, $all_hds, $o_suggestions) = @_;
    my $suggestions = $o_suggestions || $suggestions{server} || $suggestions{simple};

    #- suggestions now use {fs_type}, but still keep compatibility
    foreach (@$suggestions) {
	fs::type::set_pt_type($_, $_->{pt_type}) if !exists $_->{fs_type};
    }

    my $hd = fs::get::part2hd($part, $all_hds);
    my $hd_size = $hd && $hd->{totalsectors}; # nb: no $hd if $part is /dev/mdX
    my $has_swap = any { isSwap($_) } fs::get::fstab($all_hds);

    my @local_suggestions =
      grep { !$_->{mntpoint} && !$_->{VG_name} || !fs::get::has_mntpoint($_->{mntpoint}, $all_hds) || isSwap($_) && !$has_swap }
      grep { !$_->{min_hd_size} || !$hd_size || $_->{min_hd_size} <= $hd_size }
      grep { !$_->{hd} || $_->{hd} eq $part->{rootDevice} }
	@$suggestions;

    #- this allows specifying the size using a relative size.
    #- one should rather use {ratio} instead
    foreach (@local_suggestions) {
	if ($_->{percent_size} && $_->{percent_size} =~ /(.+?)%?$/) {
	    $_->{size} = $1 / 100 * $hd_size;
	    log::l("in suggestion, setting size=$_->{size} for percent_size=$_->{percent_size}");
	}
    }

    my ($best) =
      grep { !$_->{maxsize} || $part->{size} <= $_->{maxsize} }
      grep { $_->{size} <= ($part->{maxsize} || $part->{size}) }
      grep { !$part->{fs_type} || $part->{fs_type} eq $_->{fs_type} || isTrueFS($part) && isTrueFS($_) }
	@local_suggestions;

    defined $best or return 0; #- sorry no suggestion :(

    $part->{mntpoint} = $best->{mntpoint};
    fs::type::set_type_subpart($part, $best) if !isTrueFS($best) || !isTrueFS($part);
    $part->{size} = computeSize($part, $best, $all_hds, \@local_suggestions);
    foreach ('options', 'lv_name', 'encrypt_key', 'primaryOrExtended',
	     'device_LABEL', 'prefer_device_LABEL', 'device_UUID', 'prefer_device_UUID', 'prefer_device') {
	$part->{$_} = $best->{$_} if $best->{$_};
    }
    $best;
}

sub suggestions_mntpoint {
    my ($all_hds) = @_;
    sort grep { !/swap/ && !fs::get::has_mntpoint($_, $all_hds) }
      (@suggestions_mntpoints, map { $_->{mntpoint} } @{$suggestions{server} || $suggestions{simple}});
}

#- you can do this before modifying $part->{mntpoint}
#- so $part->{mntpoint} should not be used here, use $mntpoint instead
sub check_mntpoint {
    my ($mntpoint, $part, $all_hds) = @_;

    $mntpoint eq '' || isSwap($part) || isNonMountable($part) and return 0;
    $mntpoint =~ m|^/| or die N("Mount points must begin with a leading /");
    $mntpoint =~ m|[\x7f-\xff]| and cdie N("Mount points should contain only alphanumerical characters");
    fs::get::mntpoint2part($mntpoint, [ grep { $_ ne $part } fs::get::really_all_fstab($all_hds) ]) and die N("There is already a partition with mount point %s\n", $mntpoint);

    if ($mntpoint eq "/" && isRAID($part) && !fs::get::has_mntpoint("/boot", $all_hds)) {
	# lilo handles / on RAID1
	if ($part->{level} ne '1') {
	    cdie N("You've selected a software RAID partition as root (/).
No bootloader is able to handle this without a /boot partition.
Please be sure to add a separate /boot partition");
	} else {
	    # LILO only handles 0.90 metadata
	    if ($part->{isFormatted} && $part->{metadata} && $part->{metadata} ne '0.90') {
		cdie N("Metadata version unsupported for a boot partition. Please be sure to add a separate /boot partition.");
	    } else {
		$part->{metadata} = '0.90';
	    }
	}
    }

    if ($mntpoint eq "/boot" && isRAID($part))  {
	die N("You've selected a software RAID partition as /boot.
No bootloader is able to handle this.") if $part->{level} ne '1'; # lilo handles /boot on RAID1
	# LILO only handles 0.90 metadata
	if ($part->{isFormatted} && $part->{metadata} && $part->{metadata} ne '0.90') {
	    die N("Metadata version unsupported for a boot partition.");
	} else {
	    $part->{metadata} = '0.90';
	}
    }

    if ($mntpoint eq "/" && (isLUKS($part) || isRawLUKS($part)) && !fs::get::has_mntpoint("/boot", $all_hds)) {
	cdie N("You've selected an encrypted partition as root (/).
No bootloader is able to handle this without a /boot partition.
Please be sure to add a separate /boot partition");
    }

    if ($mntpoint eq "/boot" && (isLUKS($part) || isRawLUKS($part)))  {
	die N("You cannot use an encrypted filesystem for mount point %s", "/boot");
    }

    #- NB: if the LV doesn't exist, lv_nb_pvs returns 0
    die N("You cannot use the LVM Logical Volume for mount point %s since it spans physical volumes", $mntpoint)
      if $mntpoint eq '/boot' && isLVM($part) && lvm::lv_nb_pvs($part) > 1;
    cdie N("You've selected the LVM Logical Volume as root (/).
The bootloader is not able to handle this when the volume spans physical volumes.
You should create a separate /boot partition first") if $mntpoint eq "/" && isLVM($part) && lvm::lv_nb_pvs($part) != 1 && !fs::get::has_mntpoint("/boot", $all_hds);

    cdie N("This directory should remain within the root filesystem")
      if member($mntpoint, qw(/root));
    die N("This directory should remain within the root filesystem")
      if member($mntpoint, qw(/bin /dev /etc /lib /sbin /mnt /media));
    die N("You need a true filesystem (ext2/3/4, reiserfs, xfs, or jfs) for this mount point\n")
      if !isTrueLocalFS($part) && $mntpoint eq '/';
    die N("You need a true filesystem (ext2/3/4, reiserfs, xfs, or jfs) for this mount point\n")
      if !isTrueFS($part) && member($mntpoint, '/home', fs::type::directories_needed_to_boot());
    die N("You cannot use an encrypted filesystem for mount point %s", $mntpoint)
      if $part->{options} =~ /encrypted/ && member($mntpoint, qw(/ /usr /var /boot));

    local $part->{mntpoint} = $mntpoint;
    fs::loopback::check_circular_mounts($part, $all_hds);
}

sub add {
    my ($hd, $part, $all_hds, $options) = @_;

    isSwap($part) ?
      ($part->{mntpoint} = 'swap') :
      $options->{force} || check_mntpoint($part->{mntpoint}, $part, $all_hds);

    delete $part->{maxsize};

    if (isLVM($hd)) {
	lvm::lv_create($hd, $part);
    } else {
	partition_table::add($hd, $part, $options->{primaryOrExtended});
    }
    fs::get_major_minor([ $part ]);
}

sub allocatePartitions {
    my ($all_hds, $to_add) = @_;

    my @to_add = @$to_add;
 
    foreach my $part_ (fs::get::holes($all_hds, 'non_readonly')) {
	my ($start, $size, $dev) = @$part_{"start", "size", "rootDevice"};
	my ($part, $suggested);
	while ($suggested = suggest_part($part = { start => $start, size => 0, maxsize => $size, rootDevice => $dev }, 
					 $all_hds, \@to_add)) {
	    my $hd = fs::get::part2hd($part, $all_hds);
	    add($hd, $part, $all_hds, { primaryOrExtended => $part->{primaryOrExtended} });
	    $size -= $part->{size} + $part->{start} - $start;
	    $start = $part->{start} + $part->{size};
 	    @to_add = grep { $_ != $suggested } @to_add;
	}
    }
}

sub auto_allocate {
    my ($all_hds, $o_suggestions) = @_;
    my $before = listlength(fs::get::fstab($all_hds));

    my $suggestions = $o_suggestions || $suggestions{simple};
    allocatePartitions($all_hds, $suggestions);

    if ($o_suggestions) {
	auto_allocate_raids($all_hds, $suggestions);
	if (auto_allocate_vgs($all_hds, $suggestions)) {
	    #- allocatePartitions needs to be called twice, once for allocating PVs, once for allocating LVs
	    my @vgs = map { $_->{VG_name} } @{$all_hds->{lvms}};
	    my @suggested_lvs = grep { member($_->{hd}, @vgs) } @$suggestions;
	    allocatePartitions($all_hds, \@suggested_lvs);
	}
    }

    partition_table::assign_device_numbers($_) foreach @{$all_hds->{hds}};

    if ($before == listlength(fs::get::fstab($all_hds))) {
	# find out why auto_allocate failed
	if (any { !fs::get::has_mntpoint($_->{mntpoint}, $all_hds) } @$suggestions) {
	    die N("Not enough free space for auto-allocating");
	} else {
	    die N("Nothing to do");
	}
    }
}

sub auto_allocate_raids {
    my ($all_hds, $suggestions) = @_;

    my @raids = grep { isRawRAID($_) } fs::get::fstab($all_hds) or return;

    require raid;
    my @mds = grep { $_->{hd} =~ /md/ } @$suggestions;
    foreach my $md (@mds) {
	my @raids_ = grep { !$md->{parts} || $md->{parts} =~ /\Q$_->{mntpoint}/ } @raids;
	@raids = difference2(\@raids, \@raids_);

	my %h = %$md;
	delete @h{'hd', 'parts'}; # keeping mntpoint, level, chunk-size, fs_type/pt_type
	$h{disks} = \@raids_;

	my $part = raid::new($all_hds->{raids}, %h);

	raid::updateSize($part);
	push @raids, $part; #- we can build raid over raid
    }
}

sub auto_allocate_vgs {
    my ($all_hds, $suggestions) = @_;

    my @pvs = grep { isRawLVM($_) } fs::get::fstab($all_hds) or return 0;

    my @vgs = grep { $_->{VG_name} } @$suggestions or return 0;

    partition_table::write($_) foreach @{$all_hds->{hds}};

    require lvm;

    foreach my $vg (@vgs) {
	my $lvm = new lvm($vg->{VG_name});
	push @{$all_hds->{lvms}}, $lvm;
	
	my @pvs_ = grep { !$vg->{parts} || $vg->{parts} =~ /\Q$_->{mntpoint}/ } @pvs;
	@pvs = difference2(\@pvs, \@pvs_);

	foreach my $part (@pvs_) {
	    raid::make($all_hds->{raids}, $part) if isRAID($part);
	    $part->{lvm} = $lvm->{VG_name};
	    delete $part->{mntpoint};
	    lvm::vg_add($part);
	    push @{$lvm->{disks}}, $part;
	}
	lvm::update_size($lvm);
    }
    1;
}

sub change_type {
    my ($type, $hd, $part) = @_;
    $type->{pt_type} != $part->{pt_type} || $type->{fs_type} ne $part->{fs_type} or return;
    fs::type::check($type->{fs_type}, $hd, $part);
    delete $part->{device_UUID};
    $hd->{isDirty} = 1;
    $part->{mntpoint} = '' if isSwap($part) && $part->{mntpoint} eq "swap";
    $part->{mntpoint} = '' if fs::type::cannotBeMountable($part);
    set_isFormatted($part, 0);
    fs::type::set_type_subpart($part, $type);
    fs::mount_options::rationalize($part);
    1;
}

sub partition_table_clear_and_initialize {
    my ($lvms, $hd, $o_in, $o_type, $b_warn) = @_;
    $hd->clear_existing;
    partition_table_initialize($lvms, $hd, $o_in, $o_type, $b_warn);
}

sub partition_table_initialize {
    my ($lvms, $hd, $o_in, $o_type, $b_warn) = @_;
    partition_table::initialize($hd, $o_type);
    if ($hd->isa('partition_table::lvm')) {
	if ($b_warn && $o_in) {
	    $o_in->ask_okcancel_('', N("ALL existing partitions and their data will be lost on drive %s", partition_table::description($hd))) or return;
	}
	require lvm;
	lvm::check($o_in ? $o_in->do_pkgs : do_pkgs_standalone->new) if $::isStandalone;
	lvm::create_singleton_vg($lvms, fs::get::hds_fstab($hd));
    }
}

1;
n class="hl kwb">@row = $sth->fetchrow_array; # normalize bits my $bit; if (defined $row[0]) { $bit = $row[0] << 1; } else { $bit = 1; } print "Adding group $name ...\n"; $sth = $dbh->prepare('INSERT INTO groups (bit, name, description, userregexp) VALUES (?, ?, ?, ?)'); $sth->execute($bit, $name, $desc, $userregexp); return $bit; } # # BugZilla uses --GROUPS-- to assign various rights to its users. # AddGroup 'tweakparams', 'Can tweak operating parameters'; AddGroup 'editusers', 'Can edit or disable users'; AddGroup 'creategroups', 'Can create and destroy groups.'; AddGroup 'editcomponents', 'Can create, destroy, and edit components.'; AddGroup 'editkeywords', 'Can create, destroy, and edit keywords.'; # Add the groupset field here because this code is run before the # code that updates the database structure. &AddField('profiles', 'groupset', 'bigint not null'); if (!GroupExists("editbugs")) { my $id = AddGroup('editbugs', 'Can edit all aspects of any bug.', ".*"); $dbh->do("UPDATE profiles SET groupset = groupset | $id"); } if (!GroupExists("canconfirm")) { my $id = AddGroup('canconfirm', 'Can confirm a bug.', ".*"); $dbh->do("UPDATE profiles SET groupset = groupset | $id"); } ########################################################################### # Create initial test product if there are no products present. ########################################################################### my $sth = $dbh->prepare("SELECT product FROM products"); $sth->execute; unless ($sth->rows) { print "Creating initial dummy product 'TestProduct' ...\n"; $dbh->do('INSERT INTO products(product, description) VALUES ("TestProduct", "This is a test product. This ought to be blown away and ' . 'replaced with real stuff in a finished installation of ' . 'bugzilla.")'); $dbh->do('INSERT INTO versions (value, program) VALUES ("other", "TestProduct")'); $dbh->do('INSERT INTO components (value, program, description) VALUES (' . '"TestComponent", "TestProduct", ' . '"This is a test component in the test product database. ' . 'This ought to be blown away and replaced with real stuff in ' . 'a finished installation of bugzilla.")'); $dbh->do('INSERT INTO milestones (product, value) VALUES ("TestProduct","---")'); } ########################################################################### # Populate the list of fields. ########################################################################### my $headernum = 1; sub AddFDef ($$$) { my ($name, $description, $mailhead) = (@_); $name = $dbh->quote($name); $description = $dbh->quote($description); my $sth = $dbh->prepare("SELECT fieldid FROM fielddefs " . "WHERE name = $name"); $sth->execute(); my ($fieldid) = ($sth->fetchrow_array()); if (!$fieldid) { $fieldid = 'NULL'; } $dbh->do("REPLACE INTO fielddefs " . "(fieldid, name, description, mailhead, sortkey) VALUES " . "($fieldid, $name, $description, $mailhead, $headernum)"); $headernum++; } AddFDef("bug_id", "Bug \#", 1); AddFDef("short_desc", "Summary", 1); AddFDef("product", "Product", 1); AddFDef("version", "Version", 1); AddFDef("rep_platform", "Platform", 1); AddFDef("bug_file_loc", "URL", 1); AddFDef("op_sys", "OS/Version", 1); AddFDef("bug_status", "Status", 1); AddFDef("status_whiteboard", "Status Whiteboard", 1); AddFDef("keywords", "Keywords", 1); AddFDef("resolution", "Resolution", 1); AddFDef("bug_severity", "Severity", 1); AddFDef("priority", "Priority", 1); AddFDef("component", "Component", 1); AddFDef("assigned_to", "AssignedTo", 1); AddFDef("reporter", "ReportedBy", 1); AddFDef("votes", "Votes", 0); AddFDef("qa_contact", "QAContact", 0); AddFDef("cc", "CC", 0); AddFDef("dependson", "BugsThisDependsOn", 0); AddFDef("blocked", "OtherBugsDependingOnThis", 0); AddFDef("attachments.description", "Attachment description", 0); AddFDef("attachments.thedata", "Attachment data", 0); AddFDef("attachments.mimetype", "Attachment mime type", 0); AddFDef("attachments.ispatch", "Attachment is patch", 0); AddFDef("target_milestone", "Target Milestone", 0); AddFDef("delta_ts", "Last changed date", 0); AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed", 0); AddFDef("longdesc", "Comment", 0); ########################################################################### # Detect changed local settings ########################################################################### sub GetFieldDef ($$) { my ($table, $field) = @_; my $sth = $dbh->prepare("SHOW COLUMNS FROM $table"); $sth->execute; while (my $ref = $sth->fetchrow_arrayref) { next if $$ref[0] ne $field; return $ref; } } sub GetIndexDef ($$) { my ($table, $field) = @_; my $sth = $dbh->prepare("SHOW INDEX FROM $table"); $sth->execute; while (my $ref = $sth->fetchrow_arrayref) { next if $$ref[2] ne $field; return $ref; } } sub CountIndexes ($) { my ($table) = @_; my $sth = $dbh->prepare("SHOW INDEX FROM $table"); $sth->execute; if ( $sth->rows == -1 ) { die ("Unexpected response while counting indexes in $table:" . " \$sth->rows == -1"); } return ($sth->rows); } sub DropIndexes ($) { my ($table) = @_; my %SEEN; # get the list of indexes # my $sth = $dbh->prepare("SHOW INDEX FROM $table"); $sth->execute; # drop each index # while ( my $ref = $sth->fetchrow_arrayref) { # note that some indexes are described by multiple rows in the # index table, so we may have already dropped the index described # in the current row. # next if exists $SEEN{$$ref[2]}; my $dropSth = $dbh->prepare("ALTER TABLE $table DROP INDEX $$ref[2]"); $dropSth->execute; $dropSth->finish; $SEEN{$$ref[2]} = 1; } } # # Check if the enums in the bugs table return the same values that are defined # in the various locally changeable variables. If this is true, then alter the # table definition. # sub CheckEnumField ($$@) { my ($table, $field, @against) = @_; my $ref = GetFieldDef($table, $field); #print "0: $$ref[0] 1: $$ref[1] 2: $$ref[2] 3: $$ref[3] 4: $$ref[4]\n"; $_ = "enum('" . join("','", @against) . "')"; if ($$ref[1] ne $_) { print "Updating field $field in table $table ...\n"; $_ .= " NOT NULL" if $$ref[3]; $dbh->do("ALTER TABLE $table CHANGE $field $field $_"); $::regenerateshadow = 1; } } # # This code changes the enum types of some SQL tables whenever you change # some --LOCAL-- variables. Once you have a running system, to add new # severities, priorities, operating systems and platforms, add them to # the localconfig file and then re-run checksetup.pl which will make the # necessary changes to your database. Additions to these fields in # checksetup.pl after the initial installation of bugzilla on a system # are ignored. # CheckEnumField('bugs', 'bug_severity', @::severities); CheckEnumField('bugs', 'priority', @::priorities); CheckEnumField('bugs', 'op_sys', @::opsys); CheckEnumField('bugs', 'rep_platform', @::platforms); ########################################################################### # Promote first user into every group ########################################################################### # # Assume you just logged in. Now how can you administrate the system? Just # execute checksetup.pl again. If there is only 1 user in bugzilla, then # this user is promoted into every group. # $sth = $dbh->prepare("SELECT login_name FROM profiles"); $sth->execute; # when we have exactly one user ... if ($sth->rows == 1) { my @row = $sth->fetchrow_array; print "Putting user $row[0] into every group ...\n"; # are this enought f's for now? :-) $dbh->do("update profiles set groupset=0xffffffffffff"); } ########################################################################### # Update the tables to the current definition ########################################################################### # # As time passes, fields in tables get deleted, added, changed and so on. # So we need some helper subroutines to make this possible: # sub ChangeFieldType ($$$) { my ($table, $field, $newtype) = @_; my $ref = GetFieldDef($table, $field); #print "0: $$ref[0] 1: $$ref[1] 2: $$ref[2] 3: $$ref[3] 4: $$ref[4]\n"; my $oldtype = $ref->[1]; if ($ref->[4]) { $oldtype .= qq{ default "$ref->[4]"}; } if ($oldtype ne $newtype) { print "Updating field type $field in table $table ...\n"; print "old: $oldtype\n"; print "new: $newtype\n"; $newtype .= " NOT NULL" if $$ref[3]; $dbh->do("ALTER TABLE $table CHANGE $field $field $newtype"); } } sub RenameField ($$$) { my ($table, $field, $newname) = @_; my $ref = GetFieldDef($table, $field); return unless $ref; # already fixed? #print "0: $$ref[0] 1: $$ref[1] 2: $$ref[2] 3: $$ref[3] 4: $$ref[4]\n"; if ($$ref[1] ne $newname) { print "Updating field $field in table $table ...\n"; my $type = $$ref[1]; $type .= " NOT NULL" if $$ref[3]; $dbh->do("ALTER TABLE $table CHANGE $field $newname $type"); } } sub AddField ($$$) { my ($table, $field, $definition) = @_; my $ref = GetFieldDef($table, $field); return if $ref; # already added? print "Adding new field $field to table $table ...\n"; $dbh->do("ALTER TABLE $table ADD COLUMN $field $definition"); } sub DropField ($$) { my ($table, $field) = @_; my $ref = GetFieldDef($table, $field); return unless $ref; # already dropped? print "Deleting unused field $field from table $table ...\n"; $dbh->do("ALTER TABLE $table DROP COLUMN $field"); } $::regenerateshadow = 0; # really old fields that were added before checksetup.pl existed # but aren't in very old bugzilla's (like 2.1) # Steve Stock (sstock@iconnect-inc.com) AddField('bugs', 'target_milestone', 'varchar(20) not null default "---"'); AddField('bugs', 'groupset', 'bigint not null'); AddField('bugs', 'qa_contact', 'mediumint not null'); AddField('bugs', 'status_whiteboard', 'mediumtext not null'); AddField('products', 'disallownew', 'tinyint not null'); AddField('products', 'milestoneurl', 'tinytext not null'); AddField('components', 'initialqacontact', 'tinytext not null'); AddField('components', 'description', 'mediumtext not null'); ChangeFieldType('components', 'program', 'varchar(64)'); # 1999-05-12 Added a pref to control how much email you get. This needs a new # column in the profiles table, so feed the following to mysql: AddField('profiles', 'emailnotification', 'enum("ExcludeSelfChanges", "CConly", "All") not null default "ExcludeSelfChanges"'); # 1999-06-22 Added an entry to the attachments table to record who the # submitter was. Nothing uses this yet, but it still should be recorded. AddField('attachments', 'submitter_id', 'mediumint not null'); # # One could even populate this field automatically, e.g. with # # unless (GetField('attachments', 'submitter_id') { # AddField ... # populate # } # # For now I was too lazy, so you should read the README :-) # 1999-9-15 Apparently, newer alphas of MySQL won't allow you to have "when" # as a column name. So, I have had to rename a column in the bugs_activity # table. RenameField ('bugs_activity', 'when', 'bug_when'); # 1999-10-11 Restructured voting database to add a cached value in each bug # recording how many total votes that bug has. While I'm at it, I removed # the unused "area" field from the bugs database. It is distressing to # realize that the bugs table has reached the maximum number of indices # allowed by MySQL (16), which may make future enhancements awkward. # (P.S. All is not lost; it appears that the latest betas of MySQL support # a new table format which will allow 32 indices.) DropField('bugs', 'area'); AddField('bugs', 'votes', 'mediumint not null, add index (votes)'); AddField('products', 'votesperuser', 'mediumint not null'); # The product name used to be very different in various tables. # # It was varchar(16) in bugs # tinytext in components # tinytext in products # tinytext in versions # # tinytext is equivalent to varchar(255), which is quite huge, so I change # them all to varchar(64). ChangeFieldType ('bugs', 'product', 'varchar(64)'); ChangeFieldType ('components', 'program', 'varchar(64)'); ChangeFieldType ('products', 'product', 'varchar(64)'); ChangeFieldType ('versions', 'program', 'varchar(64)'); # 2000-01-16 Added a "keywords" field to the bugs table, which # contains a string copy of the entries of the keywords table for this # bug. This is so that I can easily sort and display a keywords # column in bug lists. if (!GetFieldDef('bugs', 'keywords')) { AddField('bugs', 'keywords', 'mediumtext not null'); my @kwords; print "Making sure 'keywords' field of table 'bugs' is empty ...\n"; $dbh->do("UPDATE bugs SET delta_ts = delta_ts, keywords = '' " . "WHERE keywords != ''"); print "Repopulating 'keywords' field of table 'bugs' ...\n"; my $sth = $dbh->prepare("SELECT keywords.bug_id, keyworddefs.name " . "FROM keywords, keyworddefs " . "WHERE keyworddefs.id = keywords.keywordid " . "ORDER BY keywords.bug_id, keyworddefs.name"); $sth->execute; my @list; my $bugid = 0; my @row; while (1) { my ($b, $k) = ($sth->fetchrow_array()); if (!defined $b || $b ne $bugid) { if (@list) { $dbh->do("UPDATE bugs SET delta_ts = delta_ts, keywords = " . $dbh->quote(join(', ', @list)) . " WHERE bug_id = $bugid"); } if (!$b) { last; } $bugid = $b; @list = (); } push(@list, $k); } } # 2000-01-18 Added a "disabledtext" field to the profiles table. If not # empty, then this account has been disabled, and this field is to contain # text describing why. AddField('profiles', 'disabledtext', 'mediumtext not null'); # 2000-01-20 Added a new "longdescs" table, which is supposed to have all the # long descriptions in it, replacing the old long_desc field in the bugs # table. The below hideous code populates this new table with things from # the old field, with ugly parsing and heuristics. sub WriteOneDesc { my ($id, $who, $when, $buffer) = (@_); $buffer = trim($buffer); if ($buffer eq '') { return; } $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES " . "($id, $who, " . time2str("'%Y/%m/%d %H:%M:%S'", $when) . ", " . $dbh->quote($buffer) . ")"); } if (GetFieldDef('bugs', 'long_desc')) { eval("use Date::Parse"); eval("use Date::Format"); my $sth = $dbh->prepare("SELECT count(*) FROM bugs"); $sth->execute(); my ($total) = ($sth->fetchrow_array); print "Populating new long_desc table. This is slow. There are $total\n"; print "bugs to process; a line of dots will be printed for each 50.\n\n"; $| = 1; $dbh->do("LOCK TABLES bugs write, longdescs write, profiles write"); $dbh->do('DELETE FROM longdescs'); $sth = $dbh->prepare("SELECT bug_id, creation_ts, reporter, long_desc " . "FROM bugs ORDER BY bug_id"); $sth->execute(); my $count = 0; while (1) { my ($id, $createtime, $reporterid, $desc) = ($sth->fetchrow_array()); if (!$id) { last; } print "."; $count++; if ($count % 10 == 0) { print " "; if ($count % 50 == 0) { print "$count/$total (" . int($count * 100 / $total) . "%)\n"; } } $desc =~ s/\r//g; my $who = $reporterid; my $when = str2time($createtime); my $buffer = ""; foreach my $line (split(/\n/, $desc)) { $line =~ s/\s+$//g; # Trim trailing whitespace. if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) { my $name = $1; my $date = str2time($2); $date += 59; # Oy, what a hack. The creation time is # accurate to the second. But we the long # text only contains things accurate to the # minute. And so, if someone makes a comment # within a minute of the original bug creation, # then the comment can come *before* the # bug creation. So, we add 59 seconds to # the time of all comments, so that they # are always considered to have happened at # the *end* of the given minute, not the # beginning. if ($date >= $when) { WriteOneDesc($id, $who, $when, $buffer); $buffer = ""; $when = $date; my $s2 = $dbh->prepare("SELECT userid FROM profiles " . "WHERE login_name = " . $dbh->quote($name)); $s2->execute(); ($who) = ($s2->fetchrow_array()); if (!$who) { # This username doesn't exist. Try a special # netscape-only hack (sorry about that, but I don't # think it will hurt any other installations). We # have many entries in the bugsystem from an ancient # world where the "@netscape.com" part of the loginname # was omitted. So, look up the user again with that # appended, and use it if it's there. if ($name !~ /\@/) { my $nsname = $name . "\@netscape.com"; $s2 = $dbh->prepare("SELECT userid FROM profiles " . "WHERE login_name = " . $dbh->quote($nsname)); $s2->execute(); ($who) = ($s2->fetchrow_array()); } } if (!$who) { # This username doesn't exist. Maybe someone renamed # him or something. Invent a new profile entry, # disabled, just to represent him. $dbh->do("INSERT INTO profiles " . "(login_name, password, cryptpassword," . " disabledtext) VALUES (" . $dbh->quote($name) . ", 'okthen', encrypt('okthen'), " . "'Account created only to maintain database integrity')"); $s2 = $dbh->prepare("SELECT LAST_INSERT_ID()"); $s2->execute(); ($who) = ($s2->fetchrow_array()); } next; } else { # print "\nDecided this line of bug $id has a date of " . # time2str("'%Y/%m/%d %H:%M:%S'", $date) . # "\nwhich is less than previous line:\n$line\n\n"; } } $buffer .= $line . "\n"; } WriteOneDesc($id, $who, $when, $buffer); } print "\n\n"; DropField('bugs', 'long_desc'); $dbh->do("UNLOCK TABLES"); $::regenerateshadow = 1; } # 2000-01-18 Added a new table fielddefs that records information about the # different fields we keep an activity log on. The bugs_activity table # now has a pointer into that table instead of recording the name directly. if (GetFieldDef('bugs_activity', 'field')) { AddField('bugs_activity', 'fieldid', 'mediumint not null, ADD INDEX (fieldid)'); print "Populating new fieldid field ...\n"; $dbh->do("LOCK TABLES bugs_activity WRITE, fielddefs WRITE"); my $sth = $dbh->prepare('SELECT DISTINCT field FROM bugs_activity'); $sth->execute(); my %ids; while (my ($f) = ($sth->fetchrow_array())) { my $q = $dbh->quote($f); my $s2 = $dbh->prepare("SELECT fieldid FROM fielddefs WHERE name = $q"); $s2->execute(); my ($id) = ($s2->fetchrow_array()); if (!$id) { $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . "($q, $q)"); $s2 = $dbh->prepare("SELECT LAST_INSERT_ID()"); $s2->execute(); ($id) = ($s2->fetchrow_array()); } $dbh->do("UPDATE bugs_activity SET fieldid = $id WHERE field = $q"); } $dbh->do("UNLOCK TABLES"); DropField('bugs_activity', 'field'); } # 2000-01-18 New email-notification scheme uses a new field in the bug to # record when email notifications were last sent about this bug. Also, # added a user pref whether a user wants to use the brand new experimental # stuff. if (!GetFieldDef('bugs', 'lastdiffed')) { AddField('bugs', 'lastdiffed', 'datetime not null'); $dbh->do('UPDATE bugs SET lastdiffed = now(), delta_ts = delta_ts'); } AddField('profiles', 'newemailtech', 'tinyint not null'); # 2000-01-22 The "login_name" field in the "profiles" table was not # declared to be unique. Sure enough, somehow, I got 22 duplicated entries # in my database. This code detects that, cleans up the duplicates, and # then tweaks the table to declare the field to be unique. What a pain. if (GetIndexDef('profiles', 'login_name')->[1]) { print "Searching for duplicate entries in the profiles table ...\n"; while (1) { # This code is weird in that it loops around and keeps doing this # select again. That's because I'm paranoid about deleting entries # out from under us in the profiles table. Things get weird if # there are *three* or more entries for the same user... $sth = $dbh->prepare("SELECT p1.userid, p2.userid, p1.login_name " . "FROM profiles AS p1, profiles AS p2 " . "WHERE p1.userid < p2.userid " . "AND p1.login_name = p2.login_name " . "ORDER BY p1.login_name"); $sth->execute(); my ($u1, $u2, $n) = ($sth->fetchrow_array); if (!$u1) { last; } print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1 ...\n"; foreach my $i (["bugs", "reporter"], ["bugs", "assigned_to"], ["bugs", "qa_contact"], ["attachments", "submitter_id"], ["bugs_activity", "who"], ["cc", "who"], ["votes", "who"], ["longdescs", "who"]) { my ($table, $field) = (@$i); print " Updating $table.$field ...\n"; my $extra = ""; if ($table eq "bugs") { $extra = ", delta_ts = delta_ts"; } $dbh->do("UPDATE $table SET $field = $u1 $extra " . "WHERE $field = $u2"); } $dbh->do("DELETE FROM profiles WHERE userid = $u2"); } print "OK, changing index type to prevent duplicates in the future ...\n"; $dbh->do("ALTER TABLE profiles DROP INDEX login_name"); $dbh->do("ALTER TABLE profiles ADD UNIQUE (login_name)"); } # 2000-01-24 Added a new field to let people control whether the "My # bugs" link appears at the bottom of each page. Also can control # whether each named query should show up there. AddField('profiles', 'mybugslink', 'tinyint not null default 1'); AddField('namedqueries', 'linkinfooter', 'tinyint not null'); # 2000-02-12 Added a new state to bugs, UNCONFIRMED. Added ability to confirm # a vote via bugs. Added user bits to control which users can confirm bugs # by themselves, and which users can edit bugs without their names on them. # Added a user field which controls which groups a user can put other users # into. my @resolutions = ("", "FIXED", "INVALID", "WONTFIX", "LATER", "REMIND", "DUPLICATE", "WORKSFORME", "MOVED"); CheckEnumField('bugs', 'resolution', @resolutions); my @states = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED", "RESOLVED", "VERIFIED", "CLOSED"); CheckEnumField('bugs', 'bug_status', @states); if (!GetFieldDef('bugs', 'everconfirmed')) { AddField('bugs', 'everconfirmed', 'tinyint not null'); $dbh->do("UPDATE bugs SET everconfirmed = 1, delta_ts = delta_ts"); } AddField('products', 'maxvotesperbug', 'smallint not null default 10000'); AddField('products', 'votestoconfirm', 'smallint not null'); AddField('profiles', 'blessgroupset', 'bigint not null'); # 2000-03-21 Adding a table for target milestones to # database - matthew@zeroknowledge.com $sth = $dbh->prepare("SELECT count(*) from milestones"); $sth->execute(); if (!($sth->fetchrow_arrayref()->[0])) { print "Replacing blank milestones...\n"; $dbh->do("UPDATE bugs SET target_milestone = '---', delta_ts=delta_ts WHERE target_milestone = ' '"); # Populate milestone table with all exisiting values in database $sth = $dbh->prepare("SELECT DISTINCT target_milestone, product FROM bugs"); $sth->execute(); print "Populating milestones table...\n"; my $value; my $product; while(($value, $product) = $sth->fetchrow_array()) { # check if the value already exists my $sortkey = substr($value, 1); if ($sortkey !~ /^\d+$/) { $sortkey = 0; } else { $sortkey *= 10; } $value = $dbh->quote($value); $product = $dbh->quote($product); my $s2 = $dbh->prepare("SELECT value FROM milestones WHERE value = $value AND product = $product"); $s2->execute(); if(!$s2->fetchrow_array()) { $dbh->do("INSERT INTO milestones(value, product, sortkey) VALUES($value, $product, $sortkey)"); } } } # 2000-03-22 Changed the default value for target_milestone to be "---" # (which is still not quite correct, but much better than what it was # doing), and made the size of the value field in the milestones table match # the size of the target_milestone field in the bugs table. ChangeFieldType('bugs', 'target_milestone', 'varchar(20) default "---"'); ChangeFieldType('milestones', 'value', 'varchar(20)'); # 2000-03-23 Added a defaultmilestone field to the products table, so that # we know which milestone to initially assign bugs to. if (!GetFieldDef('products', 'defaultmilestone')) { AddField('products', 'defaultmilestone', 'varchar(20) not null default "---"'); $sth = $dbh->prepare("SELECT product, defaultmilestone FROM products"); $sth->execute(); while (my ($product, $defaultmilestone) = $sth->fetchrow_array()) { $product = $dbh->quote($product); $defaultmilestone = $dbh->quote($defaultmilestone); my $s2 = $dbh->prepare("SELECT value FROM milestones " . "WHERE value = $defaultmilestone " . "AND product = $product"); $s2->execute(); if (!$s2->fetchrow_array()) { $dbh->do("INSERT INTO milestones(value, product) " . "VALUES ($defaultmilestone, $product)"); } } } # 2000-03-24 Added unique indexes into the cc and keyword tables. This # prevents certain database inconsistencies, and, moreover, is required for # new generalized list code to work. if ( CountIndexes('cc') != 3 ) { # XXX should eliminate duplicate entries before altering # print "Recreating indexes on cc table.\n"; DropIndexes('cc'); $dbh->do("ALTER TABLE cc ADD UNIQUE (bug_id,who)"); $dbh->do("ALTER TABLE cc ADD INDEX (who)"); $::regenerateshadow=1; # cc fields no longer have spaces in them } if ( CountIndexes('keywords') != 3 ) { # XXX should eliminate duplicate entries before altering # print "Recreating indexes on keywords table.\n"; DropIndexes('keywords'); $dbh->do("ALTER TABLE keywords ADD INDEX (keywordid)"); $dbh->do("ALTER TABLE keywords ADD UNIQUE (bug_id,keywordid)"); } # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a better # way than it used to. This code searches the comments to populate the table # initially. It's executed if the table is empty; if it's empty because there # are no dupes (as opposed to having just created the table) it won't have # any effect anyway, so it doesn't matter. $sth = $dbh->prepare("SELECT count(*) from duplicates"); $sth->execute(); if (!($sth->fetchrow_arrayref()->[0])) { # populate table print("Populating duplicates table...\n"); $sth = $dbh->prepare("SELECT longdescs.bug_id, thetext FROM longdescs left JOIN bugs using(bug_id) WHERE (thetext " . "regexp 'This bug has been marked as a duplicate of') AND (resolution = 'DUPLICATE') ORDER" . " BY longdescs.bug_when"); $sth->execute(); my %dupes; my $key; # Because of the way hashes work, this loop removes all but the last dupe # resolution found for a given bug. while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { $dupes{$dupe} = $dupe_of; } foreach $key (keys(%dupes)) { $dupes{$key} =~ s/.*This bug has been marked as a duplicate of (\d{1,5}).*/$1/; $dbh->do("INSERT INTO duplicates VALUES('$dupes{$key}', '$key')"); # BugItsADupeOf Dupe } $::regenerateshadow = 1; } # 2000-12-14 New graphing system requires a directory to put the graphs in # How do we make the new directory owned by the webserver's group? Until # we find out, make it 0777. unless (-d 'graphs') { print "Creating graphs directory ...\n"; mkdir 'graphs', 0777; # was 0770 in the code (above) I pinched this from if ($::webservergroup eq "") { chmod 0777, 'graphs'; } } # # If you had to change the --TABLE-- definition in any way, then add your # differential change code *** A B O V E *** this comment. # # That is: if you add a new field, you first search for the first occurence # of --TABLE-- and add your field to into the table hash. This new setting # would be honored for every new installation. Then add your # AddField/DropField/ChangeFieldType/RenameField code above. This would then # be honored by everyone who updates his Bugzilla installation. # # # Final checks... if ($::regenerateshadow) { print "Now regenerating the shadow database for all bugs.\n"; system("./processmail", "regenerate"); } unlink "data/versioncache"; print "Reminder: Bugzilla now requires version 3.22.5 or later of MySQL.\n"; print "Reminder: Bugzilla now requires version 8.7 or later of sendmail.\n";