Allocate new volumes remotely
This commit is contained in:
parent
73a618228b
commit
9de82a9b47
@ -49,3 +49,28 @@ roleRef:
|
|||||||
kind: ClusterRole
|
kind: ClusterRole
|
||||||
name: rawfile-external-provisioner-runner
|
name: rawfile-external-provisioner-runner
|
||||||
apiGroup: rbac.authorization.k8s.io
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
---
|
||||||
|
kind: ClusterRole
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: rawfile-handler
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["persistentvolumes"]
|
||||||
|
verbs: ["get"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch"]
|
||||||
|
---
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: rawfile-handler
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: rawfile-csi-controller
|
||||||
|
namespace: kube-system
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: rawfile-handler
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
0
orchestrator/__init__.py
Normal file
0
orchestrator/__init__.py
Normal file
65
orchestrator/k8s.py
Normal file
65
orchestrator/k8s.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import pykube
|
||||||
|
import yaml
|
||||||
|
from munch import Munch
|
||||||
|
|
||||||
|
api = pykube.HTTPClient(pykube.KubeConfig.from_env())
|
||||||
|
|
||||||
|
|
||||||
|
def volume_to_node(volume_id):
|
||||||
|
pv = pykube.PersistentVolume.objects(api).get_by_name(name=volume_id)
|
||||||
|
pv = Munch.fromDict(pv.obj)
|
||||||
|
node_name = pv.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0][
|
||||||
|
"values"
|
||||||
|
][0]
|
||||||
|
expected_node_affainity = yaml.safe_load(
|
||||||
|
f"""
|
||||||
|
required:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: hostname
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- {node_name}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert pv.spec.nodeAffinity == expected_node_affainity
|
||||||
|
return node_name
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for(pred, desc=""):
|
||||||
|
print(f"Waiting for {desc}", end="", flush=True)
|
||||||
|
while not pred():
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
sleep(0.5)
|
||||||
|
print(" done")
|
||||||
|
|
||||||
|
|
||||||
|
def run_on_node(fn, node):
|
||||||
|
name = f"task-{uuid.uuid4()}"
|
||||||
|
ctx = {
|
||||||
|
"name": name,
|
||||||
|
"namespace": "kube-system", # FIXME
|
||||||
|
"nodeSelector": json.dumps({"kubernetes.io/hostname": node}),
|
||||||
|
"cmd": json.dumps(fn),
|
||||||
|
}
|
||||||
|
template = Path("./templates/task.yaml").read_bytes().decode()
|
||||||
|
manifest = template.format(**ctx)
|
||||||
|
obj = yaml.safe_load(manifest)
|
||||||
|
task_pod = pykube.Pod(api, obj)
|
||||||
|
task_pod.create()
|
||||||
|
|
||||||
|
def is_finished():
|
||||||
|
task_pod.reload()
|
||||||
|
status = task_pod.obj["status"]
|
||||||
|
if status["phase"] in ["Succeeded", "Failed"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
wait_for(is_finished, "task to finish")
|
||||||
|
if task_pod.obj["status"]["phase"] != "Succeeded":
|
||||||
|
raise Exception(f"Task {name} failed")
|
@ -4,7 +4,9 @@ from google.protobuf.wrappers_pb2 import BoolValue
|
|||||||
|
|
||||||
from consts import DATA_DIR
|
from consts import DATA_DIR
|
||||||
from csi import csi_pb2, csi_pb2_grpc
|
from csi import csi_pb2, csi_pb2_grpc
|
||||||
|
from orchestrator.k8s import volume_to_node, run_on_node
|
||||||
from util import log_grpc_request, run
|
from util import log_grpc_request, run
|
||||||
|
from remote import init_rawfile, scrub
|
||||||
|
|
||||||
NODE_NAME_TOPOLOGY_KEY = "hostname"
|
NODE_NAME_TOPOLOGY_KEY = "hostname"
|
||||||
|
|
||||||
@ -48,10 +50,7 @@ class RawFileNodeServicer(csi_pb2_grpc.NodeServicer):
|
|||||||
mount_path = request.target_path
|
mount_path = request.target_path
|
||||||
img_dir = Path(f"{DATA_DIR}/{request.volume_id}")
|
img_dir = Path(f"{DATA_DIR}/{request.volume_id}")
|
||||||
img_file = Path(f"{img_dir}/raw.img")
|
img_file = Path(f"{img_dir}/raw.img")
|
||||||
img_dir.mkdir(parents=False, exist_ok=True)
|
|
||||||
if not img_file.exists():
|
|
||||||
run(f"truncate -s1G {img_file}")
|
|
||||||
run(f"mkfs.ext4 {img_file}")
|
|
||||||
run(f"mount {img_file} {mount_path}")
|
run(f"mount {img_file} {mount_path}")
|
||||||
return csi_pb2.NodePublishVolumeResponse()
|
return csi_pb2.NodePublishVolumeResponse()
|
||||||
|
|
||||||
@ -81,16 +80,20 @@ class RawFileControllerServicer(csi_pb2_grpc.ControllerServicer):
|
|||||||
|
|
||||||
@log_grpc_request
|
@log_grpc_request
|
||||||
def CreateVolume(self, request, context):
|
def CreateVolume(self, request, context):
|
||||||
# TODO: capacity_range
|
|
||||||
# TODO: volume_capabilities
|
# TODO: volume_capabilities
|
||||||
|
size = request.capacity_range.required_bytes
|
||||||
node_name = request.accessibility_requirements.preferred[0].segments[
|
node_name = request.accessibility_requirements.preferred[0].segments[
|
||||||
NODE_NAME_TOPOLOGY_KEY
|
NODE_NAME_TOPOLOGY_KEY
|
||||||
]
|
]
|
||||||
|
|
||||||
|
run_on_node(
|
||||||
|
init_rawfile.as_cmd(volume_id=request.name, size=size), node=node_name
|
||||||
|
)
|
||||||
|
|
||||||
return csi_pb2.CreateVolumeResponse(
|
return csi_pb2.CreateVolumeResponse(
|
||||||
volume=csi_pb2.Volume(
|
volume=csi_pb2.Volume(
|
||||||
volume_id=request.name,
|
volume_id=request.name,
|
||||||
capacity_bytes=0,
|
capacity_bytes=size,
|
||||||
accessible_topology=[
|
accessible_topology=[
|
||||||
csi_pb2.Topology(segments={NODE_NAME_TOPOLOGY_KEY: node_name})
|
csi_pb2.Topology(segments={NODE_NAME_TOPOLOGY_KEY: node_name})
|
||||||
],
|
],
|
||||||
@ -99,6 +102,6 @@ class RawFileControllerServicer(csi_pb2_grpc.ControllerServicer):
|
|||||||
|
|
||||||
@log_grpc_request
|
@log_grpc_request
|
||||||
def DeleteVolume(self, request, context):
|
def DeleteVolume(self, request, context):
|
||||||
pv_name = request.volume_id
|
node_name = volume_to_node(request.volume_id)
|
||||||
# TODO: Run a pod on that node to scrub the data
|
run_on_node(scrub.as_cmd(volume_id=request.volume_id), node=node_name)
|
||||||
return csi_pb2.DeleteVolumeResponse()
|
return csi_pb2.DeleteVolumeResponse()
|
||||||
|
20
remote.py
Normal file
20
remote.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from util import remote_fn
|
||||||
|
|
||||||
|
|
||||||
|
@remote_fn
|
||||||
|
def scrub(volume_id):
|
||||||
|
# TODO: stub
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@remote_fn
|
||||||
|
def init_rawfile(volume_id, size):
|
||||||
|
from util import run
|
||||||
|
from consts import DATA_DIR
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
img_dir = Path(f"{DATA_DIR}/{volume_id}")
|
||||||
|
img_dir.mkdir(parents=False, exist_ok=False)
|
||||||
|
img_file = Path(f"{img_dir}/raw.img")
|
||||||
|
run(f"truncate -s {size} {img_file}")
|
||||||
|
run(f"mkfs.ext4 {img_file}")
|
@ -1,3 +1,6 @@
|
|||||||
grpcio-tools
|
grpcio-tools
|
||||||
grpcio
|
grpcio
|
||||||
click
|
click
|
||||||
|
pyyaml
|
||||||
|
pykube-ng
|
||||||
|
munch
|
||||||
|
@ -4,11 +4,19 @@
|
|||||||
#
|
#
|
||||||
# pip-compile
|
# pip-compile
|
||||||
#
|
#
|
||||||
|
certifi==2020.4.5.1 # via requests
|
||||||
|
chardet==3.0.4 # via requests
|
||||||
click==7.1.1 # via -r requirements.in
|
click==7.1.1 # via -r requirements.in
|
||||||
grpcio-tools==1.28.1 # via -r requirements.in
|
grpcio-tools==1.28.1 # via -r requirements.in
|
||||||
grpcio==1.28.1 # via -r requirements.in, grpcio-tools
|
grpcio==1.28.1 # via -r requirements.in, grpcio-tools
|
||||||
|
idna==2.9 # via requests
|
||||||
|
munch==2.5.0 # via -r requirements.in
|
||||||
protobuf==3.11.3 # via grpcio-tools
|
protobuf==3.11.3 # via grpcio-tools
|
||||||
six==1.14.0 # via grpcio, protobuf
|
pykube-ng==20.4.1 # via -r requirements.in
|
||||||
|
pyyaml==5.3.1 # via -r requirements.in, pykube-ng
|
||||||
|
requests==2.23.0 # via pykube-ng
|
||||||
|
six==1.14.0 # via grpcio, munch, protobuf
|
||||||
|
urllib3==1.25.9 # via requests
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools
|
# setuptools
|
||||||
|
37
templates/task.yaml
Normal file
37
templates/task.yaml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: {name}
|
||||||
|
namespace: {namespace}
|
||||||
|
spec:
|
||||||
|
# FIXME: hardcoded
|
||||||
|
serviceAccount: rawfile-csi-controller
|
||||||
|
restartPolicy: Never
|
||||||
|
terminationGracePeriodSeconds: 0
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
volumes:
|
||||||
|
- name: data-dir
|
||||||
|
hostPath:
|
||||||
|
path: /var/csi/rawfile
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
nodeSelector: {nodeSelector}
|
||||||
|
containers:
|
||||||
|
- name: task
|
||||||
|
# FIXME: use immutable tag
|
||||||
|
image: registry.hamdocker.ir/hamravesh/rawfile-csi:master
|
||||||
|
# FIXME: change to IfNotPresent
|
||||||
|
imagePullPolicy: Always
|
||||||
|
volumeMounts:
|
||||||
|
- name: data-dir
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests: &rsc
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
limit: *rsc
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- {cmd}
|
28
util.py
28
util.py
@ -1,4 +1,7 @@
|
|||||||
|
import base64
|
||||||
import functools
|
import functools
|
||||||
|
import inspect
|
||||||
|
import pickle
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
@ -24,3 +27,28 @@ def log_grpc_request(func):
|
|||||||
|
|
||||||
def run(cmd):
|
def run(cmd):
|
||||||
return subprocess.run(cmd, shell=True, check=True)
|
return subprocess.run(cmd, shell=True, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
class remote_fn(object):
|
||||||
|
def __init__(self, fn):
|
||||||
|
self.fn = fn
|
||||||
|
|
||||||
|
def as_cmd(self, *args, **kwargs):
|
||||||
|
call_data = [inspect.getsource(self.fn).encode(), args, kwargs]
|
||||||
|
call_data_serialized = base64.b64encode(pickle.dumps(call_data))
|
||||||
|
|
||||||
|
run_cmd = f"""
|
||||||
|
python <<EOF
|
||||||
|
import base64
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
remote_fn = lambda fn: fn # FIXME: dirty hack
|
||||||
|
call_data = pickle.loads(base64.b64decode({call_data_serialized}))
|
||||||
|
exec(call_data[0])
|
||||||
|
{self.fn.__name__}(*call_data[1], **call_data[2])
|
||||||
|
EOF
|
||||||
|
"""
|
||||||
|
return run_cmd
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
raise Exception("Should only be run inside pod")
|
||||||
|
Loading…
Reference in New Issue
Block a user