sagemaker-hyperpod 3.0.0__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.
Files changed (66) hide show
  1. sagemaker/__init__.py +0 -0
  2. sagemaker/hyperpod/__init__.py +2 -0
  3. sagemaker/hyperpod/cli/__init__.py +0 -0
  4. sagemaker/hyperpod/cli/clients/__init__.py +12 -0
  5. sagemaker/hyperpod/cli/clients/kubernetes_client.py +365 -0
  6. sagemaker/hyperpod/cli/commands/__init__.py +0 -0
  7. sagemaker/hyperpod/cli/commands/cluster.py +627 -0
  8. sagemaker/hyperpod/cli/commands/inference.py +659 -0
  9. sagemaker/hyperpod/cli/commands/training.py +347 -0
  10. sagemaker/hyperpod/cli/constants/__init__.py +0 -0
  11. sagemaker/hyperpod/cli/constants/command_constants.py +105 -0
  12. sagemaker/hyperpod/cli/constants/exception_constants.py +13 -0
  13. sagemaker/hyperpod/cli/constants/hyperpod_instance_types.py +100 -0
  14. sagemaker/hyperpod/cli/constants/kueue_constants.py +17 -0
  15. sagemaker/hyperpod/cli/constants/pytorch_constants.py +15 -0
  16. sagemaker/hyperpod/cli/hyp_cli.py +132 -0
  17. sagemaker/hyperpod/cli/inference_utils.py +121 -0
  18. sagemaker/hyperpod/cli/service/__init__.py +12 -0
  19. sagemaker/hyperpod/cli/service/cancel_training_job.py +70 -0
  20. sagemaker/hyperpod/cli/service/discover_namespaces.py +108 -0
  21. sagemaker/hyperpod/cli/service/exec_command.py +75 -0
  22. sagemaker/hyperpod/cli/service/get_logs.py +136 -0
  23. sagemaker/hyperpod/cli/service/get_namespaces.py +59 -0
  24. sagemaker/hyperpod/cli/service/get_training_job.py +110 -0
  25. sagemaker/hyperpod/cli/service/list_pods.py +138 -0
  26. sagemaker/hyperpod/cli/service/list_training_jobs.py +178 -0
  27. sagemaker/hyperpod/cli/service/self_subject_access_review.py +47 -0
  28. sagemaker/hyperpod/cli/telemetry/__init__.py +12 -0
  29. sagemaker/hyperpod/cli/telemetry/user_agent.py +36 -0
  30. sagemaker/hyperpod/cli/templates/__init__.py +12 -0
  31. sagemaker/hyperpod/cli/templates/k8s_pytorch_job_template.py +74 -0
  32. sagemaker/hyperpod/cli/training_utils.py +200 -0
  33. sagemaker/hyperpod/cli/utils.py +183 -0
  34. sagemaker/hyperpod/cli/validators/__init__.py +12 -0
  35. sagemaker/hyperpod/cli/validators/cluster_validator.py +52 -0
  36. sagemaker/hyperpod/cli/validators/job_validator.py +346 -0
  37. sagemaker/hyperpod/cli/validators/validator.py +75 -0
  38. sagemaker/hyperpod/common/__init__.py +0 -0
  39. sagemaker/hyperpod/common/config/__init__.py +1 -0
  40. sagemaker/hyperpod/common/config/metadata.py +18 -0
  41. sagemaker/hyperpod/common/utils.py +299 -0
  42. sagemaker/hyperpod/inference/__init__.py +11 -0
  43. sagemaker/hyperpod/inference/config/__init__.py +0 -0
  44. sagemaker/hyperpod/inference/config/constants.py +18 -0
  45. sagemaker/hyperpod/inference/config/hp_endpoint_config.py +767 -0
  46. sagemaker/hyperpod/inference/config/hp_jumpstart_endpoint_config.py +572 -0
  47. sagemaker/hyperpod/inference/hp_endpoint.py +203 -0
  48. sagemaker/hyperpod/inference/hp_endpoint_base.py +223 -0
  49. sagemaker/hyperpod/inference/hp_jumpstart_endpoint.py +232 -0
  50. sagemaker/hyperpod/observability/MonitoringConfig.py +10 -0
  51. sagemaker/hyperpod/observability/__init__.py +0 -0
  52. sagemaker/hyperpod/observability/constants.py +2 -0
  53. sagemaker/hyperpod/observability/utils.py +64 -0
  54. sagemaker/hyperpod/training/__init__.py +9 -0
  55. sagemaker/hyperpod/training/config/__init__.py +1 -0
  56. sagemaker/hyperpod/training/config/hyperpod_pytorch_job_config.py +2977 -0
  57. sagemaker/hyperpod/training/config/hyperpod_pytorch_job_status.py +3083 -0
  58. sagemaker/hyperpod/training/hyperpod_pytorch_job.py +253 -0
  59. sagemaker_hyperpod-3.0.0.dist-info/METADATA +534 -0
  60. sagemaker_hyperpod-3.0.0.dist-info/RECORD +66 -0
  61. sagemaker_hyperpod-3.0.0.dist-info/WHEEL +5 -0
  62. sagemaker_hyperpod-3.0.0.dist-info/entry_points.txt +2 -0
  63. sagemaker_hyperpod-3.0.0.dist-info/licenses/LICENSE +175 -0
  64. sagemaker_hyperpod-3.0.0.dist-info/licenses/NOTICE +1 -0
  65. sagemaker_hyperpod-3.0.0.dist-info/top_level.txt +1 -0
  66. sagemaker_hyperpod-3.0.0.dist-info/zip-safe +1 -0
