1 /** 2 * Copyright © 2018 Intel Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #pragma once 17 #include <algorithm> 18 #include <boost/asio/spawn.hpp> 19 #include <boost/callable_traits.hpp> 20 #include <cstdint> 21 #include <exception> 22 #include <ipmid/api-types.hpp> 23 #include <ipmid/message.hpp> 24 #include <memory> 25 #include <optional> 26 #include <phosphor-logging/log.hpp> 27 #include <stdexcept> 28 #include <tuple> 29 #include <user_channel/channel_layer.hpp> 30 #include <utility> 31 32 #ifdef ALLOW_DEPRECATED_API 33 #include <ipmid/api.h> 34 35 #include <ipmid/oemrouter.hpp> 36 #endif /* ALLOW_DEPRECATED_API */ 37 38 namespace ipmi 39 { 40 41 template <typename... Args> 42 static inline message::Response::ptr 43 errorResponse(message::Request::ptr request, ipmi::Cc cc, Args&&... args) 44 { 45 message::Response::ptr response = request->makeResponse(); 46 response->cc = cc; 47 response->pack(args...); 48 return response; 49 } 50 static inline message::Response::ptr 51 errorResponse(message::Request::ptr request, ipmi::Cc cc) 52 { 53 message::Response::ptr response = request->makeResponse(); 54 response->cc = cc; 55 return response; 56 } 57 58 /** @brief Exception extension that allows setting an IPMI return code */ 59 class HandlerCompletion 60 { 61 public: 62 HandlerCompletion(Cc cc) noexcept : cc(cc) 63 { 64 } 65 66 Cc code() const noexcept 67 { 68 return cc; 69 } 70 71 private: 72 Cc cc; 73 }; 74 75 /** @brief Exception extension that allows setting an IPMI return code and 76 * printing out a logged error */ 77 class HandlerException : public HandlerCompletion, public std::runtime_error 78 { 79 public: 80 HandlerException(Cc cc, const char* what) : 81 HandlerCompletion(cc), std::runtime_error(what) 82 { 83 } 84 HandlerException(Cc cc, const std::string& what) : 85 HandlerException(cc, what.c_str()) 86 { 87 } 88 }; 89 90 /** 91 * @brief Handler base class for dealing with IPMI request/response 92 * 93 * The subclasses are all templated so they can provide access to any type 94 * of command callback functions. 95 */ 96 class HandlerBase 97 { 98 public: 99 using ptr = std::shared_ptr<HandlerBase>; 100 101 /** @brief wrap the call to the registered handler with the request 102 * 103 * This is called from the running queue context after it has already 104 * created a request object that contains all the information required to 105 * execute the ipmi command. This function will return the response object 106 * pointer that owns the response object that will ultimately get sent back 107 * to the requester. 108 * 109 * This is a non-virtual function wrapper to the virtualized executeCallback 110 * function that actually does the work. This is required because of how 111 * templates and virtualization work together. 112 * 113 * @param request a shared_ptr to a Request object 114 * 115 * @return a shared_ptr to a Response object 116 */ 117 message::Response::ptr call(message::Request::ptr request) 118 { 119 return executeCallback(request); 120 } 121 122 private: 123 /** @brief call the registered handler with the request 124 * 125 * This is called from the running queue context after it has already 126 * created a request object that contains all the information required to 127 * execute the ipmi command. This function will return the response object 128 * pointer that owns the response object that will ultimately get sent back 129 * to the requester. 130 * 131 * @param request a shared_ptr to a Request object 132 * 133 * @return a shared_ptr to a Response object 134 */ 135 virtual message::Response::ptr 136 executeCallback(message::Request::ptr request) = 0; 137 }; 138 139 /** 140 * @brief Main IPMI handler class 141 * 142 * New IPMI handlers will resolve into this class, which will read the signature 143 * of the registering function, attempt to extract the appropriate arguments 144 * from a request, pass the arguments to the function, and then pack the 145 * response of the function back into an IPMI response. 146 */ 147 template <typename Handler> 148 class IpmiHandler final : public HandlerBase 149 { 150 public: 151 explicit IpmiHandler(Handler&& handler) : 152 handler_(std::forward<Handler>(handler)) 153 { 154 } 155 156 private: 157 Handler handler_; 158 159 /** @brief call the registered handler with the request 160 * 161 * This is called from the running queue context after it has already 162 * created a request object that contains all the information required to 163 * execute the ipmi command. This function will return the response object 164 * pointer that owns the response object that will ultimately get sent back 165 * to the requester. 166 * 167 * Because this is the new variety of IPMI handler, this is the function 168 * that attempts to extract the requested parameters in order to pass them 169 * onto the callback function and then packages up the response into a plain 170 * old vector to pass back to the caller. 171 * 172 * @param request a shared_ptr to a Request object 173 * 174 * @return a shared_ptr to a Response object 175 */ 176 message::Response::ptr 177 executeCallback(message::Request::ptr request) override 178 { 179 message::Response::ptr response = request->makeResponse(); 180 181 using CallbackSig = boost::callable_traits::args_t<Handler>; 182 using InputArgsType = typename utility::DecayTuple<CallbackSig>::type; 183 using UnpackArgsType = typename utility::StripFirstArgs< 184 utility::NonIpmiArgsCount<InputArgsType>::size(), 185 InputArgsType>::type; 186 using ResultType = boost::callable_traits::return_type_t<Handler>; 187 188 UnpackArgsType unpackArgs; 189 request->payload.trailingOk = false; 190 ipmi::Cc unpackError = request->unpack(unpackArgs); 191 if (unpackError != ipmi::ccSuccess) 192 { 193 response->cc = unpackError; 194 return response; 195 } 196 /* callbacks can contain an optional first argument of one of: 197 * 1) boost::asio::yield_context 198 * 2) ipmi::Context::ptr 199 * 3) ipmi::message::Request::ptr 200 * 201 * If any of those is part of the callback signature as the first 202 * argument, it will automatically get packed into the parameter pack 203 * here. 204 * 205 * One more special optional argument is an ipmi::message::Payload. 206 * This argument can be in any position, though logically it makes the 207 * most sense if it is the last. If this class is included in the 208 * handler signature, it will allow for the handler to unpack optional 209 * parameters. For example, the Set LAN Configuration Parameters 210 * command takes variable length (and type) values for each of the LAN 211 * parameters. This means that the only fixed data is the channel and 212 * parameter selector. All the remaining data can be extracted using 213 * the Payload class and the unpack API available to the Payload class. 214 */ 215 ResultType result; 216 try 217 { 218 std::optional<InputArgsType> inputArgs; 219 if constexpr (std::tuple_size<InputArgsType>::value > 0) 220 { 221 if constexpr (std::is_same< 222 std::tuple_element_t<0, InputArgsType>, 223 boost::asio::yield_context>::value) 224 { 225 inputArgs.emplace(std::tuple_cat( 226 std::forward_as_tuple(request->ctx->yield), 227 std::move(unpackArgs))); 228 } 229 else if constexpr (std::is_same< 230 std::tuple_element_t<0, InputArgsType>, 231 ipmi::Context::ptr>::value) 232 { 233 inputArgs.emplace( 234 std::tuple_cat(std::forward_as_tuple(request->ctx), 235 std::move(unpackArgs))); 236 } 237 else if constexpr (std::is_same< 238 std::tuple_element_t<0, InputArgsType>, 239 ipmi::message::Request::ptr>::value) 240 { 241 inputArgs.emplace(std::tuple_cat( 242 std::forward_as_tuple(request), std::move(unpackArgs))); 243 } 244 else 245 { 246 // no special parameters were requested (but others were) 247 inputArgs.emplace(std::move(unpackArgs)); 248 } 249 } 250 else 251 { 252 // no parameters were requested 253 inputArgs = std::move(unpackArgs); 254 } 255 256 // execute the registered callback function and get the 257 // ipmi::RspType<> 258 result = std::apply(handler_, *inputArgs); 259 } 260 catch (const HandlerException& e) 261 { 262 phosphor::logging::log<phosphor::logging::level::INFO>( 263 "Handler produced exception", 264 phosphor::logging::entry("CC=%x", e.code()), 265 phosphor::logging::entry("EXCEPTION=%s", e.what()), 266 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 267 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 268 return errorResponse(request, e.code()); 269 } 270 catch (const std::exception& e) 271 { 272 phosphor::logging::log<phosphor::logging::level::ERR>( 273 "Handler failed to catch exception", 274 phosphor::logging::entry("EXCEPTION=%s", e.what()), 275 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 276 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 277 return errorResponse(request, ccUnspecifiedError); 278 } 279 catch (const HandlerCompletion& c) 280 { 281 return errorResponse(request, c.code()); 282 } 283 catch (...) 284 { 285 std::exception_ptr eptr; 286 try 287 { 288 eptr = std::current_exception(); 289 if (eptr) 290 { 291 std::rethrow_exception(eptr); 292 } 293 } 294 catch (const std::exception& e) 295 { 296 phosphor::logging::log<phosphor::logging::level::ERR>( 297 "Handler failed to catch exception", 298 phosphor::logging::entry("EXCEPTION=%s", e.what()), 299 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 300 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 301 return errorResponse(request, ccUnspecifiedError); 302 } 303 } 304 305 response->cc = std::get<0>(result); 306 auto payload = std::get<1>(result); 307 // check for optional payload 308 if (payload) 309 { 310 response->pack(*payload); 311 } 312 return response; 313 } 314 }; 315 316 #ifdef ALLOW_DEPRECATED_API 317 static constexpr size_t maxLegacyBufferSize = 64 * 1024; 318 /** 319 * @brief Legacy IPMI handler class 320 * 321 * Legacy IPMI handlers will resolve into this class, which will behave the same 322 * way as the legacy IPMI queue, passing in a big buffer for the request and a 323 * big buffer for the response. 324 * 325 * As soon as all the handlers have been rewritten, this class will be marked as 326 * deprecated and eventually removed. 327 */ 328 template <> 329 class IpmiHandler<ipmid_callback_t> final : public HandlerBase 330 { 331 public: 332 explicit IpmiHandler(const ipmid_callback_t& handler, void* ctx = nullptr) : 333 handler_(handler), handlerCtx(ctx) 334 { 335 } 336 337 private: 338 ipmid_callback_t handler_; 339 void* handlerCtx; 340 341 /** @brief call the registered handler with the request 342 * 343 * This is called from the running queue context after it has already 344 * created a request object that contains all the information required to 345 * execute the ipmi command. This function will return the response object 346 * pointer that owns the response object that will ultimately get sent back 347 * to the requester. 348 * 349 * Because this is the legacy variety of IPMI handler, this function does 350 * not really have to do much other than pass the payload to the callback 351 * and return response to the caller. 352 * 353 * @param request a shared_ptr to a Request object 354 * 355 * @return a shared_ptr to a Response object 356 */ 357 message::Response::ptr 358 executeCallback(message::Request::ptr request) override 359 { 360 message::Response::ptr response = request->makeResponse(); 361 // allocate a big response buffer here 362 response->payload.resize(maxLegacyBufferSize); 363 364 size_t len = request->payload.size() - request->payload.rawIndex; 365 Cc ccRet{ccSuccess}; 366 try 367 { 368 ccRet = 369 handler_(request->ctx->netFn, request->ctx->cmd, 370 request->payload.data() + request->payload.rawIndex, 371 response->payload.data(), &len, handlerCtx); 372 } 373 catch (const HandlerException& e) 374 { 375 phosphor::logging::log<phosphor::logging::level::INFO>( 376 "Legacy Handler produced exception", 377 phosphor::logging::entry("CC=%x", e.code()), 378 phosphor::logging::entry("EXCEPTION=%s", e.what()), 379 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 380 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 381 return errorResponse(request, e.code()); 382 } 383 catch (const std::exception& e) 384 { 385 phosphor::logging::log<phosphor::logging::level::ERR>( 386 "Legacy Handler failed to catch exception", 387 phosphor::logging::entry("EXCEPTION=%s", e.what()), 388 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 389 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 390 return errorResponse(request, ccUnspecifiedError); 391 } 392 catch (const HandlerCompletion& c) 393 { 394 return errorResponse(request, c.code()); 395 } 396 catch (...) 397 { 398 std::exception_ptr eptr; 399 try 400 { 401 eptr = std::current_exception(); 402 if (eptr) 403 { 404 std::rethrow_exception(eptr); 405 } 406 } 407 catch (const std::exception& e) 408 { 409 phosphor::logging::log<phosphor::logging::level::ERR>( 410 "Handler failed to catch exception", 411 phosphor::logging::entry("EXCEPTION=%s", e.what()), 412 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 413 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 414 return errorResponse(request, ccUnspecifiedError); 415 } 416 } 417 response->cc = ccRet; 418 response->payload.resize(len); 419 return response; 420 } 421 }; 422 423 /** 424 * @brief Legacy IPMI OEM handler class 425 * 426 * Legacy IPMI OEM handlers will resolve into this class, which will behave the 427 * same way as the legacy IPMI queue, passing in a big buffer for the request 428 * and a big buffer for the response. 429 * 430 * As soon as all the handlers have been rewritten, this class will be marked as 431 * deprecated and eventually removed. 432 */ 433 template <> 434 class IpmiHandler<oem::Handler> final : public HandlerBase 435 { 436 public: 437 explicit IpmiHandler(const oem::Handler& handler) : handler_(handler) 438 { 439 } 440 441 private: 442 oem::Handler handler_; 443 444 /** @brief call the registered handler with the request 445 * 446 * This is called from the running queue context after it has already 447 * created a request object that contains all the information required to 448 * execute the ipmi command. This function will return the response object 449 * pointer that owns the response object that will ultimately get sent back 450 * to the requester. 451 * 452 * Because this is the legacy variety of IPMI handler, this function does 453 * not really have to do much other than pass the payload to the callback 454 * and return response to the caller. 455 * 456 * @param request a shared_ptr to a Request object 457 * 458 * @return a shared_ptr to a Response object 459 */ 460 message::Response::ptr 461 executeCallback(message::Request::ptr request) override 462 { 463 message::Response::ptr response = request->makeResponse(); 464 // allocate a big response buffer here 465 response->payload.resize(maxLegacyBufferSize); 466 467 size_t len = request->payload.size() - request->payload.rawIndex; 468 Cc ccRet{ccSuccess}; 469 try 470 { 471 ccRet = 472 handler_(request->ctx->cmd, 473 request->payload.data() + request->payload.rawIndex, 474 response->payload.data(), &len); 475 } 476 catch (const HandlerException& e) 477 { 478 phosphor::logging::log<phosphor::logging::level::INFO>( 479 "Legacy OEM Handler produced exception", 480 phosphor::logging::entry("CC=%x", e.code()), 481 phosphor::logging::entry("EXCEPTION=%s", e.what()), 482 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 483 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 484 return errorResponse(request, e.code()); 485 } 486 catch (const std::exception& e) 487 { 488 phosphor::logging::log<phosphor::logging::level::ERR>( 489 "Legacy OEM Handler failed to catch exception", 490 phosphor::logging::entry("EXCEPTION=%s", e.what()), 491 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 492 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 493 return errorResponse(request, ccUnspecifiedError); 494 } 495 catch (const HandlerCompletion& c) 496 { 497 return errorResponse(request, c.code()); 498 } 499 catch (...) 500 { 501 std::exception_ptr eptr; 502 try 503 { 504 eptr = std::current_exception(); 505 if (eptr) 506 { 507 std::rethrow_exception(eptr); 508 } 509 } 510 catch (const std::exception& e) 511 { 512 phosphor::logging::log<phosphor::logging::level::ERR>( 513 "Handler failed to catch exception", 514 phosphor::logging::entry("EXCEPTION=%s", e.what()), 515 phosphor::logging::entry("NETFN=%x", request->ctx->netFn), 516 phosphor::logging::entry("CMD=%x", request->ctx->cmd)); 517 return errorResponse(request, ccUnspecifiedError); 518 } 519 } 520 response->cc = ccRet; 521 response->payload.resize(len); 522 return response; 523 } 524 }; 525 526 /** 527 * @brief create a legacy IPMI handler class and return a shared_ptr 528 * 529 * The queue uses a map of pointers to do the lookup. This function returns the 530 * shared_ptr that owns the Handler object. 531 * 532 * This is called internally via the ipmi_register_callback function. 533 * 534 * @param handler the function pointer to the callback 535 * 536 * @return A shared_ptr to the created handler object 537 */ 538 inline auto makeLegacyHandler(const ipmid_callback_t& handler, 539 void* ctx = nullptr) 540 { 541 HandlerBase::ptr ptr(new IpmiHandler<ipmid_callback_t>(handler, ctx)); 542 return ptr; 543 } 544 545 /** 546 * @brief create a legacy IPMI OEM handler class and return a shared_ptr 547 * 548 * The queue uses a map of pointers to do the lookup. This function returns the 549 * shared_ptr that owns the Handler object. 550 * 551 * This is called internally via the Router::registerHandler method. 552 * 553 * @param handler the function pointer to the callback 554 * 555 * @return A shared_ptr to the created handler object 556 */ 557 inline auto makeLegacyHandler(oem::Handler&& handler) 558 { 559 HandlerBase::ptr ptr( 560 new IpmiHandler<oem::Handler>(std::forward<oem::Handler>(handler))); 561 return ptr; 562 } 563 #endif // ALLOW_DEPRECATED_API 564 565 /** 566 * @brief create an IPMI handler class and return a shared_ptr 567 * 568 * The queue uses a map of pointers to do the lookup. This function returns the 569 * shared_ptr that owns the Handler object. 570 * 571 * This is called internally via the ipmi::registerHandler function. 572 * 573 * @param handler the function pointer to the callback 574 * 575 * @return A shared_ptr to the created handler object 576 */ 577 template <typename Handler> 578 inline auto makeHandler(Handler&& handler) 579 { 580 HandlerBase::ptr ptr( 581 new IpmiHandler<Handler>(std::forward<Handler>(handler))); 582 return ptr; 583 } 584 585 namespace impl 586 { 587 588 // IPMI command handler registration implementation 589 bool registerHandler(int prio, NetFn netFn, Cmd cmd, Privilege priv, 590 ::ipmi::HandlerBase::ptr handler); 591 bool registerGroupHandler(int prio, Group group, Cmd cmd, Privilege priv, 592 ::ipmi::HandlerBase::ptr handler); 593 bool registerOemHandler(int prio, Iana iana, Cmd cmd, Privilege priv, 594 ::ipmi::HandlerBase::ptr handler); 595 596 } // namespace impl 597 598 /** 599 * @brief main IPMI handler registration function 600 * 601 * This function should be used to register all new-style IPMI handler 602 * functions. This function just passes the callback to makeHandler, which 603 * creates a new wrapper object that will automatically extract the appropriate 604 * parameters for the callback function as well as pack up the response. 605 * 606 * @param prio - priority at which to register; see api.hpp 607 * @param netFn - the IPMI net function number to register 608 * @param cmd - the IPMI command number to register 609 * @param priv - the IPMI user privilige required for this command 610 * @param handler - the callback function that will handle this request 611 * 612 * @return bool - success of registering the handler 613 */ 614 template <typename Handler> 615 bool registerHandler(int prio, NetFn netFn, Cmd cmd, Privilege priv, 616 Handler&& handler) 617 { 618 auto h = ipmi::makeHandler(std::forward<Handler>(handler)); 619 return impl::registerHandler(prio, netFn, cmd, priv, h); 620 } 621 622 /** 623 * @brief register a IPMI OEM group handler 624 * 625 * From IPMI 2.0 spec Network Function Codes Table (Row 2Ch): 626 * The first data byte position in requests and responses under this network 627 * function identifies the defining body that specifies command functionality. 628 * Software assumes that the command and completion code field positions will 629 * hold command and completion code values. 630 * 631 * The following values are used to identify the defining body: 632 * 00h PICMG - PCI Industrial Computer Manufacturer’s Group. (www.picmg.com) 633 * 01h DMTF Pre-OS Working Group ASF Specification (www.dmtf.org) 634 * 02h Server System Infrastructure (SSI) Forum (www.ssiforum.org) 635 * 03h VITA Standards Organization (VSO) (www.vita.com) 636 * DCh DCMI Specifications (www.intel.com/go/dcmi) 637 * all other Reserved 638 * 639 * When this network function is used, the ID for the defining body occupies 640 * the first data byte in a request, and the second data byte (following the 641 * completion code) in a response. 642 * 643 * @tparam Handler - implicitly specified callback function type 644 * @param prio - priority at which to register; see api.hpp 645 * @param netFn - the IPMI net function number to register 646 * @param cmd - the IPMI command number to register 647 * @param priv - the IPMI user privilige required for this command 648 * @param handler - the callback function that will handle this request 649 * 650 * @return bool - success of registering the handler 651 * 652 */ 653 template <typename Handler> 654 void registerGroupHandler(int prio, Group group, Cmd cmd, Privilege priv, 655 Handler&& handler) 656 { 657 auto h = ipmi::makeHandler(handler); 658 impl::registerGroupHandler(prio, group, cmd, priv, h); 659 } 660 661 /** 662 * @brief register a IPMI OEM IANA handler 663 * 664 * From IPMI spec Network Function Codes Table (Row 2Eh): 665 * The first three data bytes of requests and responses under this network 666 * function explicitly identify the OEM or non-IPMI group that specifies the 667 * command functionality. While the OEM or non-IPMI group defines the 668 * functional semantics for the cmd and remaining data fields, the cmd field 669 * is required to hold the same value in requests and responses for a given 670 * operation in order to be supported under the IPMI message handling and 671 * transport mechanisms. 672 * 673 * When this network function is used, the IANA Enterprise Number for the 674 * defining body occupies the first three data bytes in a request, and the 675 * first three data bytes following the completion code position in a 676 * response. 677 * 678 * @tparam Handler - implicitly specified callback function type 679 * @param prio - priority at which to register; see api.hpp 680 * @param netFn - the IPMI net function number to register 681 * @param cmd - the IPMI command number to register 682 * @param priv - the IPMI user privilige required for this command 683 * @param handler - the callback function that will handle this request 684 * 685 * @return bool - success of registering the handler 686 * 687 */ 688 template <typename Handler> 689 void registerOemHandler(int prio, Iana iana, Cmd cmd, Privilege priv, 690 Handler&& handler) 691 { 692 auto h = ipmi::makeHandler(handler); 693 impl::registerOemHandler(prio, iana, cmd, priv, h); 694 } 695 696 } // namespace ipmi 697 698 #ifdef ALLOW_DEPRECATED_API 699 /** 700 * @brief legacy IPMI handler registration function 701 * 702 * This function should be used to register all legacy IPMI handler 703 * functions. This function just behaves just as the legacy registration 704 * mechanism did, silently replacing any existing handler with a new one. 705 * 706 * @param netFn - the IPMI net function number to register 707 * @param cmd - the IPMI command number to register 708 * @param context - ignored 709 * @param handler - the callback function that will handle this request 710 * @param priv - the IPMI user privilige required for this command 711 */ 712 // [[deprecated("Use ipmi::registerHandler() instead")]] 713 void ipmi_register_callback(ipmi_netfn_t netFn, ipmi_cmd_t cmd, 714 ipmi_context_t context, ipmid_callback_t handler, 715 ipmi_cmd_privilege_t priv); 716 717 #endif /* ALLOW_DEPRECATED_API */ 718