Skip to content

Commit 55dda4a

Browse files
authored
ydbd_slice remove dd command (#14498)
1 parent 88b1d54 commit 55dda4a

File tree

7 files changed

+348
-14
lines changed

7 files changed

+348
-14
lines changed

ydb/tools/ydbd_slice/handlers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _get_all_drives(self):
6060
def _format_drives(self):
6161
tasks = []
6262
for (host_name, drive_path) in self._get_all_drives():
63-
cmd = "sudo dd if=/dev/zero of={} bs=1M count=1 status=none conv=notrunc".format(drive_path)
63+
cmd = "sudo {} admin bs disk obliterate {}".format(self.slice_kikimr_path, drive_path)
6464
tasks.extend(self.nodes.execute_async_ret(cmd, nodes=[host_name]))
6565
self.nodes._check_async_execution(tasks)
6666

@@ -125,11 +125,11 @@ def slice_install(self):
125125
self._clear_logs()
126126

127127
if 'kikimr' in self.components:
128-
self._format_drives()
129-
130128
if 'bin' in self.components.get('kikimr', []):
131129
self._update_kikimr()
132130

131+
self._format_drives()
132+
133133
if 'cfg' in self.components.get('kikimr', []):
134134
static_cfg_path = self.configurator.create_static_cfg()
135135
self._update_cfg(static_cfg_path)

ydb/tools/ydbd_slice/kube/api.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import time
44
import logging
5-
from kubernetes.client import ApiClient as KubeApiClient, CustomObjectsApi, ApiException, CoreV1Api
5+
from kubernetes.client import ApiClient as KubeApiClient, CustomObjectsApi, ApiException, CoreV1Api, BatchV1Api
66
from kubernetes.config import load_kube_config
77
from ydb.tools.ydbd_slice.kube import kubectl
88

@@ -221,6 +221,11 @@ def get_storage(api_client, namespace, name):
221221
)
222222

223223

224+
def get_jobs(api_client, namespace):
225+
api = BatchV1Api(api_client)
226+
return api.list_namespaced_job(namespace=namespace)
227+
228+
224229
def create_storage(api_client, body):
225230
namespace = body['metadata']['namespace']
226231
name = body['metadata']['name']
@@ -422,3 +427,77 @@ def wait_pods_deleted(api_client, pods, timeout=1800):
422427
time.sleep(1)
423428

424429
raise TimeoutError('waiting for pods to delete timed out')
430+
431+
432+
def get_job_pods(api_client, namespace, job_name):
433+
api = CoreV1Api(api_client)
434+
return api.list_namespaced_pod(namespace, label_selector=f'job-name={job_name}').items
435+
436+
437+
def delete_job_pods(api_client, namespace, job_name):
438+
api = CoreV1Api(api_client)
439+
pods = api.list_namespaced_pod(namespace, label_selector=f'job-name={job_name}')
440+
for pod in pods.items:
441+
if pod.status.phase == 'Succeeded':
442+
try:
443+
api.delete_namespaced_pod(pod.metadata.name, namespace)
444+
except ApiException as e:
445+
if e.status == 404:
446+
pass
447+
else:
448+
raise
449+
else:
450+
logger.error(f'Pod {pod.metadata.name} in namespace {namespace} has not completed successfully')
451+
raise RuntimeError(f'Pod {pod.metadata.name} in namespace {namespace} has not completed successfully')
452+
453+
return
454+
455+
456+
def wait_job_pods_completed(api_client, namespace, job_name, timeout=300):
457+
logger.debug(f'waiting for job pods to complete: {namespace}/{job_name}')
458+
end_ts = time.time() + timeout
459+
while time.time() < end_ts:
460+
pods = get_job_pods(api_client, namespace, job_name)
461+
if not pods:
462+
return
463+
for pod in pods:
464+
if pod.status.phase != 'Succeeded':
465+
break
466+
else:
467+
return
468+
time.sleep(5)
469+
raise TimeoutError(f'waiting for job pods in namespace {namespace} to complete timed out')
470+
471+
472+
def create_job(api_client, namespace, body):
473+
logger.debug(f'creating job: {namespace}/{body["metadata"]["name"]}')
474+
api = BatchV1Api(api_client)
475+
body = add_kubectl_last_applied_configuration(body)
476+
return api.create_namespaced_job(namespace=namespace, body=body)
477+
478+
479+
def delete_job(api_client, namespace, name):
480+
logger.debug(f'deleting job: {namespace}/{name}')
481+
api = BatchV1Api(api_client)
482+
try:
483+
return api.delete_namespaced_job(name=name, namespace=namespace)
484+
except ApiException as e:
485+
if e.status == 404:
486+
return
487+
raise
488+
489+
490+
def wait_jobs_completed(api_client, namespace, timeout=300):
491+
logger.debug(f'waiting for jobs to complete: {namespace}')
492+
end_ts = time.time() + timeout
493+
while time.time() < end_ts:
494+
jobs = get_jobs(api_client, namespace)
495+
if not jobs.items:
496+
return
497+
498+
for job in jobs.items:
499+
if not job.status.succeeded:
500+
break
501+
return
502+
time.sleep(5)
503+
raise TimeoutError(f'waiting for jobs in namespace {namespace} to complete timed out')

