pull-prototypes-to-head-20020821
[openafs.git] / src / budb / db_dump.c
1 /*
2  * Copyright 2000, International Business Machines Corporation and others.
3  * All Rights Reserved.
4  * 
5  * This software has been released under the terms of the IBM Public
6  * License.  For details, see the LICENSE file in the top-level source
7  * directory or online at http://www.openafs.org/dl/license10.html
8  */
9
10 /* dump the database
11  * Dump is made to a local file. Structures are dumped in network byte order
12  * for transportability between hosts
13  */
14
15 #include <afsconfig.h>
16 #include <afs/param.h>
17
18 RCSID("$Header$");
19
20 #ifdef AFS_NT40_ENV
21 #include <winsock2.h>
22 #else
23 #include <netinet/in.h>
24 #include <sys/param.h>
25 #endif
26 #include <afs/stds.h>
27 #include <sys/types.h>
28 #include <ubik.h>
29 #include <lock.h>
30
31 #ifdef HAVE_STRING_H
32 #include <string.h>
33 #else
34 #ifdef HAVE_STRINGS_H
35 #include <strings.h>
36 #endif
37 #endif
38
39 #include "database.h"
40 #include "budb.h"
41 #include "globals.h"
42 #include "error_macros.h"
43 #include "budb_errs.h"
44 #include "afs/audit.h"
45
46
47 /* dump ubik database - routines to scan the database and dump all
48  *      the information
49  */
50
51 /* -----------------------
52  * synchronization on pipe
53  * -----------------------
54  */
55
56 /* interlocking for database dump */
57
58 dumpSyncT       dumpSync;
59 dumpSyncP       dumpSyncPtr = &dumpSync;
60
61
62 /* canWrite
63  *      check if we should dump more of the database. Waits for the reader
64  *      to drain the information before allowing the writer to proceed.
65  * exit:
66  *      1 - ok to write
67  */
68
69 afs_int32
70 canWrite(fid)
71      int fid;
72 {
73     afs_int32 code = 0;
74     extern dumpSyncP dumpSyncPtr;
75
76     ObtainWriteLock(&dumpSyncPtr->ds_lock);
77
78     /* let the pipe drain */
79     while ( dumpSyncPtr->ds_bytes > 0 )
80     {
81         if ( dumpSyncPtr->ds_readerStatus == DS_WAITING )
82         {
83             dumpSyncPtr->ds_readerStatus = 0;
84             code = LWP_SignalProcess(&dumpSyncPtr->ds_readerStatus);
85             if ( code )
86                 LogError(code, "canWrite: Signal delivery failed\n");
87         }
88         dumpSyncPtr->ds_writerStatus = DS_WAITING;
89         ReleaseWriteLock(&dumpSyncPtr->ds_lock);
90         LWP_WaitProcess(&dumpSyncPtr->ds_writerStatus);
91         ObtainWriteLock(&dumpSyncPtr->ds_lock);
92     }
93     return(1);
94 }
95
96
97 /* haveWritten
98  *      record the fact that nbytes have been written. Signal the reader
99  *      to proceed, and unlock.
100  * exit:
101  *      no return value
102  */
103
104 haveWritten(nbytes)
105      afs_int32 nbytes;
106 {
107     afs_int32 code = 0;
108     extern dumpSyncP dumpSyncPtr;
109
110     dumpSyncPtr->ds_bytes += nbytes;
111     if ( dumpSyncPtr->ds_readerStatus == DS_WAITING )
112     {
113         dumpSyncPtr->ds_readerStatus = 0;
114         code = LWP_SignalProcess(&dumpSyncPtr->ds_readerStatus);
115         if ( code )
116             LogError(code, "haveWritten: Signal delivery failed\n");
117     }
118     ReleaseWriteLock(&dumpSyncPtr->ds_lock);
119 }
120
121 /* doneWriting
122  *      wait for the reader to drain all the information, and then set the
123  *      done flag.
124  */
125
126 doneWriting(error)
127     afs_int32 error;
128 {
129     afs_int32 code = 0;
130
131     /* wait for the reader */
132     ObtainWriteLock(&dumpSyncPtr->ds_lock);
133     while ( dumpSyncPtr->ds_readerStatus != DS_WAITING )
134     {
135         LogDebug(4, "doneWriting: waiting for Reader\n");
136         dumpSyncPtr->ds_writerStatus = DS_WAITING;
137         ReleaseWriteLock(&dumpSyncPtr->ds_lock);
138         LWP_WaitProcess(&dumpSyncPtr->ds_writerStatus);
139         ObtainWriteLock(&dumpSyncPtr->ds_lock);
140     }
141
142     LogDebug(4, "doneWriting: setting done\n");
143
144     /* signal that we are done */
145     if (error) dumpSyncPtr->ds_writerStatus = DS_DONE_ERROR;
146     else       dumpSyncPtr->ds_writerStatus = DS_DONE;
147     dumpSyncPtr->ds_readerStatus = 0;
148     code = LWP_NoYieldSignal(&dumpSyncPtr->ds_readerStatus);
149     if ( code )
150         LogError(code, "doneWriting: Signal delivery failed\n");
151     ReleaseWriteLock(&dumpSyncPtr->ds_lock);
152 }
153  
154 /* notes:
155  *      ut - setup and pass down
156  */
157
158 /* writeStructHeader
159  *      write header appropriate for requested structure type
160  */
161
162 afs_int32
163 writeStructHeader(fid, type)
164      int fid;
165      afs_int32 type;
166 {
167     struct structDumpHeader hostDumpHeader, netDumpHeader;
168
169     hostDumpHeader.type = type;
170     hostDumpHeader.structversion = 1;
171
172     
173     switch ( type )
174     {
175       case SD_DBHEADER:
176         hostDumpHeader.size = sizeof(struct DbHeader);
177         break;
178
179       case SD_DUMP:
180         hostDumpHeader.size = sizeof(struct budb_dumpEntry);
181         break;
182
183       case SD_TAPE:
184         hostDumpHeader.size = sizeof(struct budb_tapeEntry);
185         break;
186
187       case SD_VOLUME:
188         hostDumpHeader.size = sizeof(struct budb_volumeEntry);
189         break;
190
191       case SD_END:
192         hostDumpHeader.size = 0;
193         break;
194
195       default:
196         LogError(0, "writeStructHeader: invalid type %d\n", type);
197         BUDB_EXIT(1);
198     }
199
200     structDumpHeader_hton(&hostDumpHeader, &netDumpHeader);
201
202     if ( canWrite(fid) <= 0 )
203         return(BUDB_DUMPFAILED);
204     if ( write(fid, &netDumpHeader, sizeof(netDumpHeader)) !=
205                 sizeof(netDumpHeader) )
206         return(BUDB_DUMPFAILED);
207     haveWritten(sizeof(netDumpHeader));
208     
209     return(0);
210 }
211
212 /* writeTextHeader
213  *      write header appropriate for requested structure type
214  */
215
216 afs_int32
217 writeTextHeader(fid, type)
218      int fid;
219      afs_int32 type;
220 {
221     struct structDumpHeader hostDumpHeader, netDumpHeader;
222     
223     hostDumpHeader.structversion = 1;
224
225     switch ( type )
226     {   
227       case TB_DUMPSCHEDULE:
228         hostDumpHeader.type = SD_TEXT_DUMPSCHEDULE;
229         break;
230
231       case TB_VOLUMESET:
232         hostDumpHeader.type = SD_TEXT_VOLUMESET;
233         break;
234
235       case TB_TAPEHOSTS:
236         hostDumpHeader.type = SD_TEXT_TAPEHOSTS;
237         break;
238
239       default:
240         LogError(0, "writeTextHeader: invalid type %d\n", type);
241         BUDB_EXIT(1);
242     }
243
244     hostDumpHeader.size = ntohl(db.h.textBlock[type].size);
245     structDumpHeader_hton(&hostDumpHeader, &netDumpHeader);
246
247     if (canWrite(fid) <= 0)
248         return(BUDB_DUMPFAILED);
249
250     if (write(fid, &netDumpHeader, sizeof(netDumpHeader)) != sizeof(netDumpHeader))
251         return(BUDB_DUMPFAILED);
252
253     haveWritten(sizeof(netDumpHeader));
254     
255     return(0);
256 }
257
258 afs_int32
259 writeDbHeader(fid)
260      int fid;
261 {
262     struct DbHeader header;
263     afs_int32 curtime;
264     afs_int32 code = 0, tcode;
265
266     extern struct memoryDB db;
267
268     /* check the memory database header for integrity */
269     if ( db.h.version != db.h.checkVersion )
270         ERROR(BUDB_DATABASEINCONSISTENT);
271
272     curtime = time(0);
273
274     /* copy selected fields. Source is in xdr format. */
275     header.dbversion = db.h.version;
276     header.created = htonl(curtime);
277     strcpy(header.cell, "");
278     header.lastDumpId = db.h.lastDumpId;
279     header.lastInstanceId = db.h.lastInstanceId;
280     header.lastTapeId = db.h.lastTapeId;
281
282     tcode = writeStructHeader(fid, SD_DBHEADER);
283     if (tcode)
284         ERROR(tcode);
285
286     if (canWrite(fid) <= 0)
287         ERROR(BUDB_DUMPFAILED);
288
289     if (write(fid, &header, sizeof(header)) != sizeof(header))
290         ERROR(BUDB_DUMPFAILED);
291
292     haveWritten(sizeof(header));
293
294 error_exit:
295     return(code);
296 }
297
298 /* writeDump
299  *      write out a dump entry structure
300  */
301
302 afs_int32    
303 writeDump(fid, dumpPtr)
304      int fid;
305      dbDumpP dumpPtr;
306 {
307     struct budb_dumpEntry dumpEntry;
308     afs_int32 code = 0, tcode;
309     
310     tcode = dumpToBudbDump(dumpPtr, &dumpEntry);
311     if (tcode)
312         ERROR(tcode);
313
314     writeStructHeader(fid, SD_DUMP);
315
316     if (canWrite(fid) <= 0)
317         ERROR(BUDB_DUMPFAILED);
318
319     if ( write(fid, &dumpEntry, sizeof(dumpEntry)) != sizeof(dumpEntry) )
320         ERROR(BUDB_DUMPFAILED);
321     haveWritten(sizeof(dumpEntry));
322
323 error_exit:
324     return(code);
325 }
326
327 afs_int32
328 writeTape(fid, tapePtr, dumpid)
329      int fid;
330      struct tape *tapePtr;
331      afs_int32 dumpid;
332 {
333     struct budb_tapeEntry tapeEntry;
334     afs_int32 code = 0, tcode;
335     
336     tcode = writeStructHeader(fid, SD_TAPE);
337     if (tcode)
338         ERROR(tcode);
339
340     tapeToBudbTape(tapePtr, &tapeEntry);
341
342     tapeEntry.dump = htonl(dumpid);
343
344     if (canWrite(fid) <= 0)
345         ERROR(BUDB_DUMPFAILED);
346
347     if (write(fid, &tapeEntry, sizeof(tapeEntry)) != sizeof(tapeEntry))
348         ERROR(BUDB_DUMPFAILED);
349
350     haveWritten(sizeof(tapeEntry));
351
352 error_exit:
353     return(code);
354 }
355
356 /* combines volFragment and volInfo */
357
358 afs_int32
359 writeVolume(ut, fid, volFragmentPtr, volInfoPtr, dumpid, tapeName)
360      struct ubik_trans *ut;
361      int fid;
362      struct volFragment *volFragmentPtr;
363      struct volInfo     *volInfoPtr;
364      afs_int32 dumpid;
365      char *tapeName;
366 {
367     struct budb_volumeEntry budbVolume;
368     afs_int32 code = 0;
369     
370     volsToBudbVol(volFragmentPtr, volInfoPtr, &budbVolume);
371
372     budbVolume.dump = htonl(dumpid);
373     strcpy(budbVolume.tape, tapeName);
374
375     writeStructHeader(fid, SD_VOLUME);
376
377     if (canWrite(fid) <= 0)
378         ERROR(BUDB_DUMPFAILED);
379
380     if (write(fid, &budbVolume, sizeof(budbVolume)) != sizeof(budbVolume))
381         ERROR(BUDB_DUMPFAILED);
382
383     haveWritten(sizeof(budbVolume));
384
385 error_exit:
386     return(code);
387 }
388
389 /* -------------------
390  * handlers for the text blocks
391  * -------------------
392  */
393
394 /* checkLock
395  *      make sure a text lock is NOT held
396  * exit:
397  *      0 - not held
398  *      n - error
399  */
400
401 afs_int32
402 checkLock(textType)
403      afs_int32 textType;
404 {
405     db_lockP    lockPtr;
406
407     if ( (textType < 0) || (textType > TB_NUM-1) )
408         return(BUDB_BADARGUMENT);
409
410     lockPtr = &db.h.textLocks[textType];
411
412     if ( lockPtr->lockState != 0 )
413         return(BUDB_LOCKED);
414     return(0);
415 }
416
417 /* checkText
418  *      check the integrity of the specified text type
419  */
420
421 checkText(ut, textType)
422      struct ubik_trans *ut;
423      afs_int32 textType;
424 {
425     struct textBlock *tbPtr;
426     afs_int32 nBytes = 0;                       /* accumulated actual size */
427     afs_int32 size;
428     struct block block;
429     dbadr blockAddr;
430
431     afs_int32 code = 0;
432
433     tbPtr = &db.h.textBlock[textType];
434     blockAddr = ntohl(tbPtr->textAddr);
435     size = ntohl(tbPtr->size);
436
437     while ( blockAddr != 0 )
438     {
439         /* read the block */
440         code = cdbread(ut, text_BLOCK, blockAddr, (char *) &block, sizeof(block));
441         if ( code )
442             ERROR(code);
443         
444         /* check its type */
445         if ( block.h.type != text_BLOCK )
446             ERROR(BUDB_DATABASEINCONSISTENT);
447
448         /* add up the size */
449         nBytes += BLOCK_DATA_SIZE;
450
451         blockAddr = ntohl(block.h.next);
452     }
453
454     /* ensure that we have at least the expected amount of text */
455     if ( nBytes < size )
456         ERROR(BUDB_DATABASEINCONSISTENT);
457     
458 error_exit:
459     return(code);
460 }
461
462 /* writeText
463  * entry:
464  *      textType - type of text block, e.g. TB_DUMPSCHEDULE
465  */
466
467 afs_int32       
468 writeText(ut, fid, textType)
469      struct ubik_trans *ut;
470      int fid;
471      int textType;
472 {
473     struct textBlock *tbPtr;
474     afs_int32 textSize, writeSize;
475     dbadr dbAddr;
476     struct block block;
477     afs_int32 code = 0;
478     
479     /* check lock is free */
480     code = checkLock(textType);
481     if (code)
482         ERROR(code);
483
484      /* ensure that this block has the correct type */
485     code = checkText(ut, textType);
486     if (code) {
487         LogError(0, "writeText: text type %d damaged\n", textType);
488         ERROR(code);
489     }
490
491     tbPtr = &db.h.textBlock[textType];
492     textSize = ntohl(tbPtr->size);
493     dbAddr = ntohl(tbPtr->textAddr);
494
495     if (!dbAddr)
496        goto error_exit;                    /* Don't save anything if no blocks */
497
498     writeTextHeader(fid, textType);
499
500     while (dbAddr) {
501         code = cdbread(ut, text_BLOCK, dbAddr, (char *) &block, sizeof(block));
502         if (code)
503             ERROR(code);
504
505         writeSize = MIN(textSize, BLOCK_DATA_SIZE);
506         if (!writeSize)
507             break;
508
509         if (canWrite(fid) <= 0)
510             ERROR(BUDB_DUMPFAILED);
511
512         if (write(fid, &block.a[0], writeSize) != writeSize) 
513             ERROR(BUDB_IO);
514
515         haveWritten(writeSize);
516         textSize -= writeSize;
517
518         dbAddr = ntohl(block.h.next);
519     }
520
521 error_exit:
522     return(code);
523 }
524
525 #define MAXAPPENDS 200
526
527 afs_int32
528 writeDatabase(ut, fid)
529      struct ubik_trans *ut;
530      int fid;
531 {
532     dbadr dbAddr, dbAppAddr;
533     struct dump diskDump, apDiskDump;
534     dbadr tapeAddr;
535     struct tape diskTape;
536     dbadr volFragAddr;
537     struct volFragment diskVolFragment;
538     struct volInfo     diskVolInfo;
539     int length, hash;
540     int old = 0;
541     int entrySize;
542     afs_int32 code = 0, tcode;
543     afs_int32 appDumpAddrs[MAXAPPENDS], numaddrs, appcount, j;
544
545     struct memoryHashTable *mht;
546
547     LogDebug(4, "writeDatabase:\n");
548
549     /* write out a header identifying this database etc */
550     tcode = writeDbHeader(fid);
551     if (tcode) {
552         LogError(tcode, "writeDatabase: Can't write Header\n");
553         ERROR(tcode);
554     }
555
556     /* write out the tree of dump structures */
557
558     mht = ht_GetType (HT_dumpIden_FUNCTION, &entrySize);
559     if (!mht) {
560         LogError(tcode, "writeDatabase: Can't get dump type\n");
561         ERROR(BUDB_BADARGUMENT);
562     }
563
564     for (old = 0; old <= 1; old++) {
565         /*oldnew*/
566         /* only two states, old or not old */
567         length = (old ? mht->oldLength : mht->length);
568         if (!length)
569             continue;
570         
571         for (hash=0; hash<length; hash++) { 
572             /*hashBuckets*/
573             /* dump all the dumps in this hash bucket
574              */
575             for (dbAddr = ht_LookupBucket (ut, mht, hash, old);
576                  dbAddr;
577                  dbAddr = ntohl(diskDump.idHashChain))
578             { /*initialDumps*/
579                 /* now check if this dump had any errors/inconsistencies.
580                  * If so, don't dump it
581                  */
582                 if (badEntry(dbAddr)) {
583                     LogError(0, "writeDatabase: Damaged dump entry at addr 0x%x\n", dbAddr);
584                     Log ("     Skipping remainder of dumps on hash chain %d\n", hash);
585                     break;
586                 }
587
588                 tcode = cdbread(ut, dump_BLOCK, dbAddr, &diskDump, sizeof(diskDump));
589                 if (tcode) {
590                    LogError(tcode, "writeDatabase: Can't read dump entry (addr 0x%x)\n", dbAddr);
591                    Log ("     Skipping remainder of dumps on hash chain %d\n", hash);
592                    break;
593                 }
594
595                 /* Skip appended dumps, only start with initial dumps */
596                 if (diskDump.initialDumpID != 0)
597                    continue;
598
599                 /* Skip appended dumps, only start with initial dumps. Then
600                  * follow the appended dump chain so they are in order for restore.
601                  */
602                 appcount = numaddrs = 0;
603                 for (dbAppAddr=dbAddr; dbAppAddr; dbAppAddr = ntohl(apDiskDump.appendedDumpChain)) {
604                     /*appendedDumps*/
605                     /* Check to see if we have a circular loop of appended dumps */
606                     for (j=0; j<numaddrs; j++) {
607                        if (appDumpAddrs[j] == dbAppAddr) break;/* circular loop */
608                     }
609                     if (j < numaddrs) {                   /* circular loop */
610                        Log("writeDatabase: Circular loop found in appended dumps\n");
611                        Log("Skipping rest of appended dumps of dumpID %u\n",
612                             ntohl(diskDump.id));
613                        break;
614                     }
615                     if (numaddrs >= MAXAPPENDS) numaddrs = MAXAPPENDS-1;    /* don't overflow */
616                     appDumpAddrs[numaddrs] = dbAppAddr;
617                     numaddrs++;
618
619                     /* If we dump a 1000 appended dumps, assume a loop */
620                     if (appcount >= 5 * MAXAPPENDS) {
621                        Log("writeDatabase: Potential circular loop of appended dumps\n");
622                        Log("Skipping rest of appended dumps of dumpID %u. Dumped %d\n",
623                             ntohl(diskDump.id), appcount);
624                        break;
625                     }
626                     appcount++;
627
628                     /* Read the dump entry */
629                     if (dbAddr == dbAppAddr) {
630                        /* First time through, don't need to read the dump entry again */
631                        memcpy(&apDiskDump, &diskDump, sizeof(diskDump));
632                     }
633                     else {
634                        if (badEntry(dbAppAddr)) {
635                           LogError(0, "writeDatabase: Damaged appended dump entry at addr 0x%x\n", dbAddr);
636                           Log ("     Skipping this and remainder of appended dumps of initial DumpID %u\n",
637                                ntohl(diskDump.id));
638                           break;
639                        }
640                        
641                        tcode = cdbread(ut, dump_BLOCK, dbAppAddr, &apDiskDump, sizeof(apDiskDump));
642                        if (tcode) {
643                           LogError(tcode, "writeDatabase: Can't read appended dump entry (addr 0x%x)\n",
644                                    dbAppAddr);
645                           Log ("     Skipping this and remainder of appended dumps of initial DumpID %u\n",
646                                ntohl(diskDump.id));
647                           break;
648                        }
649
650                        /* Verify that this appended dump points to the initial dump */
651                        if (ntohl(apDiskDump.initialDumpID) != ntohl(diskDump.id)) {
652                           LogError(0, "writeDatabase: Appended dumpID %u does not reference initial dumpID %u\n", 
653                                    ntohl(apDiskDump.id), ntohl(diskDump.id));
654                           Log ("     Skipping this appended dump\n");
655                           continue;
656                        }
657                     }
658
659                     /* Save the dump entry */
660                     tcode = writeDump(fid, &apDiskDump);
661                     if (tcode) {
662                        LogError(tcode, "writeDatabase: Can't write dump entry\n");
663                        ERROR(tcode);
664                     }
665                 
666                     /* For each tape on this dump
667                      */
668                     for (tapeAddr = ntohl(apDiskDump.firstTape);
669                          tapeAddr;
670                          tapeAddr = ntohl(diskTape.nextTape))
671                     { /*tapes*/
672                         /* read the tape entry */
673                         tcode = cdbread(ut, tape_BLOCK, tapeAddr, &diskTape, sizeof(diskTape));
674                         if (tcode) {
675                            LogError(tcode, "writeDatabase: Can't read tape entry (addr 0x%x) of dumpID %u\n", 
676                                     tapeAddr, ntohl(apDiskDump.id));
677                            Log("     Skipping this and remaining tapes in the dump (and all their volumes)\n");
678                            break;
679                         }
680
681                         /* Save the tape entry */
682                         tcode = writeTape(fid, &diskTape, ntohl(apDiskDump.id));
683                         if (tcode) {
684                            LogError(tcode, "writeDatabase: Can't write tape entry\n");
685                            ERROR(tcode);
686                         }
687                     
688                         /* For each volume on this tape.
689                          */
690                         for (volFragAddr = ntohl(diskTape.firstVol); 
691                              volFragAddr; 
692                              volFragAddr = ntohl(diskVolFragment.sameTapeChain))
693                         { /*volumes*/
694                             /* Read the volume Fragment entry */
695                             tcode = cdbread(ut, volFragment_BLOCK, volFragAddr, 
696                                             &diskVolFragment, sizeof(diskVolFragment));
697                             if (tcode) {
698                                LogError(tcode, "writeDatabase: Can't read volfrag entry (addr 0x%x) of dumpID %u\n",
699                                         volFragAddr, ntohl(apDiskDump.id));
700                                Log("     Skipping this and remaining volumes on tape '%s'\n", diskTape.name);
701                                break;
702                             }
703
704                             /* Read the volume Info entry */
705                             tcode = cdbread(ut, volInfo_BLOCK, ntohl(diskVolFragment.vol), 
706                                             &diskVolInfo, sizeof(diskVolInfo));
707                             if (tcode) {
708                                LogError(tcode, "writeDatabase: Can't read volinfo entry (addr 0x%x) of dumpID %u\n",
709                                         ntohl(diskVolFragment.vol), ntohl(apDiskDump.id));
710                                Log("     Skipping volume on tape '%s'\n", diskTape.name);
711                                continue;
712                             }
713
714                             /* Save the volume entry */
715                             tcode = writeVolume(ut, fid, &diskVolFragment, &diskVolInfo, 
716                                                 ntohl(apDiskDump.id), diskTape.name);
717                             if (tcode) {
718                                LogError(tcode, "writeDatabase: Can't write volume entry\n");
719                                ERROR(tcode);
720                             }
721                         } /*volumes*/
722                     } /*tapes*/
723                 } /*appendedDumps*/
724             } /*initialDumps*/
725         } /*hashBuckets*/
726     } /*oldnew*/
727
728     /* write out the textual configuration information */
729     tcode = writeText(ut, fid, TB_DUMPSCHEDULE);
730     if (tcode) {
731         LogError(tcode, "writeDatabase: Can't write dump schedule\n");
732         ERROR(tcode);
733     }
734     tcode = writeText(ut, fid, TB_VOLUMESET);
735     if (tcode) {
736         LogError(tcode, "writeDatabase: Can't write volume set\n");
737         ERROR(tcode);
738     }
739     tcode = writeText(ut, fid, TB_TAPEHOSTS);
740     if (tcode) {
741         LogError(tcode, "writeDatabase: Can't write tape hosts\n");
742         ERROR(tcode);
743     }
744
745     tcode = writeStructHeader(fid, SD_END);
746     if (tcode) {
747         LogError(tcode, "writeDatabase: Can't write end savedb\n");
748         ERROR(tcode);
749     }
750     
751 error_exit:
752     doneWriting(code);
753     return(code);
754 }
755
756
757 #ifdef notdef
758
759 afs_int32
760 canWrite(fid)
761      int fid;
762 {
763     afs_int32 in, out, except;
764     struct timeval tp;
765     afs_int32 code;
766
767     tp.tv_sec = 0;
768     tp.tv_usec = 0;
769
770     out = ( 1 << fid );
771     in = 0;
772     except = 0;
773
774     code = IOMGR_Select(32, &in, &out, &except, &tp);
775     return(code);
776 }
777
778 #endif /* notdef */