reload_config.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import argparse
  2. import logging
  3. import sys
  4. import requests
  5. from typing import Dict, Any
  6. from gpustack import __version__, __git_commit__
  7. from gpustack.cmd.start import load_config_from_yaml
  8. from gpustack.config.config import Config
  9. from gpustack.utils.envs import get_gpustack_env
  10. from gpustack.utils.config import (
  11. WHITELIST_CONFIG_FIELDS,
  12. coerce_value_by_field,
  13. filter_whitelisted_yaml_config,
  14. )
  15. from gpustack.logging import setup_logging
  16. from gpustack.client.generated_http_client import default_versioned_prefix
  17. logger = logging.getLogger(__name__)
  18. class OptionalBoolAction(argparse.Action):
  19. def __init__(self, option_strings, dest, nargs=None, **kwargs):
  20. if nargs is not None:
  21. raise ValueError("nargs not allowed")
  22. super(OptionalBoolAction, self).__init__(
  23. option_strings, dest, nargs=0, **kwargs
  24. )
  25. self.default = kwargs.get("default")
  26. def __call__(self, parser, namespace, values, option_string=None):
  27. setattr(namespace, self.dest, True)
  28. def setup_reload_config_cmd(subparsers: argparse._SubParsersAction):
  29. parser: argparse.ArgumentParser = subparsers.add_parser(
  30. "reload-config",
  31. help="Reload GPUStack configuration.",
  32. description=("Reload GPUStack configuration via --set, --file, or --list."),
  33. )
  34. parser.add_argument(
  35. "--set",
  36. action="append",
  37. help=(
  38. "Set a single configuration value: --set key=value (key in hyphen-case). "
  39. "Only whitelisted fields are applied. "
  40. "Values are coerced by target field type. "
  41. "Lists accept comma-separated strings "
  42. "(e.g., --set allow-origins=https://a.com,https://b.com). "
  43. "Dicts require JSON string "
  44. "(e.g., --set system-reserved='{\"ram\":2,\"vram\":1}'). "
  45. "Invalid JSON will cause an error and exit."
  46. ),
  47. )
  48. parser.add_argument(
  49. "--file",
  50. type=str,
  51. help="Load configuration from YAML file: --file /path/to/gpustack_config.yaml",
  52. )
  53. parser.add_argument(
  54. "--list",
  55. action=OptionalBoolAction,
  56. help=(
  57. "List whitelisted fields that can be updated, can't use with --set or --file."
  58. ),
  59. default=False,
  60. )
  61. parser.add_argument(
  62. "--api-key",
  63. type=str,
  64. help="API Key to authenticate as admin.",
  65. default=get_gpustack_env("API_KEY"),
  66. )
  67. parser.add_argument(
  68. "--server-port",
  69. type=int,
  70. help="Port of the GPUStack API server to target.",
  71. default=get_gpustack_env("API_PORT"),
  72. )
  73. parser.add_argument(
  74. "--worker-port",
  75. type=int,
  76. help="Port of the GPUStack worker to target.",
  77. default=get_gpustack_env("WORKER_PORT"),
  78. )
  79. parser.set_defaults(func=run)
  80. def run(args):
  81. try:
  82. logger.info("Starting configuration reload...")
  83. logger.info(f"GPUStack version: {__version__} ({__git_commit__})")
  84. if handle_list_mode(args):
  85. return
  86. cfg = parse_args_with_filter(args, {})
  87. payload = {}
  88. for field in WHITELIST_CONFIG_FIELDS:
  89. if hasattr(cfg, field):
  90. value = getattr(cfg, field)
  91. if value is not None:
  92. payload[field] = value
  93. setup_logging(cfg.debug)
  94. apply_runtime_updates(payload, args)
  95. display_config_summary(cfg)
  96. except Exception as e:
  97. logger.error(f"Failed to reload configuration: {e}")
  98. sys.exit(1)
  99. def display_config_summary(cfg):
  100. """Display a summary of the reloaded configuration - only show whitelisted fields."""
  101. logger.info("=== Configuration Reload Summary ===")
  102. for field in WHITELIST_CONFIG_FIELDS:
  103. if hasattr(cfg, field):
  104. value = getattr(cfg, field)
  105. if value is not None:
  106. logger.info(f"- reload: {field} = {value}")
  107. logger.info("Configuration successfully reloaded.")
  108. logger.info("=====================================")
  109. def parse_args_with_filter(args: argparse.Namespace, filtered_changes: Dict[str, Any]):
  110. """
  111. Parse arguments with filtered configuration changes.
  112. This function reuses the logic from start.py but applies whitelist filtering.
  113. """
  114. config_data = {}
  115. # Handle config file if provided
  116. if getattr(args, "file", None):
  117. yaml_data = load_config_from_yaml(args.file)
  118. filtered_yaml_data = filter_whitelisted_yaml_config(yaml_data or {})
  119. config_data.update(filtered_yaml_data)
  120. if getattr(args, "set", None):
  121. for item in args.set:
  122. if "=" not in item:
  123. raise Exception(f"Invalid --set value: {item}. Use key=value")
  124. k, v = item.split("=", 1)
  125. key = k.replace("-", "_")
  126. if key in WHITELIST_CONFIG_FIELDS:
  127. config_data[key] = coerce_value_by_field(key, v)
  128. # Apply filtered command line changes (these override config file)
  129. for key, value in filtered_changes.items():
  130. config_data[key] = value
  131. # Create config with filtered data - only use the filtered config data
  132. # Don't call set_common_options/set_server_options/set_worker_options
  133. # as they would re-apply all command line arguments including blocked ones
  134. return Config(**config_data)
  135. def apply_runtime_updates(
  136. payload: Dict[str, Any],
  137. args: argparse.Namespace,
  138. ):
  139. api_key = getattr(args, "api_key", None)
  140. server_port = getattr(args, "server_port") or 30080
  141. worker_port = getattr(args, "worker_port") or 10150
  142. urls = [
  143. f"http://127.0.0.1:{server_port}{default_versioned_prefix}/config",
  144. f"http://127.0.0.1:{worker_port}{default_versioned_prefix}/config",
  145. ]
  146. for url in urls:
  147. try:
  148. headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
  149. resp = requests.put(url, json=payload, headers=headers)
  150. if resp.status_code == 200:
  151. logger.info(f"Applied runtime config via {url}")
  152. else:
  153. logger.warning(f"Failed to apply config via {url}: {resp.status_code}")
  154. except Exception as e:
  155. logger.warning(f"Failed to apply config via {url}: {e}")
  156. def list_runtime_values(
  157. api_key: str | None = None,
  158. server_port: int | None = None,
  159. worker_port: int | None = None,
  160. ) -> Dict[str, Dict[str, Any]]:
  161. results: Dict[str, Dict[str, Any]] = {}
  162. s_port = server_port or 30080
  163. w_port = worker_port or 10150
  164. endpoints = {
  165. "server": f"http://127.0.0.1:{s_port}{default_versioned_prefix}/config",
  166. "worker": f"http://127.0.0.1:{w_port}{default_versioned_prefix}/config",
  167. }
  168. for scope, url in endpoints.items():
  169. try:
  170. headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
  171. resp = requests.get(url, timeout=2, headers=headers)
  172. if resp.status_code == 200:
  173. results[scope] = resp.json()
  174. except Exception:
  175. continue
  176. return results
  177. def handle_list_mode(args) -> bool:
  178. if not getattr(args, "list", False):
  179. return False
  180. print("Whitelisted fields:")
  181. for field in sorted(WHITELIST_CONFIG_FIELDS):
  182. print(f"- {field.replace('_', '-')}")
  183. runtime_values = list_runtime_values(
  184. api_key=getattr(args, "api_key", None),
  185. server_port=getattr(args, "server_port", None),
  186. worker_port=getattr(args, "worker_port", None),
  187. )
  188. if runtime_values:
  189. print("Current config values:")
  190. for scope, conf in runtime_values.items():
  191. for field in sorted(WHITELIST_CONFIG_FIELDS):
  192. if field in conf and conf[field] is not None:
  193. print(f"- {scope}: {field.replace('_', '-')} = {conf[field]}")
  194. return True