ydb/tools/ydbd_slice/kube/generate.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ def generate_dynconfigs(project_path, namespace_name, cluster_uuid, preferred_po
6363
)
6464

6565

66+
def generate_obliterate(project_path, namespace_name, nodeclaim_name, ydb_image, nodes_list):
67+
for node_name in nodes_list:
68+
generate_file(
69+
project_path=project_path,
70+
filename=f'obliterate-{namespace_name}-{node_name}.yaml',
71+
template='/ydbd_slice/templates/common/obliterate.yaml',
72+
template_kwargs=dict(
73+
namespace_name=namespace_name,
74+
nodeclaim_name=nodeclaim_name,
75+
node_name=node_name,
76+
ydb_image=ydb_image
77+
)
78+
)
79+
80+
6681
def generate_8_node_block_4_2(project_path, user, namespace_name, nodeclaim_name, node_flavor,
6782
storage_name, database_name, cluster_uuid=''):
6883
generate_file(

ydb/tools/ydbd_slice/kube/handlers.py

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from kubernetes.client import Configuration
88

99
from ydb.tools.ydbd_slice import nodes
10-
from ydb.tools.ydbd_slice.kube import api, kubectl, yaml, generate, cms, dynconfig, docker
10+
from ydb.tools.ydbd_slice.kube import api, kubectl, yaml, generate, cms, dynconfig, docker, utils
1111

1212

1313
logger = logging.getLogger(__name__)
@@ -86,6 +86,36 @@ def get_all_manifests(directory):
8686
return result
8787

8888

89+
def get_job_manifests(directory):
90+
result = []
91+
for file in os.listdir(directory):
92+
path = os.path.abspath(os.path.join(directory, file))
93+
if not file.endswith(('.yaml', '.yml')):
94+
logger.info('skipping file: %s, not yaml file extension', path)
95+
continue
96+
try:
97+
with open(path) as file:
98+
data = yaml.load(file)
99+
except Exception as e:
100+
logger.error('failed to open and parse file: %s, error: %s', path, str(e))
101+
continue
102+
103+
if not utils.is_kubernetes_manifest(data):
104+
logger.info('skipping file: %s, not kubernetes manifest', path)
105+
continue
106+
107+
api_version = data['apiVersion']
108+
kind = data['kind'].lower()
109+
namespace = data['metadata'].get('namespace')
110+
name = data['metadata']['name']
111+
result.append((path, api_version, kind, namespace, name, data))
112+
113+
if not result:
114+
raise RuntimeError(f'failed to find any manifests in {os.path.abspath(directory)}')
115+
116+
return result
117+
118+
89119
def validate_components_selector(value):
90120
if not re.match(r'^[a-zA-Z][a-zA-Z0-9\-]*$', value):
91121
raise ValueError('invalid value: %s' % value)
@@ -157,6 +187,45 @@ def get_domain(api_client, project_path, manifests):
157187
return data['spec']['domain']
158188

159189

190+
def get_namespace_nodeclaim_image(manifests):
191+
"""
192+
Extracts the namespace, name, and image name from the first suitable nodeclaim manifest.
193+
194+
Args:
195+
manifests (list): A list of tuples, where each tuple contains:
196+
- path (str): The file path of the manifest.
197+
- api_version (str): The API version of the manifest.
198+
- kind (str): The kind of the manifest.
199+
- namespace (str): The namespace of the manifest.
200+
- name (str): The name of the manifest.
201+
- data (dict): The data of the manifest.
202+
203+
Returns:
204+
tuple: A tuple containing:
205+
- namespace (str): The namespace of the nodeclaim.
206+
- name (str): The name of the nodeclaim.
207+
- image_name (str): The image name specified in the nodeclaim.
208+
209+
Raises:
210+
RuntimeError: If no suitable nodeclaim manifest is found.
211+
"""
212+
nodeclaim_namespace, nodeclaim_name, image_name = "", "", ""
213+
for path, _, kind, namespace, name, data in utils.filter_manifests(manifests, 'ydb.tech/v1alpha1', ['nodeclaim', 'storage']):
214+
if kind == 'nodeclaim' and not nodeclaim_name:
215+
nodeclaim_namespace = namespace
216+
nodeclaim_name = name
217+
elif kind == 'storage' and not image_name:
218+
try:
219+
image_name = data['spec']['image']['name']
220+
except KeyError:
221+
pass
222+
if namespace and nodeclaim_name and image_name:
223+
return nodeclaim_namespace, nodeclaim_name, image_name
224+
225+
if not namespace or not nodeclaim_name or not image_name:
226+
raise RuntimeError(f"No suitable nodeclaim or storage manifest found. Namespace: {namespace}, NodeClaim: {nodeclaim_name}, Image: {image_name}")
227+
228+
160229
def manifests_ydb_set_image(project_path, manifests, image):
161230
for (path, api_version, kind, namespace, name, data) in manifests:
162231
if not (kind in ['storage', 'database'] and api_version in ['ydb.tech/v1alpha1']):
@@ -254,21 +323,78 @@ def slice_nodeclaim_nodes(api_client, project_path, manifests):
254323

255324

256325
def slice_nodeclaim_format(api_client, project_path, manifests):
326+
"""
327+
Formats and processes node claims by creating, waiting for completion, and deleting Kubernetes jobs.
328+
329+
This function performs the following steps:
330+
1. Creates a directory for job manifests if it doesn't exist.
331+
2. Retrieves the namespace, node claim, and YDB image from the provided manifests.
332+
3. Retrieves the list of nodes.
333+
4. Generates obliterate jobs and saves them to the jobs directory.
334+
5. Creates and waits for the completion of each job.
335+
6. Deletes the jobs and their associated pods.
336+
337+
Args:
338+
api_client (object): The Kubernetes API client.
339+
project_path (str): The path to the project directory.
340+
manifests (dict): The manifests containing the namespace, node claim, and YDB image information.
341+
342+
Raises:
343+
SystemExit: If no namespace or node claim is found, or if no nodes are found.
344+
TimeoutError: If some jobs do not complete within the expected time.
345+
Exception: If an error occurs during job processing or cleanup.
346+
347+
Note:
348+
This function logs errors and exits the program if critical issues are encountered.
349+
"""
350+
jobs_path = os.path.join(project_path, 'jobs')
351+
if not os.path.exists(jobs_path):
352+
os.makedirs(jobs_path)
353+
354+
namespace, nodeclaim, ydb_image = get_namespace_nodeclaim_image(manifests)
355+
if not namespace or not nodeclaim:
356+
logger.error("No namespace or nodclaim found, nothing to format.")
357+
sys.exit(2)
358+
257359
node_list = get_nodes(api_client, project_path, manifests)
258360
if len(node_list) == 0:
259-
logger.info('no nodes found, nothing to format.')
260-
return
261-
node_list = nodes.Nodes(node_list)
262-
cmd = r"sudo find /dev/disk/ -path '*/by-partlabel/kikimr_*' " \
263-
r"-exec dd if=/dev/zero of={} bs=1M count=1 status=none \;"
264-
node_list.execute_async(cmd)
361+
logger.error("No nodes found, nothing to format.")
362+
sys.exit(2)
363+
364+
# save obliterate jobs to project_path/jobs for debug porposes and to be able to rerun them manually
365+
generate.generate_obliterate(jobs_path, namespace, nodeclaim, ydb_image, node_list)
366+
jobs = get_job_manifests(jobs_path)
367+
368+
try:
369+
for (_, _, _, namespace, name, data) in utils.filter_manifests(jobs, 'batch/v1', ['job']):
370+
api.create_job(api_client, namespace, data)
371+
372+
# wait for job completion and job pods completion
373+
api.wait_jobs_completed(api_client, namespace)
374+
375+
# cleanup jobs and pods
376+
for (_, _, _, namespace, name, data) in utils.filter_manifests(jobs, 'batch/v1', ['job']):
377+
api.wait_job_pods_completed(api_client, namespace, name)
378+
api.delete_job(api_client, namespace, name)
379+
api.delete_job_pods(api_client, namespace, name)
380+
381+
except TimeoutError as e:
382+
logger.error(f"Some jobs did not complete within the expected time. Please check the job manifests in {jobs_path} for manual rerun.")
383+
sys.exit(e.args[0])
384+
except Exception as e:
385+
logger.error(f"An error occurred: {e}")
386+
sys.exit(f"""
387+
An error occurred while processing the jobs. This might indicate an issue with the job's execution or cleanup process.
388+
To investigate further, you can check the status of the jobs and pods by running:
389+
kubectl get jobs -n {namespace}
390+
kubectl get pods -n {namespace} -l job-name={name}
391+
You may need to manually delete these jobs or pods if they are stuck or not terminating properly.
392+
""")
265393

266394

267395
def slice_nodeclaim_delete(api_client, project_path, manifests):
268396
nodeclaims = []
269-
for (path, api_version, kind, namespace, name, data) in manifests:
270-
if not (kind in ['nodeclaim'] and api_version in ['ydb.tech/v1alpha1']):
271-
continue
397+
for (path, api_version, kind, namespace, name, data) in utils.filter_manifests(manifests, 'ydb.tech/v1alpha1', ['nodeclaim']):
272398
namespace = data['metadata']['namespace']
273399
name = data['metadata']['name']
274400
api.delete_nodeclaim(api_client, namespace, name)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
apiVersion: batch/v1
2+
kind: Job
3+
metadata:
4+
# https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
5+
# 255 characters is the maximum length of a DNS subdomain name
6+
name: ydbd-obl-job-{{ node_name.split('.')[0] | truncate(180) }}
7+
namespace: {{ namespace_name }}
8+
annotations:
9+
ydb.tech/node-claim: {{ nodeclaim_name }}.{{ namespace_name }}
10+
spec:
11+
template:
12+
spec:
13+
nodeSelector:
14+
kubernetes.io/hostname: "{{ node_name }}"
15+
tolerations:
16+
- effect: "NoSchedule"
17+
operator: Exists
18+
key: "ydb.tech/node-claim"
19+
- effect: NoExecute
20+
key: node.kubernetes.io/unreachable
21+
operator: Exists
22+
- effect: NoExecute
23+
key: node.kubernetes.io/not-ready
24+
operator: Exists
25+
containers:
26+
- name: ydbd-obliterate
27+
image: {{ ydb_image }}
28+
imagePullPolicy: IfNotPresent
29+
# Obliterate function doesn't sync the disk after the operation, so we need to do it manually
30+
# We don't have such problem on baremetal, because ydbd running in the same namespace witqh obliterate
31+
command:
32+
- 'sh'
33+
- '-c'
34+
- 'find /dev/disk/ -path "*/by-partlabel/kikimr_*" -exec /opt/ydb/bin/ydbd admin bs disk obliterate {} \; && sync'
35+
resources:
36+
limits:
37+
ydb-disk-manager/hostdev: '1'
38+
requests:
39+
ydb-disk-manager/hostdev: '1'
40+
securityContext:
41+
capabilities:
42+
add: ["SYS_RAWIO"]
43+
restartPolicy: Never

0 commit comments

Comments
 (0)