label.c revision 8881
1/*
2 * The new sysinstall program.
3 *
4 * This is probably the last program in the `sysinstall' line - the next
5 * generation being essentially a complete rewrite.
6 *
7 * $Id: label.c,v 1.30 1995/05/29 00:50:03 jkh Exp $
8 *
9 * Copyright (c) 1995
10 *	Jordan Hubbard.  All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 * 1. Redistributions of source code must retain the above copyright
16 *    notice, this list of conditions and the following disclaimer,
17 *    verbatim and that no modifications are made prior to this
18 *    point in the file.
19 * 2. Redistributions in binary form must reproduce the above copyright
20 *    notice, this list of conditions and the following disclaimer in the
21 *    documentation and/or other materials provided with the distribution.
22 * 3. All advertising materials mentioning features or use of this software
23 *    must display the following acknowledgement:
24 *	This product includes software developed by Jordan Hubbard
25 *	for the FreeBSD Project.
26 * 4. The name of Jordan Hubbard or the FreeBSD project may not be used to
27 *    endorse or promote products derived from this software without specific
28 *    prior written permission.
29 *
30 * THIS SOFTWARE IS PROVIDED BY JORDAN HUBBARD ``AS IS'' AND
31 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED.  IN NO EVENT SHALL JORDAN HUBBARD OR HIS PETS BE LIABLE
34 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
35 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
36 * OR SERVICES; LOSS OF USE, DATA, LIFE OR PROFITS; OR BUSINESS INTERRUPTION)
37 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
38 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
39 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
40 * SUCH DAMAGE.
41 *
42 */
43
44#include "sysinstall.h"
45#include <ctype.h>
46#include <sys/disklabel.h>
47
48/*
49 * Everything to do with editing the contents of disk labels.
50 */
51
52/* A nice message we use a lot in the disklabel editor */
53#define MSG_NOT_APPLICABLE	"That option is not applicable here"
54
55/* Where to start printing the freebsd slices */
56#define CHUNK_SLICE_START_ROW		2
57#define CHUNK_PART_START_ROW		11
58
59/* One MB worth of blocks */
60#define ONE_MEG				2048
61
62/* The smallest filesystem we're willing to create */
63#define FS_MIN_SIZE			ONE_MEG
64
65/* The smallest root filesystem we're willing to create */
66#define ROOT_MIN_SIZE			(20 * ONE_MEG)
67
68/* All the chunks currently displayed on the screen */
69static struct {
70    struct chunk *c;
71    PartType type;
72} label_chunk_info[MAX_CHUNKS + 1];
73static int here;
74
75/* See if we're already using a desired partition name */
76static Boolean
77check_conflict(char *name)
78{
79    int i;
80
81    for (i = 0; label_chunk_info[i].c; i++)
82	if (label_chunk_info[i].type == PART_FILESYSTEM && label_chunk_info[i].c->private
83	    && !strcmp(((PartInfo *)label_chunk_info[i].c->private)->mountpoint, name))
84	    return TRUE;
85    return FALSE;
86}
87
88/* How much space is in this FreeBSD slice? */
89static int
90space_free(struct chunk *c)
91{
92    struct chunk *c1 = c->part;
93    int sz = c->size;
94
95    while (c1) {
96	if (c1->type != unused)
97	    sz -= c1->size;
98	c1 = c1->next;
99    }
100    if (sz < 0)
101	msgFatal("Partitions are larger than actual chunk??");
102    return sz;
103}
104
105/* Snapshot the current situation into the displayed chunks structure */
106static void
107record_label_chunks()
108{
109    int i, j, p;
110    struct chunk *c1, *c2;
111    Device **devs;
112    Disk *d;
113
114    devs = deviceFind(NULL, DEVICE_TYPE_DISK);
115    if (!devs) {
116	msgConfirm("No disks found!");
117	return;
118    }
119
120    j = p = 0;
121    /* First buzz through and pick up the FreeBSD slices */
122    for (i = 0; devs[i]; i++) {
123	if (!devs[i]->enabled)
124	    continue;
125	d = (Disk *)devs[i]->private;
126	if (!d->chunks)
127	    msgFatal("No chunk list found for %s!", d->name);
128
129	/* Put the slice entries first */
130	for (c1 = d->chunks->part; c1; c1 = c1->next) {
131	    if (c1->type == freebsd) {
132		label_chunk_info[j].type = PART_SLICE;
133		label_chunk_info[j].c = c1;
134		++j;
135	    }
136	}
137    }
138    /* Now run through again and get the FreeBSD partition entries */
139    for (i = 0; devs[i]; i++) {
140	if (!devs[i]->enabled)
141	    continue;
142	d = (Disk *)devs[i]->private;
143	/* Then buzz through and pick up the partitions */
144	for (c1 = d->chunks->part; c1; c1 = c1->next) {
145	    if (c1->type == freebsd) {
146		for (c2 = c1->part; c2; c2 = c2->next) {
147		    if (c2->type == part) {
148			if (c2->subtype == FS_SWAP)
149			    label_chunk_info[j].type = PART_SWAP;
150			else
151			    label_chunk_info[j].type = PART_FILESYSTEM;
152			label_chunk_info[j].c = c2;
153			++j;
154		    }
155		}
156	    }
157	    else if (c1->type == fat) {
158		label_chunk_info[j].type = PART_FAT;
159		label_chunk_info[j].c = c1;
160		++j;
161	    }
162	}
163    }
164    label_chunk_info[j].c = NULL;
165    if (here >= j)
166	here = j  ? j - 1 : 0;
167}
168
169/* A new partition entry */
170static PartInfo *
171new_part(char *mpoint, Boolean newfs, u_long size)
172{
173    PartInfo *ret;
174    u_long target,divisor;
175
176    ret = (PartInfo *)safe_malloc(sizeof(PartInfo));
177    strncpy(ret->mountpoint, mpoint, FILENAME_MAX);
178    strcpy(ret->newfs_cmd, "newfs");
179    ret->newfs = newfs;
180    if (!size)
181	    return ret;
182    for (target = size; target; target--) {
183	for (divisor = 4096 ; divisor > 1023; divisor--) {
184	    if (!(target % divisor)) {
185		sprintf(ret->newfs_cmd + strlen(ret->newfs_cmd), " -u %ld",divisor);
186		return ret;
187	    }
188	}
189    }
190    return ret;
191}
192
193/* Get the mountpoint for a partition and save it away */
194PartInfo *
195get_mountpoint(struct chunk *old)
196{
197    char *val;
198    PartInfo *tmp;
199
200    if (old && old->private)
201	tmp = old->private;
202    else
203	tmp = NULL;
204    val = msgGetInput(tmp ? tmp->mountpoint : NULL, "Please specify a mount point for the partition");
205    if (!val || !*val) {
206	if (!old)
207	    return NULL;
208	else {
209	    free(old->private);
210	    old->private = NULL;
211	}
212	return NULL;
213    }
214
215    /* Is it just the same value? */
216    if (tmp && !strcmp(tmp->mountpoint, val))
217	return NULL;
218
219    /* Did we use it already? */
220    if (check_conflict(val)) {
221	msgConfirm("You already have a mount point for %s assigned!", val);
222	return NULL;
223    }
224
225    /* Is it bogus? */
226    if (*val != '/') {
227	msgConfirm("Mount point must start with a / character");
228	return NULL;
229    }
230
231    /* Is it going to be mounted on root? */
232    if (!strcmp(val, "/")) {
233	if (old)
234	    old->flags |= CHUNK_IS_ROOT;
235    }
236    else if (old)
237	old->flags &= ~CHUNK_IS_ROOT;
238
239    safe_free(tmp);
240    tmp = new_part(val, TRUE, 0);
241    if (old) {
242	old->private = tmp;
243	old->private_free = safe_free;
244    }
245    return tmp;
246}
247
248/* Get the type of the new partiton */
249static PartType
250get_partition_type(void)
251{
252    char selection[20];
253    int i;
254
255    static unsigned char *fs_types[] = {
256	"FS",
257	"A file system",
258	"Swap",
259	"A swap partition.",
260    };
261    i = dialog_menu("Please choose a partition type",
262		    "If you want to use this partition for swap space, select Swap.\nIf you want to put a filesystem on it, choose FS.", -1, -1, 2, 2, fs_types, selection, NULL, NULL);
263    if (!i) {
264	if (!strcmp(selection, "FS"))
265	    return PART_FILESYSTEM;
266	else if (!strcmp(selection, "Swap"))
267	    return PART_SWAP;
268    }
269    return PART_NONE;
270}
271
272/* If the user wants a special newfs command for this, set it */
273static void
274getNewfsCmd(PartInfo *p)
275{
276    char *val;
277
278    val = msgGetInput(p->newfs_cmd,
279		      "Please enter the newfs command and options you'd like to use in\ncreating this file system.");
280    if (val)
281	strncpy(p->newfs_cmd, val, NEWFS_CMD_MAX);
282}
283
284
285#define MAX_MOUNT_NAME	12
286
287#define PART_PART_COL	0
288#define PART_MOUNT_COL	8
289#define PART_SIZE_COL	(PART_MOUNT_COL + MAX_MOUNT_NAME + 3)
290#define PART_NEWFS_COL	(PART_SIZE_COL + 7)
291#define PART_OFF	38
292
293/* How many mounted partitions to display in column before going to next */
294#define CHUNK_COLUMN_MAX	5
295
296/* stick this all up on the screen */
297static void
298print_label_chunks(void)
299{
300    int i, j, srow, prow, pcol;
301    int sz;
302
303    attrset(A_REVERSE);
304    mvaddstr(0, 25, "FreeBSD Disklabel Editor");
305    clrtobot();
306    attrset(A_NORMAL);
307
308    for (i = 0; i < 2; i++) {
309	mvaddstr(CHUNK_PART_START_ROW - 2, PART_PART_COL + (i * PART_OFF), "Part");
310	mvaddstr(CHUNK_PART_START_ROW - 1, PART_PART_COL + (i * PART_OFF), "----");
311
312	mvaddstr(CHUNK_PART_START_ROW - 2, PART_MOUNT_COL + (i * PART_OFF), "Mount");
313	mvaddstr(CHUNK_PART_START_ROW - 1, PART_MOUNT_COL + (i * PART_OFF), "-----");
314
315	mvaddstr(CHUNK_PART_START_ROW - 2, PART_SIZE_COL + (i * PART_OFF) + 2, "Size");
316	mvaddstr(CHUNK_PART_START_ROW - 1, PART_SIZE_COL + (i * PART_OFF) + 2, "----");
317
318	mvaddstr(CHUNK_PART_START_ROW - 2, PART_NEWFS_COL + (i * PART_OFF), "Newfs");
319	mvaddstr(CHUNK_PART_START_ROW - 1, PART_NEWFS_COL + (i * PART_OFF), "-----");
320    }
321    srow = CHUNK_SLICE_START_ROW;
322    prow = CHUNK_PART_START_ROW;
323    pcol = 0;
324
325    for (i = 0; label_chunk_info[i].c; i++) {
326	if (i == here)
327	    attrset(A_REVERSE);
328	/* Is it a slice entry displayed at the top? */
329	if (label_chunk_info[i].type == PART_SLICE) {
330	    sz = space_free(label_chunk_info[i].c);
331	    mvprintw(srow++, 0, "Disk: %s\tPartition name: %s\tFree: %d blocks (%dMB)",
332		     label_chunk_info[i].c->disk->name, label_chunk_info[i].c->name, sz, (sz / ONE_MEG));
333	}
334	/* Otherwise it's a DOS, swap or filesystem entry, at the bottom */
335	else {
336	    char onestr[PART_OFF], num[10], *mountpoint, *newfs;
337
338	    /*
339	     * We copy this into a blank-padded string so that it looks like
340	     * a solid bar in reverse-video
341	     */
342	    memset(onestr, ' ', PART_OFF - 1);
343	    onestr[PART_OFF - 1] = '\0';
344	    /* Go for two columns */
345	    if (prow == (CHUNK_PART_START_ROW + CHUNK_COLUMN_MAX)) {
346		pcol = PART_OFF;
347		prow = CHUNK_PART_START_ROW;
348	    }
349	    memcpy(onestr + PART_PART_COL, label_chunk_info[i].c->name, strlen(label_chunk_info[i].c->name));
350	    /* If it's a filesystem, display the mountpoint */
351	    if (label_chunk_info[i].c->private
352		&& (label_chunk_info[i].type == PART_FILESYSTEM || label_chunk_info[i].type == PART_FAT)) {
353		mountpoint = ((PartInfo *)label_chunk_info[i].c->private)->mountpoint;
354		if (label_chunk_info[i].type == PART_FAT)
355		    newfs = "DOS";
356		else
357		    newfs = ((PartInfo *)label_chunk_info[i].c->private)->newfs ? "Y" : "N";
358	    }
359	    else if (label_chunk_info[i].type == PART_SWAP) {
360		mountpoint = "swap";
361		newfs = " ";
362	    }
363	    else {
364		mountpoint = "<NONE>";
365		newfs = "*";
366	    }
367	    for (j = 0; j < MAX_MOUNT_NAME && mountpoint[j]; j++)
368		onestr[PART_MOUNT_COL + j] = mountpoint[j];
369	    snprintf(num, 10, "%4ldMB", label_chunk_info[i].c->size ? label_chunk_info[i].c->size / ONE_MEG : 0);
370	    memcpy(onestr + PART_SIZE_COL, num, strlen(num));
371	    memcpy(onestr + PART_NEWFS_COL, newfs, strlen(newfs));
372	    onestr[PART_NEWFS_COL + strlen(newfs)] = '\0';
373	    mvaddstr(prow, pcol, onestr);
374	    ++prow;
375	}
376	if (i == here)
377	    attrset(A_NORMAL);
378    }
379}
380
381static void
382print_command_summary()
383{
384    mvprintw(17, 0, "The following commands are valid here (upper or lower case):");
385    mvprintw(19, 0, "C = Create New     D = Delete         M = Set Mountpoint");
386    mvprintw(20, 0, "N = Newfs Options  T = Toggle Newfs   U = Undo    ESC = Exit");
387    mvprintw(21, 0, "The default target will be displayed in ");
388
389    attrset(A_REVERSE);
390    addstr("reverse");
391    attrset(A_NORMAL);
392    addstr(" video.");
393    mvprintw(22, 0, "Use F1 or ? to get more help, arrow keys to move.");
394    move(0, 0);
395}
396
397int
398diskLabelEditor(char *str)
399{
400    int sz, i, key = 0;
401    Boolean labeling;
402    char *msg = NULL;
403    PartInfo *p;
404    PartType type;
405    Device **devs;
406
407    labeling = TRUE;
408    keypad(stdscr, TRUE);
409    record_label_chunks();
410
411    if (!getenv(DISK_PARTITIONED)) {
412	msgConfirm("You need to partition your disk(s) before you can assign disk labels.");
413	return 0;
414    }
415    dialog_clear(); clear();
416    while (labeling) {
417	clear();
418	print_label_chunks();
419	print_command_summary();
420	if (msg) {
421	    attrset(A_REVERSE); mvprintw(23, 0, msg); attrset(A_NORMAL);
422	    beep();
423	    msg = NULL;
424	}
425	refresh();
426	key = toupper(getch());
427	switch (key) {
428
429	case '\014':	/* ^L */
430	    continue;
431
432	case KEY_UP:
433	case '-':
434	    if (here != 0)
435		--here;
436	    else
437		while (label_chunk_info[here + 1].c)
438		    ++here;
439	    break;
440
441	case KEY_DOWN:
442	case '+':
443	case '\r':
444	case '\n':
445	    if (label_chunk_info[here + 1].c)
446		++here;
447	    else
448		here = 0;
449	    break;
450
451	case KEY_HOME:
452	    here = 0;
453	    break;
454
455	case KEY_END:
456	    while (label_chunk_info[here + 1].c)
457		++here;
458	    break;
459
460	case KEY_F(1):
461	case '?':
462	    systemDisplayFile("disklabel.hlp");
463	    break;
464
465	case 'C':
466	    if (label_chunk_info[here].type != PART_SLICE) {
467		msg = "You can only do this in a master partition (see top of screen)";
468		break;
469	    }
470	    else {
471		int i, cnt;
472
473		cnt = i = 0;
474		while (label_chunk_info[i].c)
475		    if (label_chunk_info[i++].type != PART_SLICE)
476			cnt++;
477		if (cnt == (CHUNK_COLUMN_MAX * 2)) {
478		    msgConfirm("Sorry, I can't fit any more partitions on the screen!  You can get around\nthis limitation by partitioning your disks individually rather than all\nat once.  This will be fixed just as soon as we get a scrolling partition\nbox written.  Sorry for the inconvenience!");
479		    break;
480		}
481	    }
482	    sz = space_free(label_chunk_info[here].c);
483	    if (sz <= FS_MIN_SIZE) {
484		msg = "Not enough space to create additional FreeBSD partition";
485		break;
486	    }
487	    {
488		char *val, *cp;
489		int size;
490		struct chunk *tmp;
491		char osize[80];
492		u_long flags = 0;
493
494		sprintf(osize, "%d", sz);
495		val = msgGetInput(osize, "Please specify the size for new FreeBSD partition in blocks, or\nappend a trailing `M' for megabytes (e.g. 20M) or `C' for cylinders.\n\nSpace free is %d blocks (%dMB)", sz, sz / ONE_MEG);
496		if (!val || (size = strtol(val, &cp, 0)) <= 0)
497		    break;
498
499		if (*cp) {
500		    if (toupper(*cp) == 'M')
501			size *= ONE_MEG;
502		    else if (toupper(*cp) == 'C')
503			size *= (label_chunk_info[here].c->disk->bios_hd * label_chunk_info[here].c->disk->bios_sect);
504		}
505		if (size <= FS_MIN_SIZE) {
506		    msgConfirm("The minimum filesystem size is %dMB", FS_MIN_SIZE / ONE_MEG);
507		    break;
508		}
509		type = get_partition_type();
510		if (type == PART_NONE)
511		    break;
512
513		if (type == PART_FILESYSTEM) {
514		    if ((p = get_mountpoint(NULL)) == NULL)
515			break;
516		    else if (!strcmp(p->mountpoint, "/"))
517			flags |= CHUNK_IS_ROOT;
518		    else
519			flags &= ~CHUNK_IS_ROOT;
520		} else
521		    p = NULL;
522
523		if ((flags & CHUNK_IS_ROOT)) {
524		    if (!(label_chunk_info[here].c->flags & CHUNK_BSD_COMPAT)) {
525			msgConfirm("This region cannot be used for your root partition as\nthe FreeBSD boot code cannot deal with a root partition created in\nsuch a location.  Please choose another location for your root\npartition and try again!");
526			break;
527		    }
528		    if (size < ROOT_MIN_SIZE)
529			msgConfirm("Warning: This is smaller than the recommended size for a\nroot partition.  For a variety of reasons, root\npartitions should usually be at least %dMB in size", ROOT_MIN_SIZE / ONE_MEG);
530		}
531		tmp = Create_Chunk_DWIM(label_chunk_info[here].c->disk,
532					label_chunk_info[here].c,
533					size, part,
534					(type == PART_SWAP) ? FS_SWAP : FS_BSDFFS,
535					flags);
536		if (!tmp) {
537		    msgConfirm("Unable to create the partition. Too big?");
538		    break;
539		}
540		if ((flags & CHUNK_IS_ROOT) && (tmp->flags & CHUNK_PAST_1024)) {
541		    msgConfirm("This region cannot be used for your root partition as it starts\nor extends past the 1024'th cylinder mark and is thus a\npoor location to boot from.  Please choose another\nlocation for your root partition and try again!");
542		    Delete_Chunk(label_chunk_info[here].c->disk, tmp);
543		    break;
544		}
545		if (type != PART_SWAP) {
546		    /* This is needed to tell the newfs -u about the size */
547		    tmp->private = new_part(p->mountpoint, p->newfs, tmp->size);
548		    tmp->private_free = safe_free;
549		    safe_free(p);
550		} else {
551		    tmp->private = p;
552		}
553		tmp->private_free = safe_free;
554		record_label_chunks();
555	    }
556	    break;
557
558	case 'D':	/* delete */
559	    if (label_chunk_info[here].type == PART_SLICE) {
560		msg = MSG_NOT_APPLICABLE;
561		break;
562	    }
563	    else if (label_chunk_info[here].type == PART_FAT) {
564		msg = "Use the Disk Partition Editor to delete DOS partitions";
565		break;
566	    }
567	    Delete_Chunk(label_chunk_info[here].c->disk, label_chunk_info[here].c);
568	    record_label_chunks();
569	    break;
570
571	case 'M':	/* mount */
572	    switch(label_chunk_info[here].type) {
573	    case PART_SLICE:
574		msg = MSG_NOT_APPLICABLE;
575		break;
576
577	    case PART_SWAP:
578		msg = "You don't need to specify a mountpoint for a swap partition.";
579		break;
580
581	    case PART_FAT:
582	    case PART_FILESYSTEM:
583		p = get_mountpoint(label_chunk_info[here].c);
584		if (p) {
585		    p->newfs = FALSE;
586		    if (label_chunk_info[here].type == PART_FAT
587			&& (!strcmp(p->mountpoint, "/") || !strcmp(p->mountpoint, "/usr")
588			    || !strcmp(p->mountpoint, "/var"))) {
589			msgConfirm("%s is an invalid mount point for a DOS partition!", p->mountpoint);
590			strcpy(p->mountpoint, "/bogus");
591		    }
592		}
593		record_label_chunks();
594		break;
595
596	    default:
597		msgFatal("Bogus partition under cursor???");
598		break;
599	    }
600	    break;
601
602	case 'N':	/* Set newfs options */
603	    if (label_chunk_info[here].c->private &&
604		((PartInfo *)label_chunk_info[here].c->private)->newfs)
605		getNewfsCmd(label_chunk_info[here].c->private);
606	    else
607		msg = MSG_NOT_APPLICABLE;
608	    break;
609
610	case 'T':	/* Toggle newfs state */
611	    if (label_chunk_info[here].type == PART_FILESYSTEM &&
612		label_chunk_info[here].c->private) {
613		    PartInfo *pi = ((PartInfo *)label_chunk_info[here].c->private);
614		    label_chunk_info[here].c->private = new_part(pi->mountpoint, !pi->newfs, label_chunk_info[here].c->size);
615		    safe_free(pi);
616		    label_chunk_info[here].c->private_free = safe_free;
617		}
618	    else
619		msg = MSG_NOT_APPLICABLE;
620	    break;
621
622	case 'U':
623	    devs = deviceFind(NULL, DEVICE_TYPE_DISK);
624	    for (i = 0; devs[i]; i++) {
625		if (!devs[i]->enabled)
626		    continue;
627		else {
628		    char *cp = devs[i]->name;
629
630		    Free_Disk(devs[i]->private);
631		    devs[i]->private = Open_Disk(cp);
632		}
633	    }
634	    record_label_chunks();
635	    break;
636
637	case 'W':
638	    if (!msgYesNo("Are you sure you want to go into Wizard mode?\n\nThis is an entirely undocumented feature which you are not\nexpected to understand!")) {
639		int i;
640		Device **devs;
641
642		dialog_clear();
643		end_dialog();
644		DialogActive = FALSE;
645		devs = deviceFind(NULL, DEVICE_TYPE_DISK);
646		if (!devs) {
647		    msgConfirm("Can't find any disk devicse!");
648		    break;
649		}
650		for (i = 0; devs[i] && ((Disk *)devs[i]->private); i++) {
651		    if (devs[i]->enabled)
652		    	slice_wizard(((Disk *)devs[i]->private));
653		}
654		DialogActive = TRUE;
655		dialog_clear();
656		record_label_chunks();
657	    }
658	    else
659		msg = "A most prudent choice!";
660	    break;
661
662	case 27:	/* ESC */
663	    labeling = FALSE;
664	    break;
665
666	default:
667	    beep();
668	    msg = "Type F1 or ? for help";
669	    break;
670	}
671    }
672    variable_set2(DISK_LABELLED, "yes");
673    dialog_clear();
674    return 0;
675}
676
677
678
679