q8s 0.1.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 +73 -0
- q8s/deps/__init__.py +0 -0
- q8s/deps/code_analyzer.py +61 -0
- q8s/deps/parser.py +45 -0
- q8s/execution.py +395 -0
- q8s/install.py +73 -0
- q8s/k8s.py +464 -0
- q8s/kernel.py +79 -0
- q8s/testbook.py +100 -0
- q8s-0.1.0.dist-info/LICENSE +201 -0
- q8s-0.1.0.dist-info/METADATA +300 -0
- q8s-0.1.0.dist-info/RECORD +20 -0
- q8s-0.1.0.dist-info/WHEEL +5 -0
- q8s-0.1.0.dist-info/entry_points.txt +2 -0
- q8s-0.1.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,73 @@
|
|
|
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
|
|
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
|
+
kubeconfig: Annotated[
|
|
21
|
+
Path, typer.Option(help="Kubernetes configuration", envvar="KUBECONFIG")
|
|
22
|
+
] = None,
|
|
23
|
+
image: Annotated[str, typer.Option(help="Docker image")] = "vstirbu/benchmark-deps",
|
|
24
|
+
registry_pat: Annotated[
|
|
25
|
+
str,
|
|
26
|
+
typer.Option(
|
|
27
|
+
help="Registry personal access token (PAT)",
|
|
28
|
+
envvar="REGISTRY_PAT",
|
|
29
|
+
),
|
|
30
|
+
] = None,
|
|
31
|
+
):
|
|
32
|
+
if kubeconfig.exists() is False:
|
|
33
|
+
typer.echo(f"kubeconfig file {kubeconfig} does not exist")
|
|
34
|
+
raise typer.Exit(code=1)
|
|
35
|
+
|
|
36
|
+
kubeconfig = os.environ.get("KUBECONFIG", kubeconfig.as_posix())
|
|
37
|
+
|
|
38
|
+
if kubeconfig is None:
|
|
39
|
+
typer.echo("KUBECONFIG not set")
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
|
|
42
|
+
# config.load_kube_config(kubeconfig)
|
|
43
|
+
|
|
44
|
+
k8s_context = K8sContext(kubeconfig)
|
|
45
|
+
k8s_context.set_container_image(image)
|
|
46
|
+
k8s_context.set_registry_pat(registry_pat)
|
|
47
|
+
|
|
48
|
+
with open(file, "r") as f:
|
|
49
|
+
code = f.read()
|
|
50
|
+
# output, stream_name = execute_k8s(code, None, image, registry_pat)
|
|
51
|
+
output, stream_name = k8s_context.execute(code)
|
|
52
|
+
|
|
53
|
+
print(f"output:\n{output}")
|
|
54
|
+
print(f"output stream: {stream_name}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def jupyter(
|
|
59
|
+
install: Annotated[
|
|
60
|
+
bool,
|
|
61
|
+
typer.Option(
|
|
62
|
+
help="Install kernel spec for Jupyter",
|
|
63
|
+
),
|
|
64
|
+
] = False,
|
|
65
|
+
):
|
|
66
|
+
print("install:", install)
|
|
67
|
+
if install:
|
|
68
|
+
install_my_kernel_spec(user=False, prefix=sys.prefix)
|
|
69
|
+
# install_my_kernel_spec(user=user, prefix=prefix)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# if __name__ == "__main__":
|
|
73
|
+
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,395 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from json import JSONEncoder, loads
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import string
|
|
7
|
+
from time import sleep
|
|
8
|
+
from dotenv import dotenv_values
|
|
9
|
+
from kubernetes import client, config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FORMAT = "[%(levelname)s %(asctime)-15s q8s_context] %(message)s"
|
|
13
|
+
logging.basicConfig(level=logging.INFO, format=FORMAT)
|
|
14
|
+
|
|
15
|
+
MEMORY = os.environ.get("MEMORY", "32Gi")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_env():
|
|
19
|
+
env = dotenv_values(".env.q8s")
|
|
20
|
+
|
|
21
|
+
# for key in env.keys():
|
|
22
|
+
# logging.info(f"{key} = {env[key]}")
|
|
23
|
+
|
|
24
|
+
return env
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class K8sContext:
|
|
28
|
+
container_image: str = "vsirbu/benchmark-deps"
|
|
29
|
+
registry_pat: str | None = None
|
|
30
|
+
jupyter_logger: None
|
|
31
|
+
|
|
32
|
+
def __init__(self, kubeconfig: str, logger=None):
|
|
33
|
+
config.load_kube_config(kubeconfig)
|
|
34
|
+
logging.info("Kubeconfig loaded")
|
|
35
|
+
|
|
36
|
+
_, active_context = config.list_kube_config_contexts(config_file=kubeconfig)
|
|
37
|
+
|
|
38
|
+
self.namespace = active_context["context"]["namespace"]
|
|
39
|
+
logging.info("Active namespace: %s" % self.namespace)
|
|
40
|
+
|
|
41
|
+
self.core_api_instance = client.CoreV1Api()
|
|
42
|
+
self.batch_api_instance = client.BatchV1Api()
|
|
43
|
+
|
|
44
|
+
self.name = f"qubernetes-job-{K8sContext.get_id()}"
|
|
45
|
+
|
|
46
|
+
self.__env = load_env()
|
|
47
|
+
|
|
48
|
+
self.jupyter_logger = logger
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def get_id():
|
|
52
|
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
53
|
+
|
|
54
|
+
def set_container_image(self, image: str):
|
|
55
|
+
self.container_image = image
|
|
56
|
+
|
|
57
|
+
def set_registry_pat(self, pat: str):
|
|
58
|
+
self.registry_pat = pat
|
|
59
|
+
|
|
60
|
+
def create_job_object(self, code: str) -> client.V1Job:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def __registry_credentials_secret_name(self):
|
|
64
|
+
return f"{self.name}-regcred"
|
|
65
|
+
|
|
66
|
+
def __create_job_object(self, code: str):
|
|
67
|
+
env = self.__prepare_environment()
|
|
68
|
+
|
|
69
|
+
# Configureate Pod template container
|
|
70
|
+
container = client.V1Container(
|
|
71
|
+
name=self.name,
|
|
72
|
+
image=self.container_image,
|
|
73
|
+
env=env,
|
|
74
|
+
command=["python3"],
|
|
75
|
+
args=["./app/main.py"],
|
|
76
|
+
resources=client.V1ResourceRequirements(
|
|
77
|
+
limits={
|
|
78
|
+
"cpu": "2",
|
|
79
|
+
"ephemeral-storage": "50Gi",
|
|
80
|
+
# "memory": "64Gi",
|
|
81
|
+
"memory": MEMORY,
|
|
82
|
+
"nvidia.com/gpu": "1",
|
|
83
|
+
# "qubernetes.dev/qpu": "1",
|
|
84
|
+
},
|
|
85
|
+
requests={
|
|
86
|
+
"cpu": "2",
|
|
87
|
+
"ephemeral-storage": "0",
|
|
88
|
+
# "memory": "32Gi",
|
|
89
|
+
"memory": MEMORY,
|
|
90
|
+
"nvidia.com/gpu": "1",
|
|
91
|
+
# "qubernetes.dev/qpu": "1",
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
volume_mounts=[
|
|
95
|
+
client.V1VolumeMount(
|
|
96
|
+
name="app-volume", mount_path="/app", read_only=True
|
|
97
|
+
)
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Create and configurate a spec section
|
|
102
|
+
template = client.V1PodTemplateSpec(
|
|
103
|
+
metadata=client.V1ObjectMeta(labels={"app": self.name}),
|
|
104
|
+
spec=client.V1PodSpec(
|
|
105
|
+
containers=[container],
|
|
106
|
+
image_pull_secrets=(
|
|
107
|
+
[
|
|
108
|
+
client.V1LocalObjectReference(
|
|
109
|
+
name=self.__registry_credentials_secret_name()
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
if self.registry_pat
|
|
113
|
+
else []
|
|
114
|
+
),
|
|
115
|
+
runtime_class_name="nvidia",
|
|
116
|
+
restart_policy="Never",
|
|
117
|
+
volumes=[
|
|
118
|
+
client.V1Volume(
|
|
119
|
+
name="app-volume",
|
|
120
|
+
config_map=client.V1ConfigMapVolumeSource(name=self.name),
|
|
121
|
+
)
|
|
122
|
+
],
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Create the specification of deployment
|
|
127
|
+
spec = client.V1JobSpec(template=template) # , ttl_seconds_after_finished=10
|
|
128
|
+
|
|
129
|
+
# Instantiate the job object
|
|
130
|
+
job_spec = client.V1Job(
|
|
131
|
+
api_version="batch/v1",
|
|
132
|
+
kind="Job",
|
|
133
|
+
metadata=client.V1ObjectMeta(
|
|
134
|
+
name=self.name,
|
|
135
|
+
namespace=self.namespace,
|
|
136
|
+
labels={"qubernetes.dev/job.type": "jupyter"},
|
|
137
|
+
),
|
|
138
|
+
spec=spec,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
job = self.batch_api_instance.create_namespaced_job(
|
|
142
|
+
body=job_spec, namespace=self.namespace
|
|
143
|
+
)
|
|
144
|
+
logging.info("Job created")
|
|
145
|
+
|
|
146
|
+
self.__create_config_map_object(code, job)
|
|
147
|
+
self.__create_environment_secret()
|
|
148
|
+
if self.registry_pat:
|
|
149
|
+
self.__create_registry_credentials_secret()
|
|
150
|
+
|
|
151
|
+
return job
|
|
152
|
+
|
|
153
|
+
def __create_config_map_object(self, code: str, job: client.V1Job):
|
|
154
|
+
# Configureate ConfigMap from a local file
|
|
155
|
+
configmap = client.V1ConfigMap(
|
|
156
|
+
api_version="v1",
|
|
157
|
+
kind="ConfigMap",
|
|
158
|
+
data={"main.py": code},
|
|
159
|
+
metadata=client.V1ObjectMeta(
|
|
160
|
+
name=self.name,
|
|
161
|
+
owner_references=[
|
|
162
|
+
client.V1OwnerReference(
|
|
163
|
+
api_version="v1",
|
|
164
|
+
kind="Job",
|
|
165
|
+
name=job.metadata.name,
|
|
166
|
+
uid=job.metadata.uid,
|
|
167
|
+
# block_owner_deletion=True,
|
|
168
|
+
# controller=True,
|
|
169
|
+
)
|
|
170
|
+
],
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.core_api_instance.create_namespaced_config_map(
|
|
175
|
+
namespace=self.namespace, body=configmap
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
logging.info("ConfigMap created.")
|
|
179
|
+
|
|
180
|
+
def __create_environment_secret(self):
|
|
181
|
+
data = {}
|
|
182
|
+
|
|
183
|
+
for key in self.__env.keys():
|
|
184
|
+
data[key] = base64.b64encode(self.__env[key].encode()).decode()
|
|
185
|
+
|
|
186
|
+
secret = client.V1Secret(
|
|
187
|
+
api_version="v1",
|
|
188
|
+
kind="Secret",
|
|
189
|
+
type="Opaque",
|
|
190
|
+
immutable=True,
|
|
191
|
+
data=data,
|
|
192
|
+
metadata=client.V1ObjectMeta(
|
|
193
|
+
name=self.name,
|
|
194
|
+
namespace=self.namespace,
|
|
195
|
+
# owner_references=[
|
|
196
|
+
# client.V1OwnerReference(
|
|
197
|
+
# api_version="v1",
|
|
198
|
+
# kind="Job",
|
|
199
|
+
# name=job.metadata.name,
|
|
200
|
+
# uid=job.metadata.uid,
|
|
201
|
+
# # block_owner_deletion=True,
|
|
202
|
+
# # controller=True,
|
|
203
|
+
# )
|
|
204
|
+
# ],
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.core_api_instance.create_namespaced_secret(
|
|
209
|
+
namespace=self.namespace, body=secret
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logging.info("Environment created.")
|
|
213
|
+
|
|
214
|
+
def __create_registry_credentials_secret(self):
|
|
215
|
+
segments = self.container_image.split("/")
|
|
216
|
+
# Find user name for images on Docker Hub
|
|
217
|
+
username = segments[0] if len(segments) == 2 else segments[1]
|
|
218
|
+
registry = segments[0] if len(segments) == 3 else "https://index.docker.io/v1/"
|
|
219
|
+
|
|
220
|
+
config = {
|
|
221
|
+
"auths": {
|
|
222
|
+
registry: {
|
|
223
|
+
"auth": base64.b64encode(
|
|
224
|
+
f"{username}:{self.registry_pat}".encode()
|
|
225
|
+
).decode(),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
secret = client.V1Secret(
|
|
231
|
+
api_version="v1",
|
|
232
|
+
kind="Secret",
|
|
233
|
+
type="kubernetes.io/dockerconfigjson",
|
|
234
|
+
immutable=True,
|
|
235
|
+
metadata=client.V1ObjectMeta(
|
|
236
|
+
name=self.__registry_credentials_secret_name(),
|
|
237
|
+
namespace=self.namespace,
|
|
238
|
+
),
|
|
239
|
+
data={
|
|
240
|
+
".dockerconfigjson": base64.b64encode(
|
|
241
|
+
JSONEncoder().encode(config).encode()
|
|
242
|
+
).decode(),
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self.core_api_instance.create_namespaced_secret(
|
|
247
|
+
namespace=self.namespace, body=secret
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
logging.info("Registry credentials created.")
|
|
251
|
+
|
|
252
|
+
def __delete_job(self):
|
|
253
|
+
if self.registry_pat:
|
|
254
|
+
self.core_api_instance.delete_namespaced_secret(
|
|
255
|
+
self.__registry_credentials_secret_name(), self.namespace
|
|
256
|
+
)
|
|
257
|
+
logging.info("Registry credentials removed")
|
|
258
|
+
|
|
259
|
+
self.core_api_instance.delete_namespaced_secret(self.name, self.namespace)
|
|
260
|
+
logging.info("Environment removed.")
|
|
261
|
+
|
|
262
|
+
self.core_api_instance.delete_namespaced_config_map(
|
|
263
|
+
self.name,
|
|
264
|
+
self.namespace,
|
|
265
|
+
body=client.V1DeleteOptions(propagation_policy="Foreground"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
api_response = self.batch_api_instance.delete_namespaced_job(
|
|
269
|
+
self.name,
|
|
270
|
+
self.namespace,
|
|
271
|
+
body=client.V1DeleteOptions(propagation_policy="Foreground"),
|
|
272
|
+
_preload_content=False,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
data = loads(api_response.data)
|
|
277
|
+
|
|
278
|
+
logging.info(
|
|
279
|
+
"Job removed. status='%s'"
|
|
280
|
+
% str(data["status"]["conditions"][0]["type"])
|
|
281
|
+
)
|
|
282
|
+
except:
|
|
283
|
+
logging.info(api_response.status)
|
|
284
|
+
logging.info("Job removed. status='%s'" % str(api_response.status))
|
|
285
|
+
|
|
286
|
+
def __get_job_logs(self, name="qiskit-aer-gpu"):
|
|
287
|
+
api_response = self.core_api_instance.read_namespaced_pod_log(
|
|
288
|
+
name=name, namespace=self.namespace
|
|
289
|
+
)
|
|
290
|
+
logging.debug("Job logs='%s'" % str(api_response))
|
|
291
|
+
return api_response
|
|
292
|
+
|
|
293
|
+
def __get_pods_in_job(self):
|
|
294
|
+
pods = self.core_api_instance.list_namespaced_pod(
|
|
295
|
+
self.namespace, label_selector=f"app={self.name}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
pod_name = pods.items[0].metadata.name
|
|
299
|
+
|
|
300
|
+
logging.info("Pods in the job='%s'" % str(pod_name))
|
|
301
|
+
|
|
302
|
+
return pod_name
|
|
303
|
+
|
|
304
|
+
def __complete_and_get_job_status(self):
|
|
305
|
+
pod_name = None
|
|
306
|
+
job_completed = False
|
|
307
|
+
|
|
308
|
+
while not job_completed:
|
|
309
|
+
api_response = self.batch_api_instance.read_namespaced_job_status(
|
|
310
|
+
self.name, self.namespace
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
logging.debug("Job status='%s'" % str(api_response.status.start_time))
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
api_response.status.succeeded is not None
|
|
317
|
+
or api_response.status.failed is not None
|
|
318
|
+
):
|
|
319
|
+
job_completed = True
|
|
320
|
+
elif api_response.status.active is not None:
|
|
321
|
+
if pod_name is None:
|
|
322
|
+
pod_name = self.__get_pods_in_job()
|
|
323
|
+
|
|
324
|
+
s = self.core_api_instance.read_namespaced_pod_status(
|
|
325
|
+
name=pod_name,
|
|
326
|
+
namespace=self.namespace,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
if s.status.container_statuses[0].state.terminated is not None:
|
|
331
|
+
if (
|
|
332
|
+
s.status.container_statuses[0].state.terminated.exit_code
|
|
333
|
+
== 0
|
|
334
|
+
):
|
|
335
|
+
job_completed = True
|
|
336
|
+
except TypeError:
|
|
337
|
+
pass
|
|
338
|
+
finally:
|
|
339
|
+
logging.info("Pod status='%s'" % str(s.status.phase))
|
|
340
|
+
if self.jupyter_logger is not None:
|
|
341
|
+
self.jupyter_logger(f"Pod status: {s.status.phase}")
|
|
342
|
+
|
|
343
|
+
sleep(1)
|
|
344
|
+
|
|
345
|
+
return self.__map_job_status_to_stream(api_response.status)
|
|
346
|
+
|
|
347
|
+
def __map_job_status_to_stream(self, status):
|
|
348
|
+
logging.debug("Job status='%s'" % str(status))
|
|
349
|
+
if status.succeeded is not None:
|
|
350
|
+
return "stdout"
|
|
351
|
+
elif status.failed is not None:
|
|
352
|
+
return "stderr"
|
|
353
|
+
else:
|
|
354
|
+
return "unknown"
|
|
355
|
+
|
|
356
|
+
def __prepare_environment(self):
|
|
357
|
+
env = []
|
|
358
|
+
|
|
359
|
+
for key in self.__env.keys():
|
|
360
|
+
env.append(
|
|
361
|
+
client.V1EnvVar(
|
|
362
|
+
name=key,
|
|
363
|
+
value_from=client.V1EnvVarSource(
|
|
364
|
+
secret_key_ref=client.V1SecretKeySelector(
|
|
365
|
+
name=self.name, key=key
|
|
366
|
+
)
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return env
|
|
372
|
+
|
|
373
|
+
def execute(self, code: str) -> tuple[str, str]:
|
|
374
|
+
try:
|
|
375
|
+
self.__create_job_object(code=code)
|
|
376
|
+
|
|
377
|
+
if self.jupyter_logger is not None:
|
|
378
|
+
self.jupyter_logger(f"Job {self.name} created")
|
|
379
|
+
|
|
380
|
+
stream = self.__complete_and_get_job_status()
|
|
381
|
+
|
|
382
|
+
job = self.__get_pods_in_job()
|
|
383
|
+
logs = self.__get_job_logs(job)
|
|
384
|
+
|
|
385
|
+
return logs, stream
|
|
386
|
+
except KeyboardInterrupt:
|
|
387
|
+
return "Task interrupted by user", "stderr"
|
|
388
|
+
except:
|
|
389
|
+
logging.info("An error occurred.")
|
|
390
|
+
return "An error occurred.", "stderr"
|
|
391
|
+
finally:
|
|
392
|
+
self.__delete_job()
|
|
393
|
+
|
|
394
|
+
def abort(self):
|
|
395
|
+
self.__delete_job()
|
q8s/install.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
from jupyter_client.kernelspec import KernelSpecManager
|
|
9
|
+
from IPython.utils.tempdir import TemporaryDirectory
|
|
10
|
+
|
|
11
|
+
# from .resources import _ICON_PATH
|
|
12
|
+
|
|
13
|
+
kernel_json = {
|
|
14
|
+
"argv": [sys.executable, "-m", "q8s", "-f", "{connection_file}"],
|
|
15
|
+
"display_name": "Q8s kernel",
|
|
16
|
+
"language": "python",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def install_my_kernel_spec(user=True, prefix=None):
|
|
21
|
+
with TemporaryDirectory() as td:
|
|
22
|
+
os.chmod(td, 0o755) # Starts off as 700, not user readable
|
|
23
|
+
with open(os.path.join(td, "kernel.json"), "w") as f:
|
|
24
|
+
json.dump(kernel_json, f, sort_keys=True)
|
|
25
|
+
# shutil.copyfile(_ICON_PATH, pathlib.Path(td) / _ICON_PATH.name)
|
|
26
|
+
print("Installing q8s kernel spec")
|
|
27
|
+
KernelSpecManager().install_kernel_spec(td, "q8s", user=user, prefix=prefix)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_root():
|
|
31
|
+
try:
|
|
32
|
+
return os.geteuid() == 0
|
|
33
|
+
except AttributeError:
|
|
34
|
+
return False # assume not an admin on non-Unix platforms
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv=None):
|
|
38
|
+
parser = argparse.ArgumentParser(
|
|
39
|
+
description="Install KernelSpec for Qubernetes Kernel"
|
|
40
|
+
)
|
|
41
|
+
prefix_locations = parser.add_mutually_exclusive_group()
|
|
42
|
+
|
|
43
|
+
prefix_locations.add_argument(
|
|
44
|
+
"--user",
|
|
45
|
+
help="Install KernelSpec in user's home directory",
|
|
46
|
+
action="store_true",
|
|
47
|
+
)
|
|
48
|
+
prefix_locations.add_argument(
|
|
49
|
+
"--sys-prefix",
|
|
50
|
+
help="Install KernelSpec in sys.prefix. Useful in conda / virtualenv",
|
|
51
|
+
action="store_true",
|
|
52
|
+
dest="sys_prefix",
|
|
53
|
+
)
|
|
54
|
+
prefix_locations.add_argument(
|
|
55
|
+
"--prefix", help="Install KernelSpec in this prefix", default=None
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
args = parser.parse_args(argv)
|
|
59
|
+
|
|
60
|
+
user = False
|
|
61
|
+
prefix = None
|
|
62
|
+
if args.sys_prefix:
|
|
63
|
+
prefix = sys.prefix
|
|
64
|
+
elif args.prefix:
|
|
65
|
+
prefix = args.prefix
|
|
66
|
+
elif args.user or not _is_root():
|
|
67
|
+
user = True
|
|
68
|
+
|
|
69
|
+
install_my_kernel_spec(user=user, prefix=prefix)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|