q8s 0.2.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.
- q8s/__init__.py +3 -0
- q8s/__main__.py +4 -0
- q8s/cli.py +77 -0
- q8s/deps/__init__.py +0 -0
- q8s/deps/code_analyzer.py +61 -0
- q8s/deps/parser.py +45 -0
- q8s/execution.py +454 -0
- q8s/install.py +73 -0
- q8s/k8s.py +464 -0
- q8s/kernel.py +79 -0
- q8s/testbook.py +100 -0
- q8s-0.2.0.dist-info/LICENSE +201 -0
- q8s-0.2.0.dist-info/METADATA +300 -0
- q8s-0.2.0.dist-info/RECORD +20 -0
- q8s-0.2.0.dist-info/WHEEL +5 -0
- q8s-0.2.0.dist-info/entry_points.txt +2 -0
- q8s-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/fixtures/simple/qiskit.py +25 -0
- tests/test_deps.py +19 -0
q8s/__init__.py
ADDED
q8s/__main__.py
ADDED
q8s/cli.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import typer
|
|
4
|
+
import sys
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
from q8s.execution import K8sContext, Target
|
|
7
|
+
from q8s.install import install_my_kernel_spec
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def build(tag: str = None):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def execute(
|
|
19
|
+
file: Annotated[Path, typer.Argument(help="Python file to be executed")],
|
|
20
|
+
target: Annotated[
|
|
21
|
+
Target, typer.Option(help="Execution target", case_sensitive=False)
|
|
22
|
+
] = Target.gpu,
|
|
23
|
+
kubeconfig: Annotated[
|
|
24
|
+
Path, typer.Option(help="Kubernetes configuration", envvar="KUBECONFIG")
|
|
25
|
+
] = None,
|
|
26
|
+
image: Annotated[str, typer.Option(help="Docker image")] = "vstirbu/benchmark-deps",
|
|
27
|
+
registry_pat: Annotated[
|
|
28
|
+
str,
|
|
29
|
+
typer.Option(
|
|
30
|
+
help="Registry personal access token (PAT)",
|
|
31
|
+
envvar="REGISTRY_PAT",
|
|
32
|
+
),
|
|
33
|
+
] = None,
|
|
34
|
+
):
|
|
35
|
+
if kubeconfig.exists() is False:
|
|
36
|
+
typer.echo(f"kubeconfig file {kubeconfig} does not exist")
|
|
37
|
+
raise typer.Exit(code=1)
|
|
38
|
+
|
|
39
|
+
kubeconfig = os.environ.get("KUBECONFIG", kubeconfig.as_posix())
|
|
40
|
+
|
|
41
|
+
if kubeconfig is None:
|
|
42
|
+
typer.echo("KUBECONFIG not set")
|
|
43
|
+
raise typer.Exit(code=1)
|
|
44
|
+
|
|
45
|
+
# config.load_kube_config(kubeconfig)
|
|
46
|
+
|
|
47
|
+
k8s_context = K8sContext(kubeconfig)
|
|
48
|
+
k8s_context.set_target(target)
|
|
49
|
+
k8s_context.set_container_image(image)
|
|
50
|
+
k8s_context.set_registry_pat(registry_pat)
|
|
51
|
+
|
|
52
|
+
with open(file, "r") as f:
|
|
53
|
+
code = f.read()
|
|
54
|
+
# output, stream_name = execute_k8s(code, None, image, registry_pat)
|
|
55
|
+
output, stream_name = k8s_context.execute(code)
|
|
56
|
+
|
|
57
|
+
print(f"output:\n{output}")
|
|
58
|
+
print(f"output stream: {stream_name}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def jupyter(
|
|
63
|
+
install: Annotated[
|
|
64
|
+
bool,
|
|
65
|
+
typer.Option(
|
|
66
|
+
help="Install kernel spec for Jupyter",
|
|
67
|
+
),
|
|
68
|
+
] = False,
|
|
69
|
+
):
|
|
70
|
+
print("install:", install)
|
|
71
|
+
if install:
|
|
72
|
+
install_my_kernel_spec(user=False, prefix=sys.prefix)
|
|
73
|
+
# install_my_kernel_spec(user=user, prefix=prefix)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# if __name__ == "__main__":
|
|
77
|
+
app()
|
q8s/deps/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from stdlib_list import stdlib_list
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CodeAnalyzer(ast.NodeVisitor):
|
|
8
|
+
stdlibs = []
|
|
9
|
+
version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
|
|
10
|
+
|
|
11
|
+
def __init__(self, code):
|
|
12
|
+
self.imports = set()
|
|
13
|
+
self.stdlibs = self.stdlib_list()
|
|
14
|
+
|
|
15
|
+
# with open(file, "r") as source:
|
|
16
|
+
# tree = ast.parse(source.read())
|
|
17
|
+
|
|
18
|
+
# self.visit(tree)
|
|
19
|
+
tree = ast.parse(code)
|
|
20
|
+
self.visit(tree)
|
|
21
|
+
|
|
22
|
+
# print("working with version: ", self.version)
|
|
23
|
+
|
|
24
|
+
def stdlib_list(self):
|
|
25
|
+
if sys.version_info.major == 3 and sys.version_info.minor < 10:
|
|
26
|
+
return stdlib_list(self.version)
|
|
27
|
+
else:
|
|
28
|
+
return sys.stdlib_module_names
|
|
29
|
+
|
|
30
|
+
def addImport(self, name):
|
|
31
|
+
if name in self.stdlibs:
|
|
32
|
+
pass
|
|
33
|
+
elif name in sys.builtin_module_names:
|
|
34
|
+
pass
|
|
35
|
+
else:
|
|
36
|
+
self.imports.add(name.split(".")[0])
|
|
37
|
+
|
|
38
|
+
def visit_Import(self, node):
|
|
39
|
+
for alias in node.names:
|
|
40
|
+
# pprint(vars(alias))
|
|
41
|
+
self.addImport(alias.name)
|
|
42
|
+
|
|
43
|
+
self.generic_visit(node)
|
|
44
|
+
|
|
45
|
+
def visit_ImportFrom(self, node):
|
|
46
|
+
# pprint(vars(node))
|
|
47
|
+
# pprint(self.file)
|
|
48
|
+
|
|
49
|
+
if node.level == 0:
|
|
50
|
+
self.addImport(node.module)
|
|
51
|
+
else:
|
|
52
|
+
if node.module is None:
|
|
53
|
+
for name in node.names:
|
|
54
|
+
self.addImport(name.asname or name.name)
|
|
55
|
+
else:
|
|
56
|
+
self.addImport(node.module)
|
|
57
|
+
|
|
58
|
+
self.generic_visit(node)
|
|
59
|
+
|
|
60
|
+
def getImports(self):
|
|
61
|
+
return sorted(self.imports)
|
q8s/deps/parser.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from importlib.metadata import (
|
|
2
|
+
packages_distributions,
|
|
3
|
+
version,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from .code_analyzer import CodeAnalyzer
|
|
7
|
+
|
|
8
|
+
mapping = {
|
|
9
|
+
"qiskit-aer": "qiskit-aer-gpu",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Parser:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.installed = packages_distributions()
|
|
16
|
+
|
|
17
|
+
def mapToPip(self, package: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Map the package to the pip package.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
return self.installed[package][0]
|
|
24
|
+
except KeyError:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def parse(self, code: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Parse the code and returns the requirements.
|
|
30
|
+
"""
|
|
31
|
+
imports = CodeAnalyzer(code).getImports()
|
|
32
|
+
|
|
33
|
+
pips = list(map(self.mapToPip, imports))
|
|
34
|
+
|
|
35
|
+
return "\n".join(
|
|
36
|
+
list(
|
|
37
|
+
map(
|
|
38
|
+
lambda x: "{name}=={version}".format(
|
|
39
|
+
name=mapping[x] if x in mapping.keys() else x,
|
|
40
|
+
version=version(x),
|
|
41
|
+
),
|
|
42
|
+
pips,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
)
|
q8s/execution.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from json import JSONEncoder, loads
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import string
|
|
8
|
+
from time import sleep
|
|
9
|
+
from dotenv import dotenv_values
|
|
10
|
+
from kubernetes import client, config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
FORMAT = "[%(levelname)s %(asctime)-15s q8s_context] %(message)s"
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format=FORMAT)
|
|
15
|
+
|
|
16
|
+
MEMORY = os.environ.get("MEMORY", "32Gi")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Target(str, Enum):
|
|
20
|
+
cpu = "cpu"
|
|
21
|
+
gpu = "gpu"
|
|
22
|
+
qpu = "qpu"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_env():
|
|
26
|
+
env = dotenv_values(".env.q8s")
|
|
27
|
+
|
|
28
|
+
# for key in env.keys():
|
|
29
|
+
# logging.info(f"{key} = {env[key]}")
|
|
30
|
+
|
|
31
|
+
return env
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class K8sContext:
|
|
35
|
+
container_image: str = "vsirbu/benchmark-deps"
|
|
36
|
+
registry_pat: str | None = None
|
|
37
|
+
jupyter_logger: None
|
|
38
|
+
target: Target = Target.gpu
|
|
39
|
+
|
|
40
|
+
def __init__(self, kubeconfig: str, logger=None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the Kubernetes context.
|
|
43
|
+
"""
|
|
44
|
+
config.load_kube_config(kubeconfig)
|
|
45
|
+
logging.info("Kubeconfig loaded")
|
|
46
|
+
|
|
47
|
+
_, active_context = config.list_kube_config_contexts(config_file=kubeconfig)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
self.namespace = active_context["context"]["namespace"]
|
|
51
|
+
except KeyError:
|
|
52
|
+
self.namespace = "default"
|
|
53
|
+
logging.info("Active namespace: %s" % self.namespace)
|
|
54
|
+
|
|
55
|
+
self.core_api_instance = client.CoreV1Api()
|
|
56
|
+
self.batch_api_instance = client.BatchV1Api()
|
|
57
|
+
|
|
58
|
+
self.name = f"qubernetes-job-{K8sContext.get_id()}"
|
|
59
|
+
|
|
60
|
+
self.__env = load_env()
|
|
61
|
+
|
|
62
|
+
self.jupyter_logger = logger
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def get_id():
|
|
66
|
+
"""
|
|
67
|
+
Generate a random ID.
|
|
68
|
+
"""
|
|
69
|
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
70
|
+
|
|
71
|
+
def set_container_image(self, image: str):
|
|
72
|
+
self.container_image = image
|
|
73
|
+
|
|
74
|
+
def set_registry_pat(self, pat: str):
|
|
75
|
+
self.registry_pat = pat
|
|
76
|
+
|
|
77
|
+
def set_target(self, target: Target):
|
|
78
|
+
self.target = target
|
|
79
|
+
|
|
80
|
+
def create_job_object(self, code: str) -> client.V1Job:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def __registry_credentials_secret_name(self):
|
|
84
|
+
return f"{self.name}-regcred"
|
|
85
|
+
|
|
86
|
+
def __create_job_object(self, code: str):
|
|
87
|
+
"""
|
|
88
|
+
Create a job object with the given code.
|
|
89
|
+
"""
|
|
90
|
+
env = self.__prepare_environment()
|
|
91
|
+
|
|
92
|
+
# Configureate Pod template container
|
|
93
|
+
container = client.V1Container(
|
|
94
|
+
name=self.name,
|
|
95
|
+
image=self.container_image,
|
|
96
|
+
env=env,
|
|
97
|
+
command=["python3"],
|
|
98
|
+
args=["./app/main.py"],
|
|
99
|
+
resources=(
|
|
100
|
+
client.V1ResourceRequirements(
|
|
101
|
+
limits=(
|
|
102
|
+
{
|
|
103
|
+
"cpu": "2",
|
|
104
|
+
"ephemeral-storage": "50Gi",
|
|
105
|
+
"memory": MEMORY,
|
|
106
|
+
"nvidia.com/gpu": "1",
|
|
107
|
+
# "qubernetes.dev/qpu": "1",
|
|
108
|
+
}
|
|
109
|
+
),
|
|
110
|
+
requests=(
|
|
111
|
+
{
|
|
112
|
+
"cpu": "2",
|
|
113
|
+
"ephemeral-storage": "0",
|
|
114
|
+
"memory": MEMORY,
|
|
115
|
+
"nvidia.com/gpu": "1",
|
|
116
|
+
# "qubernetes.dev/qpu": "1",
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
if self.target == Target.gpu
|
|
121
|
+
else None
|
|
122
|
+
),
|
|
123
|
+
volume_mounts=[
|
|
124
|
+
client.V1VolumeMount(
|
|
125
|
+
name="app-volume", mount_path="/app", read_only=True
|
|
126
|
+
)
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Create and configurate a spec section
|
|
131
|
+
template = client.V1PodTemplateSpec(
|
|
132
|
+
metadata=client.V1ObjectMeta(labels={"app": self.name}),
|
|
133
|
+
spec=client.V1PodSpec(
|
|
134
|
+
containers=[container],
|
|
135
|
+
image_pull_secrets=(
|
|
136
|
+
[
|
|
137
|
+
client.V1LocalObjectReference(
|
|
138
|
+
name=self.__registry_credentials_secret_name()
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
if self.registry_pat
|
|
142
|
+
else []
|
|
143
|
+
),
|
|
144
|
+
runtime_class_name="nvidia" if self.target == Target.gpu else None,
|
|
145
|
+
restart_policy="Never",
|
|
146
|
+
volumes=[
|
|
147
|
+
client.V1Volume(
|
|
148
|
+
name="app-volume",
|
|
149
|
+
config_map=client.V1ConfigMapVolumeSource(name=self.name),
|
|
150
|
+
)
|
|
151
|
+
],
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Create the specification of deployment
|
|
156
|
+
spec = client.V1JobSpec(template=template) # , ttl_seconds_after_finished=10
|
|
157
|
+
|
|
158
|
+
# Instantiate the job object
|
|
159
|
+
job_spec = client.V1Job(
|
|
160
|
+
api_version="batch/v1",
|
|
161
|
+
kind="Job",
|
|
162
|
+
metadata=client.V1ObjectMeta(
|
|
163
|
+
name=self.name,
|
|
164
|
+
namespace=self.namespace,
|
|
165
|
+
labels={"qubernetes.dev/job.type": "jupyter"},
|
|
166
|
+
),
|
|
167
|
+
spec=spec,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
job = self.batch_api_instance.create_namespaced_job(
|
|
171
|
+
body=job_spec, namespace=self.namespace
|
|
172
|
+
)
|
|
173
|
+
logging.info("Job created")
|
|
174
|
+
|
|
175
|
+
self.__create_config_map_object(code, job)
|
|
176
|
+
self.__create_environment_secret()
|
|
177
|
+
if self.registry_pat:
|
|
178
|
+
self.__create_registry_credentials_secret()
|
|
179
|
+
|
|
180
|
+
return job
|
|
181
|
+
|
|
182
|
+
def __create_config_map_object(self, code: str, job: client.V1Job):
|
|
183
|
+
"""
|
|
184
|
+
Create a ConfigMap object with the given code.
|
|
185
|
+
"""
|
|
186
|
+
# Configureate ConfigMap from a local file
|
|
187
|
+
configmap = client.V1ConfigMap(
|
|
188
|
+
api_version="v1",
|
|
189
|
+
kind="ConfigMap",
|
|
190
|
+
data={"main.py": code},
|
|
191
|
+
metadata=client.V1ObjectMeta(
|
|
192
|
+
name=self.name,
|
|
193
|
+
owner_references=[
|
|
194
|
+
client.V1OwnerReference(
|
|
195
|
+
api_version="v1",
|
|
196
|
+
kind="Job",
|
|
197
|
+
name=job.metadata.name,
|
|
198
|
+
uid=job.metadata.uid,
|
|
199
|
+
# block_owner_deletion=True,
|
|
200
|
+
# controller=True,
|
|
201
|
+
)
|
|
202
|
+
],
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
self.core_api_instance.create_namespaced_config_map(
|
|
207
|
+
namespace=self.namespace, body=configmap
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
logging.info("ConfigMap created.")
|
|
211
|
+
|
|
212
|
+
def __create_environment_secret(self):
|
|
213
|
+
"""
|
|
214
|
+
Create a Secret object with the environment variables.
|
|
215
|
+
"""
|
|
216
|
+
data = {}
|
|
217
|
+
|
|
218
|
+
for key in self.__env.keys():
|
|
219
|
+
data[key] = base64.b64encode(self.__env[key].encode()).decode()
|
|
220
|
+
|
|
221
|
+
secret = client.V1Secret(
|
|
222
|
+
api_version="v1",
|
|
223
|
+
kind="Secret",
|
|
224
|
+
type="Opaque",
|
|
225
|
+
immutable=True,
|
|
226
|
+
data=data,
|
|
227
|
+
metadata=client.V1ObjectMeta(
|
|
228
|
+
name=self.name,
|
|
229
|
+
namespace=self.namespace,
|
|
230
|
+
# owner_references=[
|
|
231
|
+
# client.V1OwnerReference(
|
|
232
|
+
# api_version="v1",
|
|
233
|
+
# kind="Job",
|
|
234
|
+
# name=job.metadata.name,
|
|
235
|
+
# uid=job.metadata.uid,
|
|
236
|
+
# # block_owner_deletion=True,
|
|
237
|
+
# # controller=True,
|
|
238
|
+
# )
|
|
239
|
+
# ],
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
self.core_api_instance.create_namespaced_secret(
|
|
244
|
+
namespace=self.namespace, body=secret
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
logging.info("Environment created.")
|
|
248
|
+
|
|
249
|
+
def __create_registry_credentials_secret(self):
|
|
250
|
+
"""
|
|
251
|
+
Create a Secret object with the registry credentials.
|
|
252
|
+
"""
|
|
253
|
+
segments = self.container_image.split("/")
|
|
254
|
+
# Find user name for images on Docker Hub
|
|
255
|
+
username = segments[0] if len(segments) == 2 else segments[1]
|
|
256
|
+
registry = segments[0] if len(segments) == 3 else "https://index.docker.io/v1/"
|
|
257
|
+
|
|
258
|
+
config = {
|
|
259
|
+
"auths": {
|
|
260
|
+
registry: {
|
|
261
|
+
"auth": base64.b64encode(
|
|
262
|
+
f"{username}:{self.registry_pat}".encode()
|
|
263
|
+
).decode(),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
secret = client.V1Secret(
|
|
269
|
+
api_version="v1",
|
|
270
|
+
kind="Secret",
|
|
271
|
+
type="kubernetes.io/dockerconfigjson",
|
|
272
|
+
immutable=True,
|
|
273
|
+
metadata=client.V1ObjectMeta(
|
|
274
|
+
name=self.__registry_credentials_secret_name(),
|
|
275
|
+
namespace=self.namespace,
|
|
276
|
+
),
|
|
277
|
+
data={
|
|
278
|
+
".dockerconfigjson": base64.b64encode(
|
|
279
|
+
JSONEncoder().encode(config).encode()
|
|
280
|
+
).decode(),
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
self.core_api_instance.create_namespaced_secret(
|
|
285
|
+
namespace=self.namespace, body=secret
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
logging.info("Registry credentials created.")
|
|
289
|
+
|
|
290
|
+
def __delete_job(self):
|
|
291
|
+
"""
|
|
292
|
+
Delete the job and its associated resources.
|
|
293
|
+
"""
|
|
294
|
+
if self.registry_pat:
|
|
295
|
+
self.core_api_instance.delete_namespaced_secret(
|
|
296
|
+
self.__registry_credentials_secret_name(), self.namespace
|
|
297
|
+
)
|
|
298
|
+
logging.info("Registry credentials removed")
|
|
299
|
+
|
|
300
|
+
self.core_api_instance.delete_namespaced_secret(self.name, self.namespace)
|
|
301
|
+
logging.info("Environment removed.")
|
|
302
|
+
|
|
303
|
+
self.core_api_instance.delete_namespaced_config_map(
|
|
304
|
+
self.name,
|
|
305
|
+
self.namespace,
|
|
306
|
+
body=client.V1DeleteOptions(propagation_policy="Foreground"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
api_response = self.batch_api_instance.delete_namespaced_job(
|
|
310
|
+
self.name,
|
|
311
|
+
self.namespace,
|
|
312
|
+
body=client.V1DeleteOptions(propagation_policy="Foreground"),
|
|
313
|
+
_preload_content=False,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
data = loads(api_response.data)
|
|
318
|
+
|
|
319
|
+
logging.info(
|
|
320
|
+
"Job removed. status='%s'"
|
|
321
|
+
% str(data["status"]["conditions"][0]["type"])
|
|
322
|
+
)
|
|
323
|
+
except:
|
|
324
|
+
logging.info(api_response.status)
|
|
325
|
+
logging.info("Job removed. status='%s'" % str(api_response.status))
|
|
326
|
+
|
|
327
|
+
def __get_job_logs(self, name="qiskit-aer-gpu"):
|
|
328
|
+
"""
|
|
329
|
+
Get the logs of the job.
|
|
330
|
+
"""
|
|
331
|
+
api_response = self.core_api_instance.read_namespaced_pod_log(
|
|
332
|
+
name=name, namespace=self.namespace
|
|
333
|
+
)
|
|
334
|
+
logging.debug("Job logs='%s'" % str(api_response))
|
|
335
|
+
return api_response
|
|
336
|
+
|
|
337
|
+
def __get_pods_in_job(self):
|
|
338
|
+
"""
|
|
339
|
+
Get the pods in the job.
|
|
340
|
+
"""
|
|
341
|
+
pods = self.core_api_instance.list_namespaced_pod(
|
|
342
|
+
self.namespace, label_selector=f"app={self.name}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
pod_name = pods.items[0].metadata.name
|
|
346
|
+
|
|
347
|
+
logging.info("Pods in the job='%s'" % str(pod_name))
|
|
348
|
+
|
|
349
|
+
return pod_name
|
|
350
|
+
|
|
351
|
+
def __complete_and_get_job_status(self):
|
|
352
|
+
"""
|
|
353
|
+
Wait for the job to complete and get its status.
|
|
354
|
+
"""
|
|
355
|
+
pod_name = None
|
|
356
|
+
job_completed = False
|
|
357
|
+
|
|
358
|
+
while not job_completed:
|
|
359
|
+
api_response = self.batch_api_instance.read_namespaced_job_status(
|
|
360
|
+
self.name, self.namespace
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
logging.debug("Job status='%s'" % str(api_response.status.start_time))
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
api_response.status.succeeded is not None
|
|
367
|
+
or api_response.status.failed is not None
|
|
368
|
+
):
|
|
369
|
+
job_completed = True
|
|
370
|
+
elif api_response.status.active is not None:
|
|
371
|
+
if pod_name is None:
|
|
372
|
+
pod_name = self.__get_pods_in_job()
|
|
373
|
+
|
|
374
|
+
s = self.core_api_instance.read_namespaced_pod_status(
|
|
375
|
+
name=pod_name,
|
|
376
|
+
namespace=self.namespace,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
if s.status.container_statuses[0].state.terminated is not None:
|
|
381
|
+
if (
|
|
382
|
+
s.status.container_statuses[0].state.terminated.exit_code
|
|
383
|
+
== 0
|
|
384
|
+
):
|
|
385
|
+
job_completed = True
|
|
386
|
+
except TypeError:
|
|
387
|
+
pass
|
|
388
|
+
finally:
|
|
389
|
+
logging.info("Pod status='%s'" % str(s.status.phase))
|
|
390
|
+
if self.jupyter_logger is not None:
|
|
391
|
+
self.jupyter_logger(f"Pod status: {s.status.phase}")
|
|
392
|
+
|
|
393
|
+
sleep(1)
|
|
394
|
+
|
|
395
|
+
return self.__map_job_status_to_stream(api_response.status)
|
|
396
|
+
|
|
397
|
+
def __map_job_status_to_stream(self, status):
|
|
398
|
+
logging.debug("Job status='%s'" % str(status))
|
|
399
|
+
if status.succeeded is not None:
|
|
400
|
+
return "stdout"
|
|
401
|
+
elif status.failed is not None:
|
|
402
|
+
return "stderr"
|
|
403
|
+
else:
|
|
404
|
+
return "unknown"
|
|
405
|
+
|
|
406
|
+
def __prepare_environment(self):
|
|
407
|
+
"""
|
|
408
|
+
Prepare the environment variables.
|
|
409
|
+
"""
|
|
410
|
+
env = []
|
|
411
|
+
|
|
412
|
+
for key in self.__env.keys():
|
|
413
|
+
env.append(
|
|
414
|
+
client.V1EnvVar(
|
|
415
|
+
name=key,
|
|
416
|
+
value_from=client.V1EnvVarSource(
|
|
417
|
+
secret_key_ref=client.V1SecretKeySelector(
|
|
418
|
+
name=self.name, key=key
|
|
419
|
+
)
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return env
|
|
425
|
+
|
|
426
|
+
def execute(self, code: str) -> tuple[str, str]:
|
|
427
|
+
"""
|
|
428
|
+
Execute the given code.
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
self.__create_job_object(code=code)
|
|
432
|
+
|
|
433
|
+
if self.jupyter_logger is not None:
|
|
434
|
+
self.jupyter_logger(f"Job {self.name} created")
|
|
435
|
+
|
|
436
|
+
stream = self.__complete_and_get_job_status()
|
|
437
|
+
|
|
438
|
+
job = self.__get_pods_in_job()
|
|
439
|
+
logs = self.__get_job_logs(job)
|
|
440
|
+
|
|
441
|
+
return logs, stream
|
|
442
|
+
except KeyboardInterrupt:
|
|
443
|
+
return "Task interrupted by user", "stderr"
|
|
444
|
+
except:
|
|
445
|
+
logging.info("An error occurred.")
|
|
446
|
+
return "An error occurred.", "stderr"
|
|
447
|
+
finally:
|
|
448
|
+
self.__delete_job()
|
|
449
|
+
|
|
450
|
+
def abort(self):
|
|
451
|
+
"""
|
|
452
|
+
Abort the execution.
|
|
453
|
+
"""
|
|
454
|
+
self.__delete_job()
|