1 /*- 2 * Copyright (c) 2004 Pawel Jakub Dawidek <pjd@FreeBSD.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND 15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE 18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 * SUCH DAMAGE. 25 */ 26 27 #include <sys/cdefs.h> 28 __FBSDID("$FreeBSD$"); 29 30 #include <sys/param.h> 31 #include <sys/systm.h> 32 #include <sys/kernel.h> 33 #include <sys/module.h> 34 #include <sys/lock.h> 35 #include <sys/mutex.h> 36 #include <sys/bio.h> 37 #include <sys/sysctl.h> 38 #include <sys/malloc.h> 39 #include <sys/bitstring.h> 40 #include <vm/uma.h> 41 #include <machine/atomic.h> 42 #include <geom/geom.h> 43 #include <sys/proc.h> 44 #include <sys/kthread.h> 45 #include <geom/mirror/g_mirror.h> 46 47 48 static struct g_mirror_softc * 49 g_mirror_find_device(struct g_class *mp, const char *name) 50 { 51 struct g_mirror_softc *sc; 52 struct g_geom *gp; 53 54 g_topology_assert(); 55 LIST_FOREACH(gp, &mp->geom, geom) { 56 sc = gp->softc; 57 if (sc == NULL) 58 continue; 59 if ((sc->sc_flags & G_MIRROR_DEVICE_FLAG_DESTROY) != 0) 60 continue; 61 if (strcmp(gp->name, name) == 0 || 62 strcmp(sc->sc_name, name) == 0) { 63 return (sc); 64 } 65 } 66 return (NULL); 67 } 68 69 static struct g_mirror_disk * 70 g_mirror_find_disk(struct g_mirror_softc *sc, const char *name) 71 { 72 struct g_mirror_disk *disk; 73 74 g_topology_assert(); 75 LIST_FOREACH(disk, &sc->sc_disks, d_next) { 76 if (disk->d_consumer == NULL) 77 continue; 78 if (disk->d_consumer->provider == NULL) 79 continue; 80 if (strcmp(disk->d_consumer->provider->name, name) == 0) 81 return (disk); 82 } 83 return (NULL); 84 } 85 86 static void 87 g_mirror_ctl_configure(struct gctl_req *req, struct g_class *mp) 88 { 89 struct g_mirror_softc *sc; 90 struct g_mirror_disk *disk; 91 const char *name, *balancep; 92 intmax_t *slicep; 93 uint32_t slice; 94 uint8_t balance; 95 int *nargs, *autosync, *noautosync, *hardcode, *dynamic, do_sync = 0; 96 97 g_topology_assert(); 98 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 99 if (*nargs != 1) { 100 gctl_error(req, "Invalid number of arguments."); 101 return; 102 } 103 name = gctl_get_asciiparam(req, "arg0"); 104 sc = g_mirror_find_device(mp, name); 105 if (sc == NULL) { 106 gctl_error(req, "No such device: %s.", name); 107 return; 108 } 109 if (g_mirror_ndisks(sc, -1) < sc->sc_ndisks) { 110 gctl_error(req, "Not all disks connected."); 111 return; 112 } 113 balancep = gctl_get_asciiparam(req, "balance"); 114 if (strcmp(balancep, "none") == 0) 115 balance = sc->sc_balance; 116 else { 117 if (balance_id(balancep) == -1) { 118 gctl_error(req, "Invalid balance algorithm."); 119 return; 120 } 121 balance = balance_id(balancep); 122 } 123 slicep = gctl_get_paraml(req, "slice", sizeof(*slicep)); 124 if (slicep == NULL) { 125 gctl_error(req, "No '%s' argument.", "slice"); 126 return; 127 } 128 if (*slicep == -1) 129 slice = sc->sc_slice; 130 else 131 slice = *slicep; 132 autosync = gctl_get_paraml(req, "autosync", sizeof(*autosync)); 133 if (autosync == NULL) { 134 gctl_error(req, "No '%s' argument.", "autosync"); 135 return; 136 } 137 noautosync = gctl_get_paraml(req, "noautosync", sizeof(*noautosync)); 138 if (noautosync == NULL) { 139 gctl_error(req, "No '%s' argument.", "noautosync"); 140 return; 141 } 142 hardcode = gctl_get_paraml(req, "hardcode", sizeof(*hardcode)); 143 if (hardcode == NULL) { 144 gctl_error(req, "No '%s' argument.", "hardcode"); 145 return; 146 } 147 dynamic = gctl_get_paraml(req, "dynamic", sizeof(*dynamic)); 148 if (dynamic == NULL) { 149 gctl_error(req, "No '%s' argument.", "dynamic"); 150 return; 151 } 152 if (sc->sc_balance == balance && sc->sc_slice == slice && !*autosync && 153 !*noautosync && !*hardcode && !*dynamic) { 154 gctl_error(req, "Nothing has changed."); 155 return; 156 } 157 if (*autosync && *noautosync) { 158 gctl_error(req, "'%s' and '%s' specified.", "autosync", 159 "noautosync"); 160 return; 161 } 162 if (*hardcode && *dynamic) { 163 gctl_error(req, "'%s' and '%s' specified.", "hardcode", 164 "dynamic"); 165 return; 166 } 167 sc->sc_balance = balance; 168 sc->sc_slice = slice; 169 if ((sc->sc_flags & G_MIRROR_DEVICE_FLAG_NOAUTOSYNC) != 0) { 170 if (*autosync) { 171 sc->sc_flags &= ~G_MIRROR_DEVICE_FLAG_NOAUTOSYNC; 172 do_sync = 1; 173 } 174 } else { 175 if (*noautosync) 176 sc->sc_flags |= G_MIRROR_DEVICE_FLAG_NOAUTOSYNC; 177 } 178 LIST_FOREACH(disk, &sc->sc_disks, d_next) { 179 if (do_sync) { 180 if (disk->d_state == G_MIRROR_DISK_STATE_SYNCHRONIZING) 181 disk->d_flags &= ~G_MIRROR_DISK_FLAG_FORCE_SYNC; 182 } 183 if (*hardcode) 184 disk->d_flags |= G_MIRROR_DISK_FLAG_HARDCODED; 185 else if (*dynamic) 186 disk->d_flags &= ~G_MIRROR_DISK_FLAG_HARDCODED; 187 g_mirror_update_metadata(disk); 188 if (do_sync) { 189 if (disk->d_state == G_MIRROR_DISK_STATE_STALE) { 190 g_mirror_event_send(disk, 191 G_MIRROR_DISK_STATE_DISCONNECTED, 192 G_MIRROR_EVENT_DONTWAIT); 193 } 194 } 195 } 196 } 197 198 static void 199 g_mirror_ctl_rebuild(struct gctl_req *req, struct g_class *mp) 200 { 201 struct g_mirror_metadata md; 202 struct g_mirror_softc *sc; 203 struct g_mirror_disk *disk; 204 struct g_provider *pp; 205 const char *name; 206 char param[16]; 207 int error, *nargs; 208 u_int i; 209 210 g_topology_assert(); 211 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 212 if (nargs == NULL) { 213 gctl_error(req, "No '%s' argument.", "nargs"); 214 return; 215 } 216 if (*nargs < 2) { 217 gctl_error(req, "Too few arguments."); 218 return; 219 } 220 name = gctl_get_asciiparam(req, "arg0"); 221 if (name == NULL) { 222 gctl_error(req, "No 'arg%u' argument.", 0); 223 return; 224 } 225 sc = g_mirror_find_device(mp, name); 226 if (sc == NULL) { 227 gctl_error(req, "No such device: %s.", name); 228 return; 229 } 230 231 for (i = 1; i < (u_int)*nargs; i++) { 232 snprintf(param, sizeof(param), "arg%u", i); 233 name = gctl_get_asciiparam(req, param); 234 if (name == NULL) { 235 gctl_error(req, "No 'arg%u' argument.", i); 236 continue; 237 } 238 disk = g_mirror_find_disk(sc, name); 239 if (disk == NULL) { 240 gctl_error(req, "No such provider: %s.", name); 241 continue; 242 } 243 if (g_mirror_ndisks(sc, G_MIRROR_DISK_STATE_ACTIVE) == 1 && 244 disk->d_state == G_MIRROR_DISK_STATE_ACTIVE) { 245 /* 246 * This is the last active disk. There will be nothing 247 * to rebuild it from, so deny this request. 248 */ 249 gctl_error(req, 250 "Provider %s is the last active provider in %s.", 251 name, sc->sc_geom->name); 252 return; 253 } 254 /* 255 * Do rebuild by resetting syncid, disconnecting the disk and 256 * connecting it again. 257 */ 258 disk->d_sync.ds_syncid = 0; 259 if ((sc->sc_flags & G_MIRROR_DEVICE_FLAG_NOAUTOSYNC) != 0) 260 disk->d_flags |= G_MIRROR_DISK_FLAG_FORCE_SYNC; 261 g_mirror_update_metadata(disk); 262 pp = disk->d_consumer->provider; 263 error = g_mirror_read_metadata(disk->d_consumer, &md); 264 g_mirror_event_send(disk, G_MIRROR_DISK_STATE_DISCONNECTED, 265 G_MIRROR_EVENT_WAIT); 266 if (error != 0) { 267 gctl_error(req, "Cannot read metadata from %s.", 268 pp->name); 269 continue; 270 } 271 error = g_mirror_add_disk(sc, pp, &md); 272 if (error != 0) { 273 gctl_error(req, "Cannot reconnect component %s.", 274 pp->name); 275 continue; 276 } 277 } 278 } 279 280 static void 281 g_mirror_ctl_insert(struct gctl_req *req, struct g_class *mp) 282 { 283 struct g_mirror_softc *sc; 284 struct g_mirror_disk *disk; 285 struct g_mirror_metadata md; 286 struct g_provider *pp; 287 struct g_consumer *cp; 288 intmax_t *priority; 289 const char *name; 290 char param[16]; 291 u_char *sector; 292 u_int i, n; 293 int error, *nargs, *hardcode, *inactive; 294 struct { 295 struct g_provider *provider; 296 struct g_consumer *consumer; 297 } *disks; 298 299 g_topology_assert(); 300 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 301 if (nargs == NULL) { 302 gctl_error(req, "No '%s' argument.", "nargs"); 303 return; 304 } 305 if (*nargs < 2) { 306 gctl_error(req, "Too few arguments."); 307 return; 308 } 309 priority = gctl_get_paraml(req, "priority", sizeof(*priority)); 310 if (priority == NULL) { 311 gctl_error(req, "No '%s' argument.", "priority"); 312 return; 313 } 314 inactive = gctl_get_paraml(req, "inactive", sizeof(*inactive)); 315 if (inactive == NULL) { 316 gctl_error(req, "No '%s' argument.", "inactive"); 317 return; 318 } 319 hardcode = gctl_get_paraml(req, "hardcode", sizeof(*hardcode)); 320 if (hardcode == NULL) { 321 gctl_error(req, "No '%s' argument.", "hardcode"); 322 return; 323 } 324 name = gctl_get_asciiparam(req, "arg0"); 325 if (name == NULL) { 326 gctl_error(req, "No 'arg%u' argument.", 0); 327 return; 328 } 329 sc = g_mirror_find_device(mp, name); 330 if (sc == NULL) { 331 gctl_error(req, "No such device: %s.", name); 332 return; 333 } 334 if (g_mirror_ndisks(sc, -1) < sc->sc_ndisks) { 335 gctl_error(req, "Not all disks connected."); 336 return; 337 } 338 339 disks = g_malloc(sizeof(*disks) * (*nargs), M_WAITOK | M_ZERO); 340 for (i = 1, n = 0; i < (u_int)*nargs; i++) { 341 snprintf(param, sizeof(param), "arg%u", i); 342 name = gctl_get_asciiparam(req, param); 343 if (name == NULL) { 344 gctl_error(req, "No 'arg%u' argument.", i); 345 continue; 346 } 347 if (strncmp(name, "/dev/", strlen("/dev/")) == 0) 348 name += strlen("/dev/"); 349 if (g_mirror_find_disk(sc, name) != NULL) { 350 gctl_error(req, "Provider %s already inserted.", name); 351 continue; 352 } 353 pp = g_provider_by_name(name); 354 if (pp == NULL) { 355 gctl_error(req, "Unknown provider %s.", name); 356 continue; 357 } 358 if (sc->sc_provider->mediasize > pp->mediasize) { 359 gctl_error(req, "Provider %s too small.", name); 360 continue; 361 } 362 if ((sc->sc_provider->sectorsize % pp->sectorsize) != 0) { 363 gctl_error(req, "Invalid sectorsize of provider %s.", 364 name); 365 continue; 366 } 367 cp = g_new_consumer(sc->sc_geom); 368 if (g_attach(cp, pp) != 0) { 369 g_destroy_consumer(cp); 370 gctl_error(req, "Cannot attach to provider %s.", name); 371 continue; 372 } 373 if (g_access(cp, 0, 1, 1) != 0) { 374 g_detach(cp); 375 g_destroy_consumer(cp); 376 gctl_error(req, "Cannot access provider %s.", name); 377 continue; 378 } 379 disks[n].provider = pp; 380 disks[n].consumer = cp; 381 n++; 382 } 383 if (n == 0) { 384 g_free(disks); 385 return; 386 } 387 sc->sc_ndisks += n; 388 again: 389 for (i = 0; i < n; i++) { 390 if (disks[i].consumer == NULL) 391 continue; 392 g_mirror_fill_metadata(sc, NULL, &md); 393 md.md_priority = *priority; 394 if (*inactive) 395 md.md_dflags |= G_MIRROR_DISK_FLAG_INACTIVE; 396 pp = disks[i].provider; 397 if (*hardcode) { 398 strlcpy(md.md_provider, pp->name, 399 sizeof(md.md_provider)); 400 } else { 401 bzero(md.md_provider, sizeof(md.md_provider)); 402 } 403 sector = g_malloc(pp->sectorsize, M_WAITOK); 404 mirror_metadata_encode(&md, sector); 405 error = g_write_data(disks[i].consumer, 406 pp->mediasize - pp->sectorsize, sector, pp->sectorsize); 407 g_free(sector); 408 if (error != 0) { 409 gctl_error(req, "Cannot store metadata on %s.", 410 pp->name); 411 g_access(disks[i].consumer, 0, -1, -1); 412 g_detach(disks[i].consumer); 413 g_destroy_consumer(disks[i].consumer); 414 disks[i].consumer = NULL; 415 disks[i].provider = NULL; 416 sc->sc_ndisks--; 417 goto again; 418 } 419 } 420 if (i == 0) { 421 /* All writes failed. */ 422 g_free(disks); 423 return; 424 } 425 LIST_FOREACH(disk, &sc->sc_disks, d_next) { 426 g_mirror_update_metadata(disk); 427 } 428 /* 429 * Release provider and wait for retaste. 430 */ 431 for (i = 0; i < n; i++) { 432 if (disks[i].consumer == NULL) 433 continue; 434 g_access(disks[i].consumer, 0, -1, -1); 435 g_detach(disks[i].consumer); 436 g_destroy_consumer(disks[i].consumer); 437 } 438 g_free(disks); 439 } 440 441 static void 442 g_mirror_ctl_remove(struct gctl_req *req, struct g_class *mp) 443 { 444 struct g_mirror_softc *sc; 445 struct g_mirror_disk *disk; 446 const char *name; 447 char param[16]; 448 int *nargs; 449 u_int i; 450 451 g_topology_assert(); 452 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 453 if (nargs == NULL) { 454 gctl_error(req, "No '%s' argument.", "nargs"); 455 return; 456 } 457 if (*nargs < 2) { 458 gctl_error(req, "Too few arguments."); 459 return; 460 } 461 name = gctl_get_asciiparam(req, "arg0"); 462 if (name == NULL) { 463 gctl_error(req, "No 'arg%u' argument.", 0); 464 return; 465 } 466 sc = g_mirror_find_device(mp, name); 467 if (sc == NULL) { 468 gctl_error(req, "No such device: %s.", name); 469 return; 470 } 471 if (g_mirror_ndisks(sc, -1) < sc->sc_ndisks) { 472 gctl_error(req, "Not all disks connected."); 473 return; 474 } 475 476 for (i = 1; i < (u_int)*nargs; i++) { 477 snprintf(param, sizeof(param), "arg%u", i); 478 name = gctl_get_asciiparam(req, param); 479 if (name == NULL) { 480 gctl_error(req, "No 'arg%u' argument.", i); 481 continue; 482 } 483 disk = g_mirror_find_disk(sc, name); 484 if (disk == NULL) { 485 gctl_error(req, "No such provider: %s.", name); 486 continue; 487 } 488 g_mirror_event_send(disk, G_MIRROR_DISK_STATE_DESTROY, 489 G_MIRROR_EVENT_WAIT); 490 } 491 } 492 493 static void 494 g_mirror_ctl_deactivate(struct gctl_req *req, struct g_class *mp) 495 { 496 struct g_mirror_softc *sc; 497 struct g_mirror_disk *disk; 498 const char *name; 499 char param[16]; 500 int *nargs; 501 u_int i; 502 503 g_topology_assert(); 504 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 505 if (nargs == NULL) { 506 gctl_error(req, "No '%s' argument.", "nargs"); 507 return; 508 } 509 if (*nargs < 2) { 510 gctl_error(req, "Too few arguments."); 511 return; 512 } 513 name = gctl_get_asciiparam(req, "arg0"); 514 if (name == NULL) { 515 gctl_error(req, "No 'arg%u' argument.", 0); 516 return; 517 } 518 sc = g_mirror_find_device(mp, name); 519 if (sc == NULL) { 520 gctl_error(req, "No such device: %s.", name); 521 return; 522 } 523 524 for (i = 1; i < (u_int)*nargs; i++) { 525 snprintf(param, sizeof(param), "arg%u", i); 526 name = gctl_get_asciiparam(req, param); 527 if (name == NULL) { 528 gctl_error(req, "No 'arg%u' argument.", i); 529 continue; 530 } 531 disk = g_mirror_find_disk(sc, name); 532 if (disk == NULL) { 533 gctl_error(req, "No such provider: %s.", name); 534 continue; 535 } 536 disk->d_flags |= G_MIRROR_DISK_FLAG_INACTIVE; 537 disk->d_flags &= ~G_MIRROR_DISK_FLAG_FORCE_SYNC; 538 g_mirror_update_metadata(disk); 539 sc->sc_bump_id |= G_MIRROR_BUMP_SYNCID; 540 g_mirror_event_send(disk, G_MIRROR_DISK_STATE_DISCONNECTED, 541 G_MIRROR_EVENT_WAIT); 542 } 543 } 544 545 static void 546 g_mirror_ctl_forget(struct gctl_req *req, struct g_class *mp) 547 { 548 struct g_mirror_softc *sc; 549 struct g_mirror_disk *disk; 550 const char *name; 551 char param[16]; 552 int *nargs; 553 u_int i; 554 555 g_topology_assert(); 556 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 557 if (nargs == NULL) { 558 gctl_error(req, "No '%s' argument.", "nargs"); 559 return; 560 } 561 if (*nargs < 1) { 562 gctl_error(req, "Missing device(s)."); 563 return; 564 } 565 566 for (i = 0; i < (u_int)*nargs; i++) { 567 snprintf(param, sizeof(param), "arg%u", i); 568 name = gctl_get_asciiparam(req, param); 569 if (name == NULL) { 570 gctl_error(req, "No 'arg%u' argument.", i); 571 return; 572 } 573 sc = g_mirror_find_device(mp, name); 574 if (sc == NULL) { 575 gctl_error(req, "No such device: %s.", name); 576 return; 577 } 578 if (g_mirror_ndisks(sc, -1) == sc->sc_ndisks) { 579 G_MIRROR_DEBUG(1, 580 "All disks connected in %s, skipping.", 581 sc->sc_name); 582 continue; 583 } 584 sc->sc_ndisks = g_mirror_ndisks(sc, -1); 585 LIST_FOREACH(disk, &sc->sc_disks, d_next) { 586 g_mirror_update_metadata(disk); 587 } 588 } 589 } 590 591 static void 592 g_mirror_ctl_stop(struct gctl_req *req, struct g_class *mp) 593 { 594 struct g_mirror_softc *sc; 595 int *force, *nargs, error; 596 const char *name; 597 char param[16]; 598 u_int i; 599 600 g_topology_assert(); 601 602 nargs = gctl_get_paraml(req, "nargs", sizeof(*nargs)); 603 if (nargs == NULL) { 604 gctl_error(req, "No '%s' argument.", "nargs"); 605 return; 606 } 607 if (*nargs < 1) { 608 gctl_error(req, "Missing device(s)."); 609 return; 610 } 611 force = gctl_get_paraml(req, "force", sizeof(*force)); 612 if (force == NULL) { 613 gctl_error(req, "No '%s' argument.", "force"); 614 return; 615 } 616 617 for (i = 0; i < (u_int)*nargs; i++) { 618 snprintf(param, sizeof(param), "arg%u", i); 619 name = gctl_get_asciiparam(req, param); 620 if (name == NULL) { 621 gctl_error(req, "No 'arg%u' argument.", i); 622 return; 623 } 624 sc = g_mirror_find_device(mp, name); 625 if (sc == NULL) { 626 gctl_error(req, "No such device: %s.", name); 627 return; 628 } 629 error = g_mirror_destroy(sc, *force); 630 if (error != 0) { 631 gctl_error(req, "Cannot destroy device %s (error=%d).", 632 sc->sc_geom->name, error); 633 return; 634 } 635 } 636 } 637 638 void 639 g_mirror_config(struct gctl_req *req, struct g_class *mp, const char *verb) 640 { 641 uint32_t *version; 642 643 g_topology_assert(); 644 645 version = gctl_get_paraml(req, "version", sizeof(*version)); 646 if (version == NULL) { 647 gctl_error(req, "No '%s' argument.", "version"); 648 return; 649 } 650 if (*version != G_MIRROR_VERSION) { 651 gctl_error(req, "Userland and kernel parts are out of sync."); 652 return; 653 } 654 655 if (strcmp(verb, "configure") == 0) 656 g_mirror_ctl_configure(req, mp); 657 else if (strcmp(verb, "rebuild") == 0) 658 g_mirror_ctl_rebuild(req, mp); 659 else if (strcmp(verb, "insert") == 0) 660 g_mirror_ctl_insert(req, mp); 661 else if (strcmp(verb, "remove") == 0) 662 g_mirror_ctl_remove(req, mp); 663 else if (strcmp(verb, "deactivate") == 0) 664 g_mirror_ctl_deactivate(req, mp); 665 else if (strcmp(verb, "forget") == 0) 666 g_mirror_ctl_forget(req, mp); 667 else if (strcmp(verb, "stop") == 0) 668 g_mirror_ctl_stop(req, mp); 669 else 670 gctl_error(req, "Unknown verb."); 671 } 672