gpustack-runtime 0.1.39.post1__py3-none-any.whl → 0.1.39.post3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ from docker.utils import parse_repository_tag
26
26
  from tqdm import tqdm
27
27
 
28
28
  from .. import envs
29
- from ..logging import debug_log_exception
29
+ from ..logging import debug_log_exception, debug_log_warning
30
30
  from .__types__ import (
31
31
  Container,
32
32
  ContainerCheck,
@@ -34,7 +34,7 @@ from .__types__ import (
34
34
  ContainerMountModeEnum,
35
35
  ContainerProfileEnum,
36
36
  ContainerRestartPolicyEnum,
37
- Deployer,
37
+ EndoscopicDeployer,
38
38
  OperationError,
39
39
  UnsupportedError,
40
40
  WorkloadExecStream,
@@ -46,7 +46,13 @@ from .__types__ import (
46
46
  WorkloadStatusOperation,
47
47
  WorkloadStatusStateEnum,
48
48
  )
49
- from .__utils__ import _MiB, bytes_to_human_readable, replace_image_with, safe_json
49
+ from .__utils__ import (
50
+ _MiB,
51
+ bytes_to_human_readable,
52
+ replace_image_with,
53
+ safe_json,
54
+ sensitive_env_var,
55
+ )
50
56
 
51
57
  if TYPE_CHECKING:
52
58
  from collections.abc import Callable, Generator
@@ -141,7 +147,7 @@ class DockerWorkloadPlan(WorkloadPlan):
141
147
  super().validate_and_default()
142
148
 
143
149
  # Adjust default image namespace if needed.
144
- if namespace := envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_NAMESPACE:
150
+ if namespace := envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_NAMESPACE:
145
151
  self.pause_image = replace_image_with(
146
152
  image=self.pause_image,
147
153
  namespace=namespace,
@@ -296,7 +302,7 @@ Name of the Docker deployer.
296
302
  """
297
303
 
298
304
 
299
- class DockerDeployer(Deployer):
305
+ class DockerDeployer(EndoscopicDeployer):
300
306
  """
301
307
  Deployer implementation for Docker containers.
302
308
  """
@@ -426,12 +432,12 @@ class DockerDeployer(Deployer):
426
432
  tag = tag or "latest"
427
433
  auth_config = None
428
434
  if (
429
- envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_REGISTRY_USERNAME
430
- and envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_REGISTRY_PASSWORD
435
+ envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY_USERNAME
436
+ and envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY_PASSWORD
431
437
  ):
432
438
  auth_config = {
433
- "username": envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_REGISTRY_USERNAME,
434
- "password": envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_REGISTRY_PASSWORD,
439
+ "username": envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY_USERNAME,
440
+ "password": envs.GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY_PASSWORD,
435
441
  }
436
442
 
437
443
  logs = self._client.api.pull(
@@ -1174,39 +1180,29 @@ class DockerDeployer(Deployer):
1174
1180
  super().__init__(_NAME)
1175
1181
  self._client = self._get_client()
1176
1182
 
1177
- def _prepare_create(self):
1183
+ def _prepare_mirrored_deployment(self):
1178
1184
  """
1179
- Prepare for creation.
1185
+ Prepare for mirrored deployment.
1180
1186
 
1181
1187
  """
1182
1188
  # Prepare mirrored deployment if enabled.
1183
1189
  if self._mutate_create_options:
1184
1190
  return
1185
1191
  self._mutate_create_options = lambda o: o
1186
- if not envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_DEPLOYMENT:
1187
- logger.debug("Mirrored deployment disabled")
1188
- return
1189
1192
 
1190
1193
  # Retrieve self-container info.
1191
- ## - Get Container name, default to hostname if not set.
1192
- self_container_id = envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME
1193
- if not self_container_id:
1194
- self_container_id = socket.gethostname()
1195
- logger.warning(
1196
- "Mirrored deployment enabled, but no Container name set, using hostname(%s) instead",
1197
- self_container_id,
1198
- )
1199
1194
  try:
1200
- self_container = self._find_self_container(self_container_id)
1195
+ self_container = self._find_self_container()
1196
+ if not self_container:
1197
+ return
1201
1198
  logger.info(
1202
1199
  "Mirrored deployment enabled, using self Container %s for options mirroring",
1203
- self_container.id[:12],
1200
+ self_container.short_id,
1204
1201
  )
1205
1202
  self_image = self_container.image
1206
1203
  except docker.errors.APIError:
1207
1204
  logger.exception(
1208
- "Mirrored deployment enabled, but failed to get self Container %s, skipping",
1209
- self_container_id,
1205
+ "Mirrored deployment enabled, but failed to get self Container, skipping",
1210
1206
  )
1211
1207
  return
1212
1208
 
@@ -1217,8 +1213,12 @@ class DockerDeployer(Deployer):
1217
1213
  self_container_envs: dict[str, str] = dict(
1218
1214
  item.split("=", 1) for item in self_container.attrs["Config"].get("Env", [])
1219
1215
  )
1220
- self_image_envs: dict[str, str] = dict(
1221
- item.split("=", 1) for item in self_image.attrs["Config"].get("Env", [])
1216
+ self_image_envs: dict[str, str] = (
1217
+ dict(
1218
+ item.split("=", 1) for item in self_image.attrs["Config"].get("Env", [])
1219
+ )
1220
+ if self_image.attrs["Config"]
1221
+ else {}
1222
1222
  )
1223
1223
  mirrored_envs: dict[str, str] = {
1224
1224
  # Filter out gpustack-internal envs and same-as-image envs.
@@ -1406,17 +1406,10 @@ class DockerDeployer(Deployer):
1406
1406
 
1407
1407
  self._mutate_create_options = mutate_create_options
1408
1408
 
1409
- def _find_self_container(
1410
- self,
1411
- self_container_id: str,
1412
- ) -> docker.models.containers.Container:
1409
+ def _find_self_container(self) -> docker.models.containers.Container | None:
1413
1410
  """
1414
1411
  Find the current container if running inside a Docker container.
1415
1412
 
1416
- Args:
1417
- self_container_id:
1418
- The container name or ID to find.
1419
-
1420
1413
  Returns:
1421
1414
  The Docker container if found, None otherwise.
1422
1415
 
@@ -1424,38 +1417,54 @@ class DockerDeployer(Deployer):
1424
1417
  If failed to find itself.
1425
1418
 
1426
1419
  """
1427
- if envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME:
1428
- # Directly get container by name or ID.
1429
- return self._client.containers.get(self_container_id)
1430
-
1431
- # Find containers that matches the hostname.
1432
- containers: list[docker.models.containers.Container] = []
1433
- for c in self._client.containers.list():
1434
- # Ignore workload containers with host network enabled.
1435
- if _LABEL_WORKLOAD in c.labels:
1436
- continue
1437
- # Ignore containers that do not match the hostname.
1438
- if c.attrs["Config"].get("Hostname", "") != self_container_id:
1439
- continue
1440
- # Ignore containers that do not match the filter labels.
1441
- if envs.GPUSTACK_RUNTIME_DOCKER_MIRRORED_NAME_FILTER_LABELS and any(
1442
- c.labels.get(k) != v
1443
- for k, v in envs.GPUSTACK_RUNTIME_DOCKER_MIRRORED_NAME_FILTER_LABELS.items()
1444
- ):
1445
- continue
1446
- containers.append(c)
1420
+ if not envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_DEPLOYMENT:
1421
+ logger.debug("Mirrored deployment disabled")
1422
+ return None
1447
1423
 
1448
- # Validate found containers.
1449
- if len(containers) != 1:
1450
- msg = (
1451
- f"Found multiple Containers with the same hostname {self_container_id}, "
1452
- if len(containers) > 1
1453
- else f"Not found Container with hostname {self_container_id}, "
1454
- "please use `--env GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME=...` to specify the exact container name"
1424
+ # Get container ID or hostname.
1425
+ self_container_id = envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME
1426
+ if not self_container_id:
1427
+ self_container_id = socket.gethostname()
1428
+ debug_log_warning(
1429
+ logger,
1430
+ "Mirrored deployment enabled, but no Container name set, using hostname(%s) instead",
1431
+ self_container_id,
1455
1432
  )
1456
- raise docker.errors.NotFound(msg)
1457
1433
 
1458
- return containers[0]
1434
+ if envs.GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME:
1435
+ # Directly get container.
1436
+ self_container = self._client.containers.get(self_container_id)
1437
+ else:
1438
+ # Find containers that matches the hostname.
1439
+ containers: list[docker.models.containers.Container] = []
1440
+ for c in self._client.containers.list():
1441
+ # Ignore workload containers with host network enabled.
1442
+ if _LABEL_WORKLOAD in c.labels:
1443
+ continue
1444
+ # Ignore containers that do not match the hostname.
1445
+ if c.attrs["Config"].get("Hostname", "") != self_container_id:
1446
+ continue
1447
+ # Ignore containers that do not match the filter labels.
1448
+ if envs.GPUSTACK_RUNTIME_DOCKER_MIRRORED_NAME_FILTER_LABELS and any(
1449
+ c.labels.get(k) != v
1450
+ for k, v in envs.GPUSTACK_RUNTIME_DOCKER_MIRRORED_NAME_FILTER_LABELS.items()
1451
+ ):
1452
+ continue
1453
+ containers.append(c)
1454
+
1455
+ # Validate found containers.
1456
+ if len(containers) != 1:
1457
+ msg = (
1458
+ f"Found multiple Containers with the same hostname {self_container_id}, "
1459
+ if len(containers) > 1
1460
+ else f"Not found Container with hostname {self_container_id}, "
1461
+ "please use `--env GPUSTACK_RUNTIME_DEPLOY_MIRRORED_NAME=...` to specify the exact Container name"
1462
+ )
1463
+ raise docker.errors.NotFound(msg)
1464
+
1465
+ self_container = containers[0]
1466
+
1467
+ return self_container
1459
1468
 
1460
1469
  @_supported
1461
1470
  def _create(self, workload: WorkloadPlan):
@@ -1481,7 +1490,7 @@ class DockerDeployer(Deployer):
1481
1490
  msg = f"Invalid workload type: {type(workload)}"
1482
1491
  raise TypeError(msg)
1483
1492
 
1484
- self._prepare_create()
1493
+ self._prepare_mirrored_deployment()
1485
1494
 
1486
1495
  if isinstance(workload, WorkloadPlan):
1487
1496
  workload = DockerWorkloadPlan(**workload.__dict__)
@@ -1881,6 +1890,211 @@ class DockerDeployer(Deployer):
1881
1890
  return output
1882
1891
  return DockerWorkloadExecStream(output)
1883
1892
 
1893
+ @_supported
1894
+ def _inspect(
1895
+ self,
1896
+ name: WorkloadName,
1897
+ namespace: WorkloadNamespace | None = None,
1898
+ ) -> str | None:
1899
+ """
1900
+ Inspect a Docker workload.
1901
+
1902
+ Args:
1903
+ name:
1904
+ The name of the workload.
1905
+ namespace:
1906
+ The namespace of the workload.
1907
+
1908
+ Returns:
1909
+ The inspection result as a JSON string. None if not found.
1910
+
1911
+ Raises:
1912
+ UnsupportedError:
1913
+ If Docker is not supported in the current environment.
1914
+ OperationError:
1915
+ If the Docker workload fails to inspect.
1916
+
1917
+ """
1918
+ workload = self._get(name=name, namespace=namespace)
1919
+ if not workload:
1920
+ return None
1921
+
1922
+ d_containers = getattr(workload, "_d_containers", [])
1923
+ if not d_containers:
1924
+ return None
1925
+
1926
+ result = []
1927
+ for c in d_containers:
1928
+ c_attrs = c.attrs
1929
+ # Mask sensitive environment variables
1930
+ if "Env" in c_attrs["Config"]:
1931
+ for i, env in enumerate(c_attrs["Config"]["Env"] or []):
1932
+ env_name, _ = env.split("=", maxsplit=1)
1933
+ if sensitive_env_var(env_name):
1934
+ c_attrs["Config"]["Env"][i] = f"{env_name}=******"
1935
+ result.append(c_attrs)
1936
+ return safe_json(result, indent=2)
1937
+
1938
+ def _find_self_container_for_endoscopy(self) -> docker.models.containers.Container:
1939
+ """
1940
+ Find the self container for endoscopy.
1941
+ Only works in mirrored deployment mode.
1942
+
1943
+ Returns:
1944
+ The self container object.
1945
+
1946
+ Raises:
1947
+ UnsupportedError:
1948
+ If endoscopy is not supported in the current environment.
1949
+
1950
+ """
1951
+ try:
1952
+ self_container = self._find_self_container()
1953
+ except docker.errors.APIError as e:
1954
+ msg = "Endoscopy is not supported in the current environment: Mirrored deployment enabled, but failed to get self Container"
1955
+ raise UnsupportedError(msg) from e
1956
+ except Exception as e:
1957
+ msg = "Endoscopy is not supported in the current environment: Failed to get self Container"
1958
+ raise UnsupportedError(msg) from e
1959
+
1960
+ if not self_container:
1961
+ msg = "Endoscopy is not supported in the current environment: Mirrored deployment disabled"
1962
+ raise UnsupportedError(msg)
1963
+ return self_container
1964
+
1965
+ def _endoscopic_logs(
1966
+ self,
1967
+ timestamps: bool = False,
1968
+ tail: int | None = None,
1969
+ since: int | None = None,
1970
+ follow: bool = False,
1971
+ ) -> Generator[bytes | str, None, None] | bytes | str:
1972
+ """
1973
+ Get the logs of the deployer itself.
1974
+ Only works in mirrored deployment mode.
1975
+
1976
+ Args:
1977
+ timestamps:
1978
+ Show timestamps in the logs.
1979
+ tail:
1980
+ Number of lines to show from the end of the logs.
1981
+ since:
1982
+ Show logs since the given epoch in seconds.
1983
+ follow:
1984
+ Whether to follow the logs.
1985
+
1986
+ Returns:
1987
+ The logs as a byte string or a generator yielding byte strings if follow is True.
1988
+
1989
+ Raises:
1990
+ UnsupportedError:
1991
+ If endoscopy is not supported in the current environment.
1992
+ OperationError:
1993
+ If the deployer fails to get logs.
1994
+
1995
+ """
1996
+ self_container = self._find_self_container_for_endoscopy()
1997
+
1998
+ logs_options = {
1999
+ "timestamps": timestamps,
2000
+ "tail": tail if tail >= 0 else None,
2001
+ "since": since,
2002
+ "follow": follow,
2003
+ }
2004
+
2005
+ try:
2006
+ output = self_container.logs(
2007
+ stream=follow,
2008
+ **logs_options,
2009
+ )
2010
+ except docker.errors.APIError as e:
2011
+ msg = f"Failed to fetch logs for self Container {self_container.short_id}{_detail_api_call_error(e)}"
2012
+ raise OperationError(msg) from e
2013
+ else:
2014
+ return output
2015
+
2016
+ def _endoscopic_exec(
2017
+ self,
2018
+ detach: bool = True,
2019
+ command: list[str] | None = None,
2020
+ args: list[str] | None = None,
2021
+ ) -> WorkloadExecStream | bytes | str:
2022
+ """
2023
+ Execute a command in the deployer itself.
2024
+ Only works in mirrored deployment mode.
2025
+
2026
+ Args:
2027
+ detach:
2028
+ Whether to detach from the command.
2029
+ command:
2030
+ The command to execute.
2031
+ If not specified, use /bin/sh and implicitly attach.
2032
+ args:
2033
+ The arguments to pass to the command.
2034
+
2035
+ Returns:
2036
+ If detach is False, return a WorkloadExecStream.
2037
+ otherwise, return the output of the command as a byte string or string.
2038
+
2039
+ Raises:
2040
+ UnsupportedError:
2041
+ If endoscopy is not supported in the current environment.
2042
+ OperationError:
2043
+ If the deployer fails to execute the command.
2044
+
2045
+ """
2046
+ self_container = self._find_self_container_for_endoscopy()
2047
+
2048
+ attach = not detach or not command
2049
+ exec_options = {
2050
+ "stdout": True,
2051
+ "stderr": True,
2052
+ "stdin": attach,
2053
+ "socket": attach,
2054
+ "tty": attach,
2055
+ "cmd": [*command, *(args or [])] if command else ["/bin/sh"],
2056
+ }
2057
+
2058
+ try:
2059
+ _, output = self_container.exec_run(
2060
+ detach=False,
2061
+ **exec_options,
2062
+ )
2063
+ except docker.errors.APIError as e:
2064
+ msg = f"Failed to exec command in self Container {self_container.short_id}{_detail_api_call_error(e)}"
2065
+ raise OperationError(msg) from e
2066
+ else:
2067
+ if not attach:
2068
+ return output
2069
+ return DockerWorkloadExecStream(output)
2070
+
2071
+ def _endoscopic_inspect(self) -> str:
2072
+ """
2073
+ Inspect the deployer itself.
2074
+ Only works in mirrored deployment mode.
2075
+
2076
+ Returns:
2077
+ The inspection result.
2078
+
2079
+ Raises:
2080
+ UnsupportedError:
2081
+ If endoscopy is not supported in the current environment.
2082
+ OperationError:
2083
+ If the deployer fails to execute the command.
2084
+
2085
+ """
2086
+ self_container = self._find_self_container_for_endoscopy()
2087
+
2088
+ c_attrs = self_container.attrs
2089
+ # Mask sensitive environment variables
2090
+ if "Env" in c_attrs["Config"]:
2091
+ for i, env in enumerate(c_attrs["Config"]["Env"] or []):
2092
+ env_name, _ = env.split("=", maxsplit=1)
2093
+ if sensitive_env_var(env_name):
2094
+ c_attrs["Config"]["Env"][i] = f"{env_name}=******"
2095
+
2096
+ return safe_json(c_attrs, indent=2)
2097
+
1884
2098
 
1885
2099
  def _has_restart_policy(
1886
2100
  container: docker.models.containers.Container,