extension.py 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """
  2. Extension plugin interface for GPUStack.
  3. Third-party or enterprise plugins can implement this interface
  4. and register via the ``gpustack.plugins`` entry-point group.
  5. A plugin is fully wired at construction time: the subclass's
  6. ``__init__(app, cfg)`` is expected to mount routers, install
  7. middleware, run migrations, and otherwise mutate ``app`` as needed.
  8. After construction the instance can optionally expose long-running
  9. background coroutines via ``async_tasks()`` and a distributed-mode
  10. coordinator via the ``coordinator`` attribute.
  11. """
  12. import logging
  13. from typing import Any, Coroutine, Generator, List, Optional, TYPE_CHECKING, Tuple
  14. from fastapi import FastAPI
  15. from gpustack.config.config import Config
  16. if TYPE_CHECKING:
  17. from gpustack.server.coordinator import Coordinator
  18. logger = logging.getLogger(__name__)
  19. def iter_plugin_classes() -> Generator[Tuple[str, type], None, None]:
  20. """Iterate over registered plugin classes via the ``gpustack.plugins``
  21. entry-point group.
  22. Used at CLI-parse time (before any FastAPI app exists) so plugins can
  23. contribute ``start`` command arguments via ``Plugin.setup_start_cmd``.
  24. At runtime the server instantiates plugins inside ``create_app`` and
  25. stores instances on ``app.state.extension_plugins``.
  26. """
  27. try:
  28. from importlib.metadata import entry_points
  29. for ep in entry_points(group="gpustack.plugins"):
  30. try:
  31. yield ep.name, ep.load()
  32. except Exception:
  33. logger.warning(f"Failed to load plugin class: {ep.name}", exc_info=True)
  34. except ImportError:
  35. pass
  36. class Plugin:
  37. """Base class that all extension plugins must implement.
  38. Subclasses override ``__init__(app, cfg)`` to perform the full
  39. registration — there is no separate ``register`` phase. To opt into
  40. distributed-mode coordination, an ``__init__`` may assign a
  41. ``Coordinator`` to ``self.coordinator``; the server picks it up from
  42. ``app.state.extension_plugins`` and owns its lifecycle.
  43. """
  44. # Optional distributed-mode coordinator; the server starts/stops it.
  45. coordinator: Optional["Coordinator"] = None
  46. def __init__(self, app: FastAPI, cfg: Config) -> None:
  47. pass
  48. def async_tasks(self) -> List[Coroutine[Any, Any, Any]]:
  49. """Long-running background coroutines to be scheduled alongside
  50. the API server. Each coroutine is awaited in the server's main
  51. gather(), so an uncaught exception aborts the server — plugins
  52. are expected to handle their own restart semantics. Default: no
  53. tasks."""
  54. return []
  55. @classmethod
  56. def setup_start_cmd(cls, parser) -> None:
  57. """Contribute arguments to the ``gpustack start`` argparse parser.
  58. Called at CLI-parse time, before ``Config`` is built and before
  59. any FastAPI app exists, so this must be a classmethod and must
  60. not depend on instance state.
  61. """
  62. pass
  63. @classmethod
  64. def contribute_config(cls, args, config_data: dict) -> None:
  65. """Forward plugin-contributed CLI args into the ``Config`` kwargs dict.
  66. Called after argparse parsing and core's ``set_*_options`` have
  67. populated ``config_data``, but before ``Config(**config_data)`` is
  68. constructed. Args registered via ``setup_start_cmd`` end up on the
  69. ``args`` namespace but are not automatically forwarded — override
  70. this method to copy the relevant fields. Because ``Config`` uses
  71. ``extra="allow"``, copied keys appear as attributes on ``cfg``.
  72. Plugins should not overwrite keys that core already set unless the
  73. intent is to override; this hook runs last and last-write-wins.
  74. """
  75. pass