Whamcloud - gitweb
b=16098
[fs/lustre-release.git] / lustre / utils / llverfs.c
1 /* -*- mode: c; c-basic-offset: 8; indent-tabs-mode: nil; -*-
2  * vim:expandtab:shiftwidth=8:tabstop=8:
3  *
4  * GPL HEADER START
5  *
6  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License version 2 only,
10  * as published by the Free Software Foundation.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * General Public License version 2 for more details (a copy is included
16  * in the LICENSE file that accompanied this code).
17  *
18  * You should have received a copy of the GNU General Public License
19  * version 2 along with this program; If not, see [sun.com URL with a
20  * copy of GPLv2].
21  *
22  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
23  * CA 95054 USA or visit www.sun.com if you need additional information or
24  * have any questions.
25  *
26  * GPL HEADER END
27  */
28 /*
29  * Copyright  2008 Sun Microsystems, Inc. All rights reserved
30  * Use is subject to license terms.
31  */
32 /*
33  * This file is part of Lustre, http://www.lustre.org/
34  * Lustre is a trademark of Sun Microsystems, Inc.
35  *
36  * lustre/utils/llverfs.c
37  *
38  * ext3 Filesystem Verification Tool.
39  * This program tests the correct operation of ext3 filesystem.
40  * This tool have two working modes
41  * 1. full mode
42  * 2. fast mode
43  *      The full mode is basic mode in which program creates a subdirectory
44  * in the test fileysytem, writes n(files_in_dir, default=16) large(4GB) files
45  * to the directory with the test pattern at the start of each 4kb block.
46  * The test pattern contains timestamp, relative file offset and per file
47  * unique idenfifier(inode number). this continues until whole filesystem is
48  * full and then this tooll verifies that the data in all of the test files
49  * is correct.
50  *      In the fast mode the tool creates a test directories with
51  * EXT3_TOPDIR_FL flag set. the number of directories equals to the number
52  * of block groups in the filesystem(e.g. 65536 directories for 8TB filesystem)
53  * and then writes a single 1MB file in each directory. The tool then verifies
54  * that the data in each file is correct.
55  */
56
57 #ifndef _GNU_SOURCE
58 #define _GNU_SOURCE
59 #endif
60 #ifndef LUSTRE_UTILS
61 #define LUSTRE_UTILS
62 #endif
63 #ifndef _LARGEFILE64_SOURCE
64 #define _LARGEFILE64_SOURCE
65 #endif
66 #ifndef _FILE_OFFSET_BITS
67 #define _FILE_OFFSET_BITS 64
68 #endif
69
70 #include <features.h>
71 #include <stdlib.h>
72 #include <stdio.h>
73 #include <string.h>
74 #include <ctype.h>
75 #include <fcntl.h>
76 #include <unistd.h>
77 #include <limits.h>
78 #include <errno.h>
79 #include <fcntl.h>
80 #include <getopt.h>
81 #include <time.h>
82 #include <dirent.h>
83 #include <mntent.h>
84 #include <sys/types.h>
85 #include <sys/stat.h>
86 #include <sys/vfs.h>
87 #include <gnu/stubs.h>
88 #include <gnu/stubs.h>
89
90 #ifdef HAVE_EXT2FS_EXT2FS_H
91 #  include <e2p/e2p.h>
92 #  include <ext2fs/ext2fs.h>
93 #endif
94
95 #define ONE_MB (1024 * 1024)
96 #define ONE_GB ((unsigned long long)(1024 * 1024 * 1024))
97 #define BLOCKSIZE 4096
98
99 /* Structure for writing test pattern */
100 struct block_data {
101         unsigned long long bd_offset;
102         unsigned long long bd_time;
103         unsigned long long bd_inode;
104 };
105 static char *progname;              /* name by which this program was run. */
106 static unsigned verbose = 1;        /* prints offset in kB, operation rate */
107 static int readoption;              /* run test in read-only (verify) mode */
108 static int writeoption;             /* run test in write_only mode */
109 char *testdir;                      /* name of device to be tested. */
110 static unsigned full = 1;           /* flag to full check */
111 static int errno_local;             /* local copy of errno */
112 static unsigned long num_files;     /* Total number of files for read/write */
113 static loff_t file_size = 4*ONE_GB; /* Size of each file */
114 static unsigned files_in_dir = 32;  /* number of files in each directioy */
115 static unsigned num_dirs = 30000;   /* total number of directories */
116 const int dirmode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
117 static int fd = -1;
118 static int isatty_flag;
119 static int perms =  S_IRWXU | S_IRGRP | S_IROTH;
120
121 static struct option const longopts[] =
122 {
123         { "chunksize", required_argument, 0, 'c' },
124         { "help", no_argument, 0, 'h' },
125         { "offset", required_argument, 0, 'o' },
126         { "long", no_argument, 0, 'l' },
127         { "partial", required_argument, 0, 'p' },
128         { "quiet", required_argument, 0, 'q' },
129         { "read", no_argument, 0, 'r' },
130         { "timestamp", required_argument, 0, 't' },
131         { "verbose", no_argument, 0, 'v' },
132         { "write", no_argument, 0, 'w' },
133         { 0, 0, 0, 0}
134 };
135
136 /*
137  * Usages: displays help information, whenever user supply --help option in
138  * command or enters incorrect command line.
139  */
140 void usage(int status)
141 {
142         if (status != 0)
143         {
144               printf("\nUsage: %s [OPTION]... <filesystem path> ...\n",
145                         progname);
146               printf("ext3 filesystem verification tool.\n"
147                   "\t-t {seconds} for --timestamp,  set test time"
148                   "(default=current time())\n"
149                   "\t-o {offset}  for --offset, directory starting offset"
150                   " from which tests should start\n"
151                   "\t-r run test in read (verify) mode\n"
152                   "\t-w run test in write (test-pattern) mode (default=r&w)\n"
153                   "\t-v for verbose\n"
154                   "\t-p for --partial, for partial check (1MB files)\n"
155                   "\t-l for --long, full check (4GB file with 4k blocks)\n"
156                   "\t-c for --chunksize, IO chunk size (default=1048576)\n"
157                   "\t-h display this help and exit\n"
158                   "\t--help display this help and exit\n");
159         }
160         exit(status);
161 }
162
163 /*
164  * open_file: Opens file in specified mode and returns fd.
165  */
166 static int open_file(const char *file, int flag)
167 {
168         fd = open(file, flag, perms);
169         if (fd < 0) {
170                 fprintf(stderr, "\n%s: Open '%s' failed:%s\n",
171                         progname, file, strerror(errno));
172                 exit(3);
173         }
174         return (fd);
175 }
176
177 /*
178  * Verify_chunk: Verifies test pattern in each 4kB (BLOCKSIZE) is correct.
179  * Returns 0 if test offset and timestamp is correct otherwise 1.
180  */
181 int verify_chunk(char *chunk_buf, size_t chunksize,unsigned long long chunk_off,
182                  unsigned long long time_st, unsigned long long inode_st,
183                  char *file)
184 {
185         struct block_data *bd;
186         char *chunk_end;
187
188         for (chunk_end = chunk_buf + chunksize - sizeof(*bd);
189              (char *)chunk_buf < chunk_end;
190              chunk_buf += BLOCKSIZE, chunk_off += BLOCKSIZE) {
191                 bd = (struct block_data *)chunk_buf;
192                 if ((bd->bd_offset == chunk_off) && (bd->bd_time == time_st) &&
193                     (bd->bd_inode == inode_st))
194                         continue;
195                 fprintf(stderr,"\n%s: verify %s failed offset/timestamp/inode "
196                         "%llu/%llu/%llu: found %llu/%llu/%llu instead\n",
197                         progname, file, chunk_off, time_st, inode_st,
198                         bd->bd_offset, bd->bd_time, bd->bd_inode);
199                 return 1;
200         }
201         return 0;
202 }
203
204 /*
205  * fill_chunk: Fills the chunk with current or user specified timestamp
206  * and  offset. The test patters is filled at the beginning of
207  * each 4kB(BLOCKSIZE) blocks in chunk_buf.
208  */
209 void fill_chunk(char *chunk_buf, size_t chunksize, loff_t chunk_off,
210                 time_t time_st, ino_t inode_st)
211 {
212         struct block_data *bd;
213         char *chunk_end;
214
215         for (chunk_end = chunk_buf + chunksize - sizeof(*bd);
216              (char *)chunk_buf < chunk_end;
217              chunk_buf += BLOCKSIZE, chunk_off += BLOCKSIZE) {
218                 bd = (struct block_data *)chunk_buf;
219                 bd->bd_offset = chunk_off;
220                 bd->bd_time = time_st;
221                 bd->bd_inode = inode_st;
222         }
223 }
224
225 /*
226  * write_chunk: write the chunk_buf on the device. The number of write
227  * operations are based on the parameters write_end, offset, and chunksize.
228  */
229 int write_chunks(int fd, unsigned long long offset,unsigned long long write_end,
230                  char *chunk_buf, size_t chunksize, time_t time_st,
231                  ino_t inode_st, const char *file)
232 {
233         unsigned long long stride;
234
235         stride = full ? chunksize : (ONE_GB - chunksize);
236         for (offset = offset & ~(chunksize - 1); offset < write_end;
237              offset += stride) {
238                 if (lseek64(fd, offset, SEEK_SET) == -1) {
239                         fprintf(stderr, "\n%s: lseek64(%s+%llu) failed: %s\n",
240                                 progname, file, offset, strerror(errno));
241                         return 1;
242                 }
243                 if (offset + chunksize > write_end)
244                         chunksize = write_end - offset;
245                 if (!full && offset > chunksize) {
246                         fill_chunk(chunk_buf, chunksize, offset, time_st,
247                                     inode_st);
248                         if (write(fd, chunk_buf, chunksize) < 0) {
249                                 if (errno == ENOSPC) {
250                                         errno_local = errno;
251                                         return 0;
252                                 }
253                                 fprintf(stderr,
254                                         "\n%s: write %s+%llu failed: %s\n",
255                                         progname, file, offset,strerror(errno));
256                                 return errno;
257                         }
258                         offset += chunksize;
259                         if (offset + chunksize > write_end)
260                                 chunksize = write_end - offset;
261                 }
262                 fill_chunk(chunk_buf, chunksize, offset, time_st, inode_st);
263                 if (write(fd, (char *) chunk_buf, chunksize) < 0) {
264                         if (errno == ENOSPC) {
265                                 errno_local = errno;
266                                 return 0;
267                         }
268                         fprintf(stderr, "\n%s: write %s+%llu failed: %s\n",
269                                 progname, file, offset, strerror(errno));
270                         return 1;
271                 }
272         }
273         return 0;
274 }
275
276 /*
277  * read_chunk: reads the chunk_buf from the device. The number of read
278  * operations are based on the parameters read_end, offset, and chunksize.
279  */
280 int read_chunks(int fd, unsigned long long offset, unsigned long long read_end,
281                 char *chunk_buf, size_t chunksize, time_t time_st,
282                 ino_t inode_st, char *file)
283 {
284         unsigned long long stride;
285
286         stride = full ? chunksize : (ONE_GB - chunksize);
287         for (offset = offset & ~(chunksize - 1); offset < read_end;
288              offset += stride) {
289                 if (lseek64(fd, offset, SEEK_SET) == -1) {
290                         fprintf(stderr, "\n%s: lseek64(%s+%llu) failed: %s\n",
291                                 progname, file, offset, strerror(errno));
292                         return 1;
293                 }
294                 if (offset + chunksize > read_end)
295                         chunksize = read_end - offset;
296                 if (!full && offset > chunksize) {
297                         if (read(fd, chunk_buf, chunksize) < 0) {
298                                 fprintf(stderr,
299                                         "\n%s: read %s+%llu failed: %s\n",
300                                         progname, file, offset,strerror(errno));
301                                 return 1;
302                         }
303                         if (verify_chunk(chunk_buf, chunksize, offset,
304                                          time_st, inode_st, file) != 0)
305                                 return 1;
306                         offset += chunksize;
307                         if (offset + chunksize >= read_end)
308                                 chunksize = read_end - offset;
309                 }
310                 if (read(fd, chunk_buf, chunksize) < 0) {
311                         fprintf(stderr, "\n%s: read %s+%llu failed: %s\n",
312                                 progname, file, offset, strerror(errno));
313                         return 1;
314                 }
315                 if (verify_chunk(chunk_buf, chunksize, offset, time_st,
316                                  inode_st, file) != 0)
317                         return 1;
318         }
319         return 0;
320 }
321
322 /*
323  * new_file: prepares new filename using file counter and current dir.
324  */
325 char *new_file(char *tempfile, char *cur_dir, int file_num)
326 {
327         sprintf(tempfile, "%s/file%03d", cur_dir, file_num);
328         return tempfile;
329 }
330
331 /*
332  * new_dir: prepares new dir name using dir counters.
333  */
334 char *new_dir(char *tempdir, int dir_num)
335 {
336         sprintf(tempdir, "%s/dir%05d", testdir, dir_num);
337         return tempdir;
338 }
339
340 /*
341  * show_filename: Displays name of current file read/write
342  */
343 void show_filename(char *op, char *filename)
344 {
345         static time_t last;
346         time_t now;
347         double diff;
348
349         now = time(NULL);
350         diff = now - last;
351         if (diff > 4 || verbose > 2) {
352                 if (isatty_flag)
353                         printf("\r");
354                 printf("%s File name: %s          ", op, filename);
355                 if (isatty_flag)
356                         fflush(stdout);
357                 else
358                         printf("\n");
359                 last = now;
360         }
361 }
362
363 /*
364  * dir_write: This function writes directories and files on device.
365  * it works for both full and fast modes.
366  */
367 static int dir_write(char *chunk_buf, size_t chunksize,
368                      time_t time_st, unsigned long dir_num)
369 {
370         char tempfile[PATH_MAX];
371         char tempdir[PATH_MAX];
372         struct stat64 file;
373         int file_num = 999999999;
374         ino_t inode_st = 0;
375
376 #ifdef HAVE_EXT2FS_EXT2FS_H
377         if (!full && fsetflags(testdir, EXT2_TOPDIR_FL))
378                 fprintf(stderr,
379                         "\n%s: can't set TOPDIR_FL on %s: %s (ignoring)",
380                         progname, testdir, strerror(errno));
381 #endif
382         for (; dir_num < num_dirs; num_files++, file_num++) {
383                 if (file_num >= files_in_dir) {
384                         if (dir_num == num_dirs - 1)
385                                 break;
386
387                         file_num = 0;
388                         if (mkdir(new_dir(tempdir, dir_num), dirmode) < 0) {
389                                 if (errno == ENOSPC)
390                                         break;
391                                 if (errno != EEXIST) {
392                                         fprintf(stderr, "\n%s: mkdir %s : %s\n",
393                                                 progname, tempdir,
394                                                 strerror(errno));
395                                         return 1;
396                                 }
397                         }
398                         dir_num++;
399                 }
400                 fd = open_file(new_file(tempfile, tempdir, file_num),
401                                O_WRONLY | O_CREAT | O_TRUNC | O_LARGEFILE);
402
403                 if (fd >= 0 && fstat64(fd, &file) == 0) {
404                         inode_st = file.st_ino;
405                 } else {
406                         fprintf(stderr, "\n%s: write stat64 to file %s: %s",
407                                 progname, tempfile, strerror(errno));
408                         exit(1);
409                 }
410
411                 if (verbose > 1)
412                         show_filename("write", tempfile);
413
414                 if (write_chunks(fd, 0, file_size, chunk_buf, chunksize,
415                                  time_st, inode_st, tempfile)) {
416                         close(fd);
417                         return 1;
418                 }
419                 close(fd);
420
421                 if (errno_local == ENOSPC)
422                         break;
423         }
424
425         if (verbose) {
426                 verbose++;
427                 show_filename("write", tempfile);
428                 printf("\nwrite complete\n");
429                 verbose--;
430         }
431
432         return 0;
433 }
434
435 /*
436  * dir_read: This function reads directories and files on device.
437  * it works for both full and fast modes.
438  */
439 static int dir_read(char *chunk_buf, size_t chunksize,
440                     time_t time_st, unsigned long dir_num)
441 {
442         char tempfile[PATH_MAX];
443         char tempdir[PATH_MAX];
444         unsigned long count = 0;
445         struct stat64 file;
446         int file_num = 0;
447         ino_t inode_st = 0;
448
449         for (count = 0; count < num_files && dir_num < num_dirs; count++) {
450                 if (file_num == 0) {
451                         if (dir_num == num_dirs - 1)
452                                 break;
453
454                         new_dir(tempdir, dir_num);
455                         dir_num++;
456                 }
457
458                 fd = open_file(new_file(tempfile, tempdir, file_num),
459                                O_RDONLY | O_LARGEFILE);
460                 if (fd >= 0 && fstat64(fd, &file) == 0) {
461                         inode_st = file.st_ino;
462                 } else {
463                         fprintf(stderr, "\n%s: read stat64 file '%s': %s\n",
464                                 progname, tempfile, strerror(errno));
465                         return 1;
466                 }
467
468                 if (verbose > 1)
469                         show_filename("read", tempfile);
470
471                 if (count == num_files)
472                         file_size = file.st_size;
473                 if (read_chunks(fd, 0, file_size, chunk_buf, chunksize,
474                                 time_st, inode_st, tempfile)) {
475                         close(fd);
476                         return 1;
477                 }
478                 close(fd);
479
480                 if (++file_num >= files_in_dir)
481                         file_num = 0;
482         }
483         if (verbose > 1){
484                 verbose++;
485                 show_filename("read", tempfile);
486                 printf("\nread complete\n");
487                 verbose--;
488         }
489         return 0;
490 }
491
492 int main(int argc, char **argv)
493 {
494         time_t time_st = 0;             /* Default timestamp */
495         size_t chunksize = ONE_MB;      /* IO chunk size(defailt=1MB) */
496         char *chunk_buf;                /* chunk buffer */
497         int error = 0;
498         FILE *countfile = NULL;
499         char filecount[PATH_MAX];
500         unsigned long dir_num = 0, dir_num_orig = 0;/* starting directory */
501         char c;
502
503         progname = strrchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0];
504         while ((c = (char)getopt_long(argc, argv, "t:rwvplo:h",
505                                       longopts, NULL)) != -1) {
506                 switch (c) {
507                 case 'c':
508                         chunksize = (strtoul(optarg, NULL, 0) * ONE_MB);
509                         if (!chunksize) {
510                                 fprintf(stderr, "%s: Chunk size value should be"
511                                         "a multiple of 1MB\n", progname);
512                                 return -1;
513                         }
514                         break;
515                 case 'l':
516                         full = 1;
517                         break;
518                 case 'o': /* offset */
519                         dir_num = strtoul(optarg, NULL, 0);
520                         break;
521                 case 'p':
522                         full = 0;
523                         break;
524                 case 'q':
525                         verbose = 0;
526                         break;
527                 case 'r':
528                         readoption = 1;
529                         break;
530                 case 't':
531                         time_st = (time_t)strtoul(optarg, NULL, 0);
532                         break;
533                 case 'w':
534                         writeoption = 1;
535                         break;
536                 case 'v':
537                         verbose++;
538                         break;
539
540                 case 'h':
541                 default:
542                         usage(1);
543                         return 0;
544                 }
545         }
546         testdir = argv[optind];
547
548         if (!testdir) {
549                 fprintf(stderr, "%s: pathname not given\n", progname);
550                 usage(1);
551                 return -1;
552         }
553         if (!readoption && !writeoption) {
554                 readoption = 1;
555                 writeoption = 1;
556         }
557         if (!time_st)
558                 (void) time(&time_st);
559         printf("Timestamp: %lu\n", (unsigned long )time_st);
560         isatty_flag = isatty(STDOUT_FILENO);
561
562         if (!full) {
563 #ifdef HAVE_EXT2FS_EXT2FS_H
564                 struct mntent *tempmnt;
565                 FILE *fp = NULL;
566                 ext2_filsys fs;
567
568                 if ((fp = setmntent("/etc/mtab", "r")) == NULL){
569                         fprintf(stderr, "%s: fail to open /etc/mtab in read"
570                                 "mode :%s\n", progname, strerror(errno));
571                         goto guess;
572                 }
573
574                 /* find device name using filesystem */
575                 while ((tempmnt = getmntent(fp)) != NULL) {
576                         if (strcmp(tempmnt->mnt_dir, testdir) == 0)
577                                 break;
578                 }
579
580                 if (tempmnt == NULL) {
581                         fprintf(stderr, "%s: no device found for '%s'\n",
582                                 progname, testdir);
583                         endmntent(fp);
584                         goto guess;
585                 }
586
587                 if (ext2fs_open(tempmnt->mnt_fsname, 0, 0, 0,
588                                 unix_io_manager, &fs)) {
589                         fprintf(stderr, "%s: unable to open ext3 fs on '%s'\n",
590                                 progname, testdir);
591                         endmntent(fp);
592                         goto guess;
593                 }
594                 endmntent(fp);
595
596                 num_dirs = (fs->super->s_blocks_count +
597                             fs->super->s_blocks_per_group - 1) /
598                         fs->super->s_blocks_per_group;
599                 if (verbose)
600                         printf("ext3 block groups: %u, fs blocks: %u "
601                                "blocks per group: %u\n",
602                                num_dirs, fs->super->s_blocks_count,
603                                fs->super->s_blocks_per_group);
604                 ext2fs_close(fs);
605 #else
606                 goto guess;
607 #endif
608                 if (0) { /* ugh */
609                         struct statfs64 statbuf;
610                 guess:
611                         if (statfs64(testdir, &statbuf) == 0) {
612                                 num_dirs = (long long)statbuf.f_blocks *
613                                         statbuf.f_bsize / (128ULL << 20);
614                                 if (verbose)
615                                         printf("dirs: %u, fs blocks: %llu\n",
616                                                num_dirs,
617                                                (long long)statbuf.f_blocks);
618                         } else {
619                                 fprintf(stderr, "%s: unable to stat '%s': %s\n",
620                                         progname, testdir, strerror(errno));
621                                 if (verbose)
622                                         printf("dirs: %u\n", num_dirs);
623                         }
624                 }
625
626                 file_size = ONE_MB;
627                 chunksize = ONE_MB;
628                 files_in_dir = 1;
629         }
630         chunk_buf = (char *)calloc(chunksize, 1);
631         if (chunk_buf == NULL) {
632                 fprintf(stderr, "Memory allocation failed for chunk_buf\n");
633                 return 4;
634         }
635         sprintf(filecount, "%s/%s.filecount", testdir, progname);
636         if (writeoption) {
637                 (void)mkdir(testdir, dirmode);
638
639                 unlink(filecount);
640                 if (dir_num != 0) {
641                         num_files = dir_num * files_in_dir;
642                         if (verbose)
643                                 printf("\n%s: %lu files already written\n",
644                                        progname, num_files);
645                 }
646                 if (dir_write(chunk_buf, chunksize, time_st, dir_num)) {
647                         error = 3;
648                         goto out;
649                 }
650                 countfile = fopen(filecount, "w");
651                 if (countfile != NULL) {
652                         if (fprintf(countfile, "%lu", num_files) < 1 ||
653                             fflush(countfile) != 0) {
654                                 fprintf(stderr, "\n%s: writing %s failed :%s\n",
655                                         progname, filecount, strerror(errno));
656                         }
657                         fclose(countfile);
658                 }
659                 dir_num = dir_num_orig;
660         }
661         if (readoption) {
662                 if (!writeoption) {
663                         countfile = fopen(filecount, "r");
664                         if (countfile == NULL ||
665                             fscanf(countfile, "%lu", &num_files) != 1) {
666                                 fprintf(stderr, "\n%s: reading %s failed :%s\n",
667                                         progname, filecount, strerror(errno));
668                                 num_files = num_dirs * files_in_dir;
669                         } else {
670                                 num_files -= (dir_num * files_in_dir);
671                         }
672                         if (countfile)
673                                 fclose(countfile);
674                 }
675                 if (dir_read(chunk_buf, chunksize, time_st, dir_num)) {
676                         fprintf(stderr, "\n%s: Data verification failed\n",
677                                 progname) ;
678                         error = 2;
679                         goto out;
680                 }
681         }
682         error = 0;
683 out:
684         free(chunk_buf);
685         return error;
686 }