@@ -0,0 +1,132 @@
1
+ import click
2
+ import yaml
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from pydantic import BaseModel, ValidationError, Field
7
+ from typing import Optional
8
+
9
+ from sagemaker.hyperpod.cli.commands.cluster import list_cluster, set_cluster_context, get_cluster_context, \
10
+ get_monitoring
11
+ from sagemaker.hyperpod.cli.commands.training import (
12
+ pytorch_create,
13
+ list_jobs,
14
+ pytorch_describe,
15
+ pytorch_delete,
16
+ pytorch_list_pods,
17
+ pytorch_get_logs,
18
+ )
19
+ from sagemaker.hyperpod.cli.commands.inference import (
20
+ js_create,
21
+ custom_create,
22
+ custom_invoke,
23
+ js_list,
24
+ custom_list,
25
+ js_describe,
26
+ custom_describe,
27
+ js_delete,
28
+ custom_delete,
29
+ js_list_pods,
30
+ custom_list_pods,
31
+ js_get_logs,
32
+ custom_get_logs,
33
+ js_get_operator_logs,
34
+ custom_get_operator_logs,
35
+ )
36
+
37
+
38
+ @click.group()
39
+ def cli():
40
+ pass
41
+
42
+
43
+ class CLICommand(click.Group):
44
+ pass
45
+
46
+
47
+ @cli.group(cls=CLICommand)
48
+ def create():
49
+ """Create a jumpstart model endpoint, a custom model endpoint, or a pytorch job."""
50
+ pass
51
+
52
+
53
+ @cli.group(cls=CLICommand)
54
+ def list():
55
+ """List all jumpstart model endpoints, custom model endpoints, or pytorch jobs."""
56
+ pass
57
+
58
+
59
+ @cli.group(cls=CLICommand)
60
+ def describe():
61
+ """Describe a jumpstart model endpoint, a custom model endpoint, or a pytorch job."""
62
+ pass
63
+
64
+
65
+ @cli.group(cls=CLICommand)
66
+ def delete():
67
+ """Delete a jumpstart model endpoint, a custom model endpoint, or a pytorch job."""
68
+ pass
69
+
70
+
71
+ @cli.group(cls=CLICommand)
72
+ def list_pods():
73
+ """List all pods for jumpstart model endpoint, custom model endpoint or pytorch jobs."""
74
+ pass
75
+
76
+
77
+ @cli.group(cls=CLICommand)
78
+ def get_logs():
79
+ """Get specific pod logs for a jumpstart model endpoint, custom model endpoint or pytorch job."""
80
+ pass
81
+
82
+
83
+ @cli.group(cls=CLICommand)
84
+ def invoke():
85
+ """Invoke a jumpstart model endpoint or a custom model endpoint."""
86
+ pass
87
+
88
+
89
+ @cli.group(cls=CLICommand)
90
+ def get_operator_logs():
91
+ """Get operator logs for jumpstart model endpoint, or custom model endpoint."""
92
+ pass
93
+
94
+
95
+ create.add_command(pytorch_create)
96
+ create.add_command(js_create)
97
+ create.add_command(custom_create)
98
+
99
+ list.add_command(list_jobs)
100
+ list.add_command(js_list)
101
+ list.add_command(custom_list)
102
+
103
+ describe.add_command(pytorch_describe)
104
+ describe.add_command(js_describe)
105
+ describe.add_command(custom_describe)
106
+
107
+ delete.add_command(pytorch_delete)
108
+ delete.add_command(js_delete)
109
+ delete.add_command(custom_delete)
110
+
111
+ list_pods.add_command(pytorch_list_pods)
112
+ list_pods.add_command(js_list_pods)
113
+ list_pods.add_command(custom_list_pods)
114
+
115
+ get_logs.add_command(pytorch_get_logs)
116
+ get_logs.add_command(js_get_logs)
117
+ get_logs.add_command(custom_get_logs)
118
+
119
+ get_operator_logs.add_command(js_get_operator_logs)
120
+ get_operator_logs.add_command(custom_get_operator_logs)
121
+
122
+ invoke.add_command(custom_invoke)
123
+ invoke.add_command(custom_invoke, name="hyp-jumpstart-endpoint")
124
+
125
+ cli.add_command(list_cluster)
126
+ cli.add_command(set_cluster_context)
127
+ cli.add_command(get_cluster_context)
128
+ cli.add_command(get_monitoring)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ cli()
@@ -0,0 +1,121 @@
1
+ import json
2
+ import pkgutil
3
+ import click
4
+ from typing import Callable, Optional, Mapping, Type
5
+
6
+
7
+ def load_schema_for_version(version: str, schema_pkg: str) -> dict:
8
+ ver_pkg = f"{schema_pkg}.v{version.replace('.', '_')}"
9
+ raw = pkgutil.get_data(ver_pkg, "schema.json")
10
+ if raw is None:
11
+ raise click.ClickException(f"Could not load schema.json for version {version}")
12
+ return json.loads(raw)
13
+
14
+
15
+ def generate_click_command(
16
+ *,
17
+ version_key: Optional[str] = None,
18
+ schema_pkg: str = "hyperpod_jumpstart_inference_template",
19
+ registry: Mapping[str, Type] = None,
20
+ ) -> Callable:
21
+ if registry is None:
22
+ raise ValueError("You must pass a registry mapping version→Model")
23
+
24
+ def decorator(func: Callable) -> Callable:
25
+ # Parser for the single JSON‐dict env var flag
26
+ def _parse_json_flag(ctx, param, value):
27
+ if value is None:
28
+ return None
29
+ try:
30
+ return json.loads(value)
31
+ except json.JSONDecodeError as e:
32
+ raise click.BadParameter(f"{param.name!r} must be valid JSON: {e}")
33
+
34
+ # 1) the wrapper click actually invokes
35
+ def wrapped_func(*args, **kwargs):
36
+ namespace = kwargs.pop("namespace", None)
37
+ version = version_key or kwargs.pop("version", "1.0")
38
+
39
+ Model = registry.get(version)
40
+ if Model is None:
41
+ raise click.ClickException(f"Unsupported schema version: {version}")
42
+
43
+ flat = Model(**kwargs)
44
+ domain = flat.to_domain()
45
+ return func(namespace, version, domain)
46
+
47
+ # 2) inject the special JSON‐env flag before everything else
48
+ wrapped_func = click.option(
49
+ "--env",
50
+ callback=_parse_json_flag,
51
+ type=str,
52
+ default=None,
53
+ help=(
54
+ "JSON object of environment variables, e.g. "
55
+ '\'{"VAR1":"foo","VAR2":"bar"}\''
56
+ ),
57
+ metavar="JSON",
58
+ )(wrapped_func)
59
+
60
+ wrapped_func = click.option(
61
+ "--dimensions",
62
+ callback=_parse_json_flag,
63
+ type=str,
64
+ default=None,
65
+ help=("JSON object of dimensions, e.g. " '\'{"VAR1":"foo","VAR2":"bar"}\''),
66
+ metavar="JSON",
67
+ )(wrapped_func)
68
+
69
+ wrapped_func = click.option(
70
+ "--resources-limits",
71
+ callback=_parse_json_flag,
72
+ help='JSON object of resource limits, e.g. \'{"cpu":"2","memory":"4Gi"}\'',
73
+ metavar="JSON",
74
+ )(wrapped_func)
75
+
76
+ wrapped_func = click.option(
77
+ "--resources-requests",
78
+ callback=_parse_json_flag,
79
+ help='JSON object of resource requests, e.g. \'{"cpu":"1","memory":"2Gi"}\'',
80
+ metavar="JSON",
81
+ )(wrapped_func)
82
+
83
+ # 3) auto-inject all schema.json fields
84
+ schema = load_schema_for_version(version_key or "1.0", schema_pkg)
85
+ props = schema.get("properties", {})
86
+ reqs = set(schema.get("required", []))
87
+
88
+ for name, spec in reversed(list(props.items())):
89
+ if name in (
90
+ "version",
91
+ "env",
92
+ "dimensions",
93
+ "resources_limits",
94
+ "resources_requests",
95
+ ):
96
+ continue
97
+
98
+ # infer click type
99
+ if "enum" in spec:
100
+ ctype = click.Choice(spec["enum"])
101
+ if spec.get("type") == "integer":
102
+ ctype = int
103
+ elif spec.get("type") == "number":
104
+ ctype = float
105
+ elif spec.get("type") == "boolean":
106
+ ctype = bool
107
+ else:
108
+ ctype = str
109
+
110
+ wrapped_func = click.option(
111
+ f"--{name.replace('_','-')}",
112
+ required=(name in reqs),
113
+ default=spec.get("default", None),
114
+ show_default=("default" in spec),
115
+ type=ctype,
116
+ help=spec.get("description", ""),
117
+ )(wrapped_func)
118
+
119
+ return wrapped_func
120
+
121
+ return decorator
@@ -0,0 +1,12 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
@@ -0,0 +1,70 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ from typing import Optional
14
+ import subprocess
15
+
16
+ from sagemaker.hyperpod.cli.clients.kubernetes_client import (
17
+ KubernetesClient,
18
+ )
19
+ from kubernetes.client.rest import ApiException
20
+
21
+ from sagemaker.hyperpod.cli.constants.pytorch_constants import (
22
+ PYTORCH_CUSTOM_OBJECT_GROUP,
23
+ PYTORCH_CUSTOM_OBJECT_PLURAL
24
+ )
25
+ from sagemaker.hyperpod.cli.service.discover_namespaces import DiscoverNamespaces
26
+ from kubernetes.client import (
27
+ V1ResourceAttributes
28
+ )
29
+
30
+ class CancelTrainingJob:
31
+ def __init__(self):
32
+ return
33
+
34
+ def cancel_training_job(self, job_name: str, namespace: Optional[str]):
35
+ """
36
+ Cancel training job provided by the user in the specified namespace.
37
+ If namespace is not provided job is canceled from the default namespace in user context
38
+ """
39
+
40
+ k8s_client = KubernetesClient()
41
+
42
+ if not namespace:
43
+ resource_attributes_template = V1ResourceAttributes(
44
+ verb="delete",
45
+ group=PYTORCH_CUSTOM_OBJECT_GROUP,
46
+ resource=PYTORCH_CUSTOM_OBJECT_PLURAL,
47
+ )
48
+ namespace = DiscoverNamespaces().discover_accessible_namespace(
49
+ resource_attributes_template
50
+ )
51
+ try:
52
+ result = k8s_client.delete_training_job(
53
+ job_name=job_name, namespace=namespace
54
+ )
55
+ except ApiException as e:
56
+ raise RuntimeError(f"Unexpected API error: {e.reason} ({e.status})")
57
+
58
+ helm_chart_cleanup_command = [
59
+ "helm",
60
+ "uninstall",
61
+ job_name,
62
+ "--namespace",
63
+ namespace,
64
+ ]
65
+
66
+ if result.get("status") and result.get("status") == "Success":
67
+ subprocess.run(helm_chart_cleanup_command, capture_output=True, text=True)
68
+ return None
69
+ else:
70
+ return result
@@ -0,0 +1,108 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ import concurrent
14
+ import sys
15
+ import copy
16
+ from kubernetes.client.rest import ApiException
17
+ from sagemaker.hyperpod.cli.clients.kubernetes_client import KubernetesClient
18
+ from sagemaker.hyperpod.cli.service.get_namespaces import GetNamespaces
19
+ from sagemaker.hyperpod.cli.service.self_subject_access_review import SelfSubjectAccessReview
20
+ from sagemaker.hyperpod.cli.utils import setup_logger
21
+
22
+
23
+ logger = setup_logger(__name__)
24
+
25
+ class DiscoverNamespaces:
26
+ def __init__(self):
27
+ return
28
+
29
+ def discover_accessible_namespace(self, resource_attributes_template, only_sm_managed=True):
30
+ """
31
+ Discover the accessible namespaces
32
+ """
33
+ k8s_client = KubernetesClient()
34
+ context_namespace = k8s_client.get_current_context_namespace()
35
+
36
+ # If the namespace is explicitly set by user, take the namespace from the config instead
37
+ # of discovering automatically
38
+ if context_namespace is not None:
39
+ return context_namespace
40
+
41
+ try:
42
+ if only_sm_managed:
43
+ namespaces = GetNamespaces().get_sagemaker_managed_namespaces()
44
+ else:
45
+ namespaces = GetNamespaces().get_namespaces()
46
+
47
+ discovered_namespaces = self.get_namespaces_by_checking_access_permission(
48
+ namespaces,
49
+ resource_attributes_template,
50
+ )
51
+
52
+ if len(discovered_namespaces) == 0:
53
+ logger.error("Found no accessible namespaces. Please ask for cluster admin for assistance or specify value of namespace explicitly in the command.")
54
+ sys.exit(1)
55
+ if len(discovered_namespaces) > 1:
56
+ logger.error(f"Found more than 1 accessible namespaces {discovered_namespaces}. Please specify value of namespace explicitly in the command.")
57
+ sys.exit(1)
58
+
59
+ if len(discovered_namespaces) == 1:
60
+ logger.info(f"Found accessible namespace: {discovered_namespaces}")
61
+ return discovered_namespaces[0]
62
+ except ApiException as e:
63
+ if e.status == 403:
64
+ logger.error("Got access denied error when discovering accessible namespaces, please specify the '--namespace' parameter in the command and try again.")
65
+ sys.exit(1)
66
+
67
+
68
+ def get_namespaces_by_checking_access_permission(
69
+ self,
70
+ namespaces,
71
+ resource_attributes_template,
72
+ max_workers=10
73
+ ):
74
+ """
75
+ Get the accessible namespaces by performing the SelfSubjectAccessReview. For each of the namespace,
76
+ check if user has specified permission to it, if the answer is NO, the namespace will be skipped.
77
+
78
+ Performing access check can take quite long if the number of namespaces is large. Thus the implementation
79
+ leverages the multi-threading to ensure that multiple access check can be performed in parallel.
80
+ """
81
+ subject_access_review = SelfSubjectAccessReview()
82
+ accessible_namespaces = list()
83
+ resource_attributes = list()
84
+
85
+ for namespace in namespaces:
86
+ resource_attribute = copy.deepcopy(resource_attributes_template)
87
+ resource_attribute.namespace = namespace
88
+ resource_attributes.append(resource_attribute)
89
+
90
+ # Multi-thread the self subject access review to improve the performance
91
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
92
+ futures = {
93
+ executor.submit(
94
+ subject_access_review.self_subject_access_review, resource_attribute
95
+ ) for resource_attribute in resource_attributes
96
+ }
97
+
98
+ for future in concurrent.futures.as_completed(futures):
99
+ try:
100
+ response = future.result()
101
+ if response.status.allowed:
102
+ accessible_namespaces.append(
103
+ response.spec.resource_attributes.namespace
104
+ )
105
+ except Exception as e:
106
+ raise(e)
107
+
108
+ return accessible_namespaces
@@ -0,0 +1,75 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ from typing import Optional
14
+
15
+ from sagemaker.hyperpod.cli.clients.kubernetes_client import (
16
+ KubernetesClient,
17
+ )
18
+ from sagemaker.hyperpod.cli.service.list_pods import (
19
+ ListPods,
20
+ )
21
+
22
+ from kubernetes.client.rest import ApiException
23
+
24
+
25
+ class ExecCommand:
26
+ def __init__(self):
27
+ return
28
+
29
+ def exec_command(
30
+ self,
31
+ job_name: str,
32
+ pod_name: Optional[str],
33
+ namespace: Optional[str],
34
+ all_pods: Optional[bool],
35
+ bash_command: tuple,
36
+ ):
37
+ bash_command_str: str = " ".join(bash_command)
38
+
39
+ k8s_client = KubernetesClient()
40
+ list_pods_service = ListPods()
41
+
42
+ if not namespace:
43
+ namespace = k8s_client.get_current_context_namespace()
44
+
45
+ pods_for_training_job = list_pods_service.list_pods_for_training_job(
46
+ job_name, namespace, False
47
+ )
48
+
49
+ try:
50
+ if all_pods:
51
+ output = ""
52
+ for pod in pods_for_training_job:
53
+ output += pod + "\n"
54
+ output += (
55
+ k8s_client.exec_command_on_pod(
56
+ pod,
57
+ namespace,
58
+ bash_command_str,
59
+ )
60
+ + "\n"
61
+ )
62
+ output += "\n"
63
+ return output
64
+ else:
65
+ if pod_name not in pods_for_training_job:
66
+ raise RuntimeError(
67
+ f"Given pod name {pod_name} is not associated with training job {job_name} in namespace {namespace}"
68
+ )
69
+ return k8s_client.exec_command_on_pod(
70
+ pod_name,
71
+ namespace,
72
+ bash_command_str,
73
+ )
74
+ except ApiException as e:
75
+ raise RuntimeError(f"Unexpected API error: {e.reason} ({e.status})")
@@ -0,0 +1,136 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ from typing import Optional
14
+ import boto3
15
+
16
+ from sagemaker.hyperpod.cli.clients.kubernetes_client import (
17
+ KubernetesClient,
18
+ )
19
+ from sagemaker.hyperpod.cli.service.discover_namespaces import DiscoverNamespaces
20
+ from sagemaker.hyperpod.cli.service.list_pods import (
21
+ ListPods,
22
+ )
23
+ from sagemaker.hyperpod.cli.utils import (
24
+ get_eks_cluster_name,
25
+ get_hyperpod_cluster_region,
26
+ validate_region_and_cluster_name,
27
+ )
28
+ from kubernetes.client.rest import ApiException
29
+ from kubernetes.client import V1ResourceAttributes
30
+ import re
31
+
32
+ AMAZON_ClOUDWATCH_OBSERVABILITY = "amazon-cloudwatch-observability"
33
+ CONTAINER_INSIGHTS_LOG_REGEX_PATTERN = "https:\/\/([a-z0-9-]+).console.aws.amazon.com\/cloudwatch\/home\?region=([a-z0-9-]+)#logsV2:log-groups\/log-group\/\$252Faws\$252Fcontainerinsights\$252F([a-zA-Z0-9-]+)\$252Fapplication\/log-events\/([a-z0-9-]+)-application.var.log.containers.([a-z0-9-]+)_([a-z0-9-]+)_([a-z0-9-]+)-([a-z0-9-]+).log"
34
+
35
+ class GetLogs:
36
+ def __init__(self):
37
+ return
38
+
39
+ def get_training_job_logs(
40
+ self,
41
+ job_name: str,
42
+ pod_name: str,
43
+ namespace: Optional[str],
44
+ ):
45
+ """
46
+ Get logs for pod asscoited with the training job
47
+ """
48
+ k8s_client = KubernetesClient()
49
+ list_pods_service = ListPods()
50
+
51
+ if not namespace:
52
+ resource_attributes_template = V1ResourceAttributes(
53
+ verb="get",
54
+ group="",
55
+ resource="pods",
56
+ subresource="log",
57
+ )
58
+ namespace = DiscoverNamespaces().discover_accessible_namespace(
59
+ resource_attributes_template
60
+ )
61
+
62
+ try:
63
+ pods_for_training_job = list_pods_service.list_pods_for_training_job(
64
+ job_name, namespace, False
65
+ )
66
+ if pod_name not in pods_for_training_job:
67
+ raise RuntimeError(
68
+ f"Given pod name {pod_name} is not associated with training job {job_name} in namespace {namespace}"
69
+ )
70
+ return k8s_client.get_logs_for_pod(pod_name, namespace)
71
+ except ApiException as e:
72
+ raise RuntimeError(f"Unexpected API error: {e.reason} ({e.status})")
73
+
74
+ def generate_cloudwatch_link(
75
+ self,
76
+ pod_name: str,
77
+ namespace: Optional[str],
78
+ ):
79
+ eks_cluster_name = get_eks_cluster_name()
80
+ region = get_hyperpod_cluster_region()
81
+
82
+ if self.is_container_insights_addon_enabled(eks_cluster_name):
83
+ k8s_client = KubernetesClient()
84
+
85
+ # pod_details is a V1Pod object
86
+ pod_details = k8s_client.get_pod_details(pod_name, namespace)
87
+
88
+ # get node name
89
+ if pod_details.spec and pod_details.spec.node_name:
90
+ node_name = pod_details.spec.node_name
91
+ else:
92
+ node_name = None
93
+
94
+ # get container name
95
+ if pod_details.spec and pod_details.spec.containers and pod_details.spec.containers[0].name:
96
+ container_name = pod_details.spec.containers[0].name
97
+ else:
98
+ container_name = None
99
+
100
+ # get container_id
101
+ if pod_details.status and pod_details.status.container_statuses and pod_details.status.container_statuses[0].container_id:
102
+ full_container_id = pod_details.status.container_statuses[0].container_id
103
+
104
+ # full_container_id has format "containerd://xxxxxxxxxx"
105
+ container_id = full_container_id[13:] if full_container_id.startswith('containerd://') else None
106
+ else:
107
+ container_id = None
108
+
109
+ # Cloudwatch container insight log groups should have the same pod log as API response
110
+ cloudwatch_url = self.get_log_url(eks_cluster_name, region, node_name, pod_name, namespace, container_name, container_id)
111
+
112
+ if not validate_region_and_cluster_name(region, eks_cluster_name):
113
+ raise ValueError('Eks cluster name or cluster region is invalid.')
114
+
115
+ if not re.match(CONTAINER_INSIGHTS_LOG_REGEX_PATTERN, cloudwatch_url):
116
+ raise ValueError("Failed to validate cloudwatch log url. Please make sure pod's node name, container name and container id are valid")
117
+
118
+ cloudwatch_link = f'The pod cloudwatch log stream link is {cloudwatch_url}'
119
+ else:
120
+ cloudwatch_link = None
121
+
122
+ return cloudwatch_link
123
+
124
+ def get_log_url(self, eks_cluster_name, region, node_name, pod_name, namespace, container_name, container_id):
125
+ console_prefix = f'https://{region}.console.aws.amazon.com/cloudwatch/home?region={region}#'
126
+ log_group_prefix = f'logsV2:log-groups/log-group/$252Faws$252Fcontainerinsights$252F{eks_cluster_name}$252Fapplication/log-events/'
127
+ log_stream = f'{node_name}-application.var.log.containers.{pod_name}_{namespace}_{container_name}-{container_id}.log'
128
+
129
+ return console_prefix + log_group_prefix + log_stream
130
+
131
+ def is_container_insights_addon_enabled(self, eks_cluster_name):
132
+ response = boto3.client("eks").list_addons(clusterName=eks_cluster_name, maxResults=50)
133
+ if AMAZON_ClOUDWATCH_OBSERVABILITY in response.get('addons', []):
134
+ return True
135
+ else:
136
+ return False