1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3 * 4 * Copyright (c) 2007 Lukas Ertl 5 * Copyright (c) 2007, 2009 Ulf Lilleengen 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 * SUCH DAMAGE. 28 */ 29 30 #include <sys/cdefs.h> 31 __FBSDID("$FreeBSD$"); 32 33 #include <sys/param.h> 34 #include <sys/bio.h> 35 #include <sys/conf.h> 36 #include <sys/jail.h> 37 #include <sys/kernel.h> 38 #include <sys/malloc.h> 39 #include <sys/systm.h> 40 41 #include <geom/geom.h> 42 #include <geom/geom_dbg.h> 43 #include <geom/vinum/geom_vinum_var.h> 44 #include <geom/vinum/geom_vinum.h> 45 46 #define DEFAULT_STRIPESIZE 262144 47 48 /* 49 * Create a new drive object, either by user request, during taste of the drive 50 * itself, or because it was referenced by a subdisk during taste. 51 */ 52 int 53 gv_create_drive(struct gv_softc *sc, struct gv_drive *d) 54 { 55 struct g_geom *gp; 56 struct g_provider *pp; 57 struct g_consumer *cp, *cp2; 58 struct gv_drive *d2; 59 struct gv_hdr *hdr; 60 struct gv_freelist *fl; 61 62 KASSERT(d != NULL, ("gv_create_drive: NULL d")); 63 64 gp = sc->geom; 65 66 pp = NULL; 67 cp = cp2 = NULL; 68 69 /* The drive already has a consumer if it was tasted before. */ 70 if (d->consumer != NULL) { 71 cp = d->consumer; 72 cp->private = d; 73 pp = cp->provider; 74 } else if (!(d->flags & GV_DRIVE_REFERENCED)) { 75 if (gv_find_drive(sc, d->name) != NULL) { 76 G_VINUM_DEBUG(0, "drive '%s' already exists", d->name); 77 g_free(d); 78 return (GV_ERR_CREATE); 79 } 80 81 if (gv_find_drive_device(sc, d->device) != NULL) { 82 G_VINUM_DEBUG(0, "provider '%s' already in use by " 83 "gvinum", d->device); 84 return (GV_ERR_CREATE); 85 } 86 87 pp = g_provider_by_name(d->device); 88 if (pp == NULL) { 89 G_VINUM_DEBUG(0, "create '%s': device '%s' disappeared", 90 d->name, d->device); 91 g_free(d); 92 return (GV_ERR_CREATE); 93 } 94 95 g_topology_lock(); 96 cp = g_new_consumer(gp); 97 if (g_attach(cp, pp) != 0) { 98 g_destroy_consumer(cp); 99 g_topology_unlock(); 100 G_VINUM_DEBUG(0, "create drive '%s': unable to attach", 101 d->name); 102 g_free(d); 103 return (GV_ERR_CREATE); 104 } 105 g_topology_unlock(); 106 107 d->consumer = cp; 108 cp->private = d; 109 } 110 111 /* 112 * If this was just a "referenced" drive, we're almost finished, but 113 * insert this drive not on the head of the drives list, as 114 * gv_drive_is_newer() expects a "real" drive from LIST_FIRST(). 115 */ 116 if (d->flags & GV_DRIVE_REFERENCED) { 117 snprintf(d->device, sizeof(d->device), "???"); 118 d2 = LIST_FIRST(&sc->drives); 119 if (d2 == NULL) 120 LIST_INSERT_HEAD(&sc->drives, d, drive); 121 else 122 LIST_INSERT_AFTER(d2, d, drive); 123 return (0); 124 } 125 126 /* 127 * Update access counts of the new drive to those of an already 128 * existing drive. 129 */ 130 LIST_FOREACH(d2, &sc->drives, drive) { 131 if ((d == d2) || (d2->consumer == NULL)) 132 continue; 133 134 cp2 = d2->consumer; 135 g_topology_lock(); 136 if ((cp2->acr || cp2->acw || cp2->ace) && 137 (g_access(cp, cp2->acr, cp2->acw, cp2->ace) != 0)) { 138 g_detach(cp); 139 g_destroy_consumer(cp); 140 g_topology_unlock(); 141 G_VINUM_DEBUG(0, "create drive '%s': unable to update " 142 "access counts", d->name); 143 if (d->hdr != NULL) 144 g_free(d->hdr); 145 g_free(d); 146 return (GV_ERR_CREATE); 147 } 148 g_topology_unlock(); 149 break; 150 } 151 152 d->size = pp->mediasize - GV_DATA_START; 153 d->avail = d->size; 154 d->vinumconf = sc; 155 LIST_INIT(&d->subdisks); 156 LIST_INIT(&d->freelist); 157 158 /* The header might have been set during taste. */ 159 if (d->hdr == NULL) { 160 hdr = g_malloc(sizeof(*hdr), M_WAITOK | M_ZERO); 161 hdr->magic = GV_MAGIC; 162 hdr->config_length = GV_CFG_LEN; 163 getcredhostname(NULL, hdr->label.sysname, GV_HOSTNAME_LEN); 164 strlcpy(hdr->label.name, d->name, sizeof(hdr->label.name)); 165 microtime(&hdr->label.date_of_birth); 166 d->hdr = hdr; 167 } 168 169 /* We also need a freelist entry. */ 170 fl = g_malloc(sizeof(struct gv_freelist), M_WAITOK | M_ZERO); 171 fl->offset = GV_DATA_START; 172 fl->size = d->avail; 173 LIST_INSERT_HEAD(&d->freelist, fl, freelist); 174 d->freelist_entries = 1; 175 176 if (gv_find_drive(sc, d->name) == NULL) 177 LIST_INSERT_HEAD(&sc->drives, d, drive); 178 179 gv_set_drive_state(d, GV_DRIVE_UP, 0); 180 return (0); 181 } 182 183 int 184 gv_create_volume(struct gv_softc *sc, struct gv_volume *v) 185 { 186 KASSERT(v != NULL, ("gv_create_volume: NULL v")); 187 188 v->vinumconf = sc; 189 v->flags |= GV_VOL_NEWBORN; 190 LIST_INIT(&v->plexes); 191 LIST_INSERT_HEAD(&sc->volumes, v, volume); 192 v->wqueue = g_malloc(sizeof(struct bio_queue_head), M_WAITOK | M_ZERO); 193 bioq_init(v->wqueue); 194 return (0); 195 } 196 197 int 198 gv_create_plex(struct gv_softc *sc, struct gv_plex *p) 199 { 200 struct gv_volume *v; 201 202 KASSERT(p != NULL, ("gv_create_plex: NULL p")); 203 204 /* Find the volume this plex should be attached to. */ 205 v = gv_find_vol(sc, p->volume); 206 if (v == NULL) { 207 G_VINUM_DEBUG(0, "create plex '%s': volume '%s' not found", 208 p->name, p->volume); 209 g_free(p); 210 return (GV_ERR_CREATE); 211 } 212 if (!(v->flags & GV_VOL_NEWBORN)) 213 p->flags |= GV_PLEX_ADDED; 214 p->vol_sc = v; 215 v->plexcount++; 216 p->vinumconf = sc; 217 p->synced = 0; 218 p->flags |= GV_PLEX_NEWBORN; 219 LIST_INSERT_HEAD(&v->plexes, p, in_volume); 220 LIST_INIT(&p->subdisks); 221 TAILQ_INIT(&p->packets); 222 LIST_INSERT_HEAD(&sc->plexes, p, plex); 223 p->bqueue = g_malloc(sizeof(struct bio_queue_head), M_WAITOK | M_ZERO); 224 bioq_init(p->bqueue); 225 p->wqueue = g_malloc(sizeof(struct bio_queue_head), M_WAITOK | M_ZERO); 226 bioq_init(p->wqueue); 227 p->rqueue = g_malloc(sizeof(struct bio_queue_head), M_WAITOK | M_ZERO); 228 bioq_init(p->rqueue); 229 return (0); 230 } 231 232 int 233 gv_create_sd(struct gv_softc *sc, struct gv_sd *s) 234 { 235 struct gv_plex *p; 236 struct gv_drive *d; 237 238 KASSERT(s != NULL, ("gv_create_sd: NULL s")); 239 240 /* Find the drive where this subdisk should be put on. */ 241 d = gv_find_drive(sc, s->drive); 242 if (d == NULL) { 243 /* 244 * It's possible that the subdisk references a drive that 245 * doesn't exist yet (during the taste process), so create a 246 * practically empty "referenced" drive. 247 */ 248 if (s->flags & GV_SD_TASTED) { 249 d = g_malloc(sizeof(struct gv_drive), 250 M_WAITOK | M_ZERO); 251 d->flags |= GV_DRIVE_REFERENCED; 252 strlcpy(d->name, s->drive, sizeof(d->name)); 253 gv_create_drive(sc, d); 254 } else { 255 G_VINUM_DEBUG(0, "create sd '%s': drive '%s' not found", 256 s->name, s->drive); 257 g_free(s); 258 return (GV_ERR_CREATE); 259 } 260 } 261 262 /* Find the plex where this subdisk belongs to. */ 263 p = gv_find_plex(sc, s->plex); 264 if (p == NULL) { 265 G_VINUM_DEBUG(0, "create sd '%s': plex '%s' not found", 266 s->name, s->plex); 267 g_free(s); 268 return (GV_ERR_CREATE); 269 } 270 271 /* 272 * First we give the subdisk to the drive, to handle autosized 273 * values ... 274 */ 275 if (gv_sd_to_drive(s, d) != 0) { 276 g_free(s); 277 return (GV_ERR_CREATE); 278 } 279 280 /* 281 * Then, we give the subdisk to the plex; we check if the 282 * given values are correct and maybe adjust them. 283 */ 284 if (gv_sd_to_plex(s, p) != 0) { 285 G_VINUM_DEBUG(0, "unable to give sd '%s' to plex '%s'", 286 s->name, p->name); 287 if (s->drive_sc && !(s->drive_sc->flags & GV_DRIVE_REFERENCED)) 288 LIST_REMOVE(s, from_drive); 289 gv_free_sd(s); 290 g_free(s); 291 /* 292 * If this subdisk can't be created, we won't create 293 * the attached plex either, if it is also a new one. 294 */ 295 if (!(p->flags & GV_PLEX_NEWBORN)) 296 return (GV_ERR_CREATE); 297 gv_rm_plex(sc, p); 298 return (GV_ERR_CREATE); 299 } 300 s->flags |= GV_SD_NEWBORN; 301 302 s->vinumconf = sc; 303 LIST_INSERT_HEAD(&sc->subdisks, s, sd); 304 305 return (0); 306 } 307 308 /* 309 * Create a concatenated volume from specified drives or drivegroups. 310 */ 311 void 312 gv_concat(struct g_geom *gp, struct gctl_req *req) 313 { 314 struct gv_drive *d; 315 struct gv_sd *s; 316 struct gv_volume *v; 317 struct gv_plex *p; 318 struct gv_softc *sc; 319 char *drive, buf[30], *vol; 320 int *drives, dcount; 321 322 sc = gp->softc; 323 dcount = 0; 324 vol = gctl_get_param(req, "name", NULL); 325 if (vol == NULL) { 326 gctl_error(req, "volume name not given"); 327 return; 328 } 329 330 drives = gctl_get_paraml(req, "drives", sizeof(*drives)); 331 332 if (drives == NULL) { 333 gctl_error(req, "drive names not given"); 334 return; 335 } 336 337 /* First we create the volume. */ 338 v = g_malloc(sizeof(*v), M_WAITOK | M_ZERO); 339 strlcpy(v->name, vol, sizeof(v->name)); 340 v->state = GV_VOL_UP; 341 gv_post_event(sc, GV_EVENT_CREATE_VOLUME, v, NULL, 0, 0); 342 343 /* Then we create the plex. */ 344 p = g_malloc(sizeof(*p), M_WAITOK | M_ZERO); 345 snprintf(p->name, sizeof(p->name), "%s.p%d", v->name, v->plexcount); 346 strlcpy(p->volume, v->name, sizeof(p->volume)); 347 p->org = GV_PLEX_CONCAT; 348 p->stripesize = 0; 349 gv_post_event(sc, GV_EVENT_CREATE_PLEX, p, NULL, 0, 0); 350 351 /* Drives are first (right now) priority */ 352 for (dcount = 0; dcount < *drives; dcount++) { 353 snprintf(buf, sizeof(buf), "drive%d", dcount); 354 drive = gctl_get_param(req, buf, NULL); 355 d = gv_find_drive(sc, drive); 356 if (d == NULL) { 357 gctl_error(req, "No such drive '%s'", drive); 358 continue; 359 } 360 s = g_malloc(sizeof(*s), M_WAITOK | M_ZERO); 361 snprintf(s->name, sizeof(s->name), "%s.s%d", p->name, dcount); 362 strlcpy(s->plex, p->name, sizeof(s->plex)); 363 strlcpy(s->drive, drive, sizeof(s->drive)); 364 s->plex_offset = -1; 365 s->drive_offset = -1; 366 s->size = -1; 367 gv_post_event(sc, GV_EVENT_CREATE_SD, s, NULL, 0, 0); 368 } 369 gv_post_event(sc, GV_EVENT_SETUP_OBJECTS, sc, NULL, 0, 0); 370 gv_post_event(sc, GV_EVENT_SAVE_CONFIG, sc, NULL, 0, 0); 371 } 372 373 /* 374 * Create a mirrored volume from specified drives or drivegroups. 375 */ 376 void 377 gv_mirror(struct g_geom *gp, struct gctl_req *req) 378 { 379 struct gv_drive *d; 380 struct gv_sd *s; 381 struct gv_volume *v; 382 struct gv_plex *p; 383 struct gv_softc *sc; 384 char *drive, buf[30], *vol; 385 int *drives, *flags, dcount, pcount, scount; 386 387 sc = gp->softc; 388 dcount = 0; 389 scount = 0; 390 pcount = 0; 391 vol = gctl_get_param(req, "name", NULL); 392 if (vol == NULL) { 393 gctl_error(req, "volume name not given"); 394 return; 395 } 396 397 flags = gctl_get_paraml(req, "flags", sizeof(*flags)); 398 drives = gctl_get_paraml(req, "drives", sizeof(*drives)); 399 400 if (drives == NULL) { 401 gctl_error(req, "drive names not given"); 402 return; 403 } 404 405 /* We must have an even number of drives. */ 406 if (*drives % 2 != 0) { 407 gctl_error(req, "mirror organization must have an even number " 408 "of drives"); 409 return; 410 } 411 if (*flags & GV_FLAG_S && *drives < 4) { 412 gctl_error(req, "must have at least 4 drives for striped plex"); 413 return; 414 } 415 416 /* First we create the volume. */ 417 v = g_malloc(sizeof(*v), M_WAITOK | M_ZERO); 418 strlcpy(v->name, vol, sizeof(v->name)); 419 v->state = GV_VOL_UP; 420 gv_post_event(sc, GV_EVENT_CREATE_VOLUME, v, NULL, 0, 0); 421 422 /* Then we create the plexes. */ 423 for (pcount = 0; pcount < 2; pcount++) { 424 p = g_malloc(sizeof(*p), M_WAITOK | M_ZERO); 425 snprintf(p->name, sizeof(p->name), "%s.p%d", v->name, 426 pcount); 427 strlcpy(p->volume, v->name, sizeof(p->volume)); 428 if (*flags & GV_FLAG_S) { 429 p->org = GV_PLEX_STRIPED; 430 p->stripesize = DEFAULT_STRIPESIZE; 431 } else { 432 p->org = GV_PLEX_CONCAT; 433 p->stripesize = -1; 434 } 435 gv_post_event(sc, GV_EVENT_CREATE_PLEX, p, NULL, 0, 0); 436 437 /* 438 * We just gives each even drive to plex one, and each odd to 439 * plex two. 440 */ 441 scount = 0; 442 for (dcount = pcount; dcount < *drives; dcount += 2) { 443 snprintf(buf, sizeof(buf), "drive%d", dcount); 444 drive = gctl_get_param(req, buf, NULL); 445 d = gv_find_drive(sc, drive); 446 if (d == NULL) { 447 gctl_error(req, "No such drive '%s', aborting", 448 drive); 449 scount++; 450 break; 451 } 452 s = g_malloc(sizeof(*s), M_WAITOK | M_ZERO); 453 snprintf(s->name, sizeof(s->name), "%s.s%d", p->name, 454 scount); 455 strlcpy(s->plex, p->name, sizeof(s->plex)); 456 strlcpy(s->drive, drive, sizeof(s->drive)); 457 s->plex_offset = -1; 458 s->drive_offset = -1; 459 s->size = -1; 460 gv_post_event(sc, GV_EVENT_CREATE_SD, s, NULL, 0, 0); 461 scount++; 462 } 463 } 464 gv_post_event(sc, GV_EVENT_SETUP_OBJECTS, sc, NULL, 0, 0); 465 gv_post_event(sc, GV_EVENT_SAVE_CONFIG, sc, NULL, 0, 0); 466 } 467 468 void 469 gv_raid5(struct g_geom *gp, struct gctl_req *req) 470 { 471 struct gv_softc *sc; 472 struct gv_drive *d; 473 struct gv_volume *v; 474 struct gv_plex *p; 475 struct gv_sd *s; 476 int *drives, *flags, dcount; 477 char *vol, *drive, buf[30]; 478 off_t *stripesize; 479 480 sc = gp->softc; 481 482 vol = gctl_get_param(req, "name", NULL); 483 if (vol == NULL) { 484 gctl_error(req, "volume name not given"); 485 return; 486 } 487 flags = gctl_get_paraml(req, "flags", sizeof(*flags)); 488 drives = gctl_get_paraml(req, "drives", sizeof(*drives)); 489 stripesize = gctl_get_paraml(req, "stripesize", sizeof(*stripesize)); 490 491 if (stripesize == NULL) { 492 gctl_error(req, "no stripesize given"); 493 return; 494 } 495 496 if (drives == NULL) { 497 gctl_error(req, "drive names not given"); 498 return; 499 } 500 501 /* We must have at least three drives. */ 502 if (*drives < 3) { 503 gctl_error(req, "must have at least three drives for this " 504 "plex organisation"); 505 return; 506 } 507 /* First we create the volume. */ 508 v = g_malloc(sizeof(*v), M_WAITOK | M_ZERO); 509 strlcpy(v->name, vol, sizeof(v->name)); 510 v->state = GV_VOL_UP; 511 gv_post_event(sc, GV_EVENT_CREATE_VOLUME, v, NULL, 0, 0); 512 513 /* Then we create the plex. */ 514 p = g_malloc(sizeof(*p), M_WAITOK | M_ZERO); 515 snprintf(p->name, sizeof(p->name), "%s.p%d", v->name, v->plexcount); 516 strlcpy(p->volume, v->name, sizeof(p->volume)); 517 p->org = GV_PLEX_RAID5; 518 p->stripesize = *stripesize; 519 gv_post_event(sc, GV_EVENT_CREATE_PLEX, p, NULL, 0, 0); 520 521 /* Create subdisks on drives. */ 522 for (dcount = 0; dcount < *drives; dcount++) { 523 snprintf(buf, sizeof(buf), "drive%d", dcount); 524 drive = gctl_get_param(req, buf, NULL); 525 d = gv_find_drive(sc, drive); 526 if (d == NULL) { 527 gctl_error(req, "No such drive '%s'", drive); 528 continue; 529 } 530 s = g_malloc(sizeof(*s), M_WAITOK | M_ZERO); 531 snprintf(s->name, sizeof(s->name), "%s.s%d", p->name, dcount); 532 strlcpy(s->plex, p->name, sizeof(s->plex)); 533 strlcpy(s->drive, drive, sizeof(s->drive)); 534 s->plex_offset = -1; 535 s->drive_offset = -1; 536 s->size = -1; 537 gv_post_event(sc, GV_EVENT_CREATE_SD, s, NULL, 0, 0); 538 } 539 gv_post_event(sc, GV_EVENT_SETUP_OBJECTS, sc, NULL, 0, 0); 540 gv_post_event(sc, GV_EVENT_SAVE_CONFIG, sc, NULL, 0, 0); 541 } 542 543 /* 544 * Create a striped volume from specified drives or drivegroups. 545 */ 546 void 547 gv_stripe(struct g_geom *gp, struct gctl_req *req) 548 { 549 struct gv_drive *d; 550 struct gv_sd *s; 551 struct gv_volume *v; 552 struct gv_plex *p; 553 struct gv_softc *sc; 554 char *drive, buf[30], *vol; 555 int *drives, *flags, dcount, pcount; 556 557 sc = gp->softc; 558 dcount = 0; 559 pcount = 0; 560 vol = gctl_get_param(req, "name", NULL); 561 if (vol == NULL) { 562 gctl_error(req, "volume name not given"); 563 return; 564 } 565 flags = gctl_get_paraml(req, "flags", sizeof(*flags)); 566 drives = gctl_get_paraml(req, "drives", sizeof(*drives)); 567 568 if (drives == NULL) { 569 gctl_error(req, "drive names not given"); 570 return; 571 } 572 573 /* We must have at least two drives. */ 574 if (*drives < 2) { 575 gctl_error(req, "must have at least 2 drives"); 576 return; 577 } 578 579 /* First we create the volume. */ 580 v = g_malloc(sizeof(*v), M_WAITOK | M_ZERO); 581 strlcpy(v->name, vol, sizeof(v->name)); 582 v->state = GV_VOL_UP; 583 gv_post_event(sc, GV_EVENT_CREATE_VOLUME, v, NULL, 0, 0); 584 585 /* Then we create the plex. */ 586 p = g_malloc(sizeof(*p), M_WAITOK | M_ZERO); 587 snprintf(p->name, sizeof(p->name), "%s.p%d", v->name, v->plexcount); 588 strlcpy(p->volume, v->name, sizeof(p->volume)); 589 p->org = GV_PLEX_STRIPED; 590 p->stripesize = 262144; 591 gv_post_event(sc, GV_EVENT_CREATE_PLEX, p, NULL, 0, 0); 592 593 /* Create subdisks on drives. */ 594 for (dcount = 0; dcount < *drives; dcount++) { 595 snprintf(buf, sizeof(buf), "drive%d", dcount); 596 drive = gctl_get_param(req, buf, NULL); 597 d = gv_find_drive(sc, drive); 598 if (d == NULL) { 599 gctl_error(req, "No such drive '%s'", drive); 600 continue; 601 } 602 s = g_malloc(sizeof(*s), M_WAITOK | M_ZERO); 603 snprintf(s->name, sizeof(s->name), "%s.s%d", p->name, dcount); 604 strlcpy(s->plex, p->name, sizeof(s->plex)); 605 strlcpy(s->drive, drive, sizeof(s->drive)); 606 s->plex_offset = -1; 607 s->drive_offset = -1; 608 s->size = -1; 609 gv_post_event(sc, GV_EVENT_CREATE_SD, s, NULL, 0, 0); 610 } 611 gv_post_event(sc, GV_EVENT_SETUP_OBJECTS, sc, NULL, 0, 0); 612 gv_post_event(sc, GV_EVENT_SAVE_CONFIG, sc, NULL, 0, 0); 613 } 614