#!/usr/bin/python3 import json import os import sys import subprocess from pathlib import Path from middlewared.client import Client from middlewared.service import ValidationErrors, CallError def path_in_locked_datasets(path: str) -> bool: with Client() as c: return c.call('pool.dataset.path_in_locked_datasets', path) def get_configured_user_group(path: str) -> dict: with Client() as c: return c.call('filesystem.stat', path) def get_host_path_attachments(path: str) -> set: with Client() as c: return { attachment['type'] for attachment in c.call('pool.dataset.attachments_with_path', path) if attachment['type'].lower() not in ['kubernetes', 'chart releases'] } def get_kubernetes_config() -> dict: with Client() as c: return c.call('kubernetes.config') def validate_host_path(path: str, schema_name: str, verrors: ValidationErrors) -> None: """ These validations are taken from `FilesystemService._common_perm_path_validate`. Including an additional validation that makes sure all the children under a path are on same device. """ schema_name += ".migration.chown" p = Path(path) if not p.is_absolute(): verrors.add(schema_name, f"Must be an absolute path: {path}") if p.is_file(): verrors.add(schema_name, f"Recursive operations on a file are invalid: {path}") if not p.absolute().as_posix().startswith("/mnt/"): verrors.add( schema_name, f"Changes to permissions on paths that are not beneath the directory /mnt are not permitted: {path}" ) elif len(p.resolve().parents) == 2: verrors.add(schema_name, f"The specified path is a ZFS pool mountpoint: {path}") # Make sure that dataset is not locked if path_in_locked_datasets(path): verrors.add(schema_name, f"Dataset is locked at path: {path}.") # Validate attachments if attachments := get_host_path_attachments(path): verrors.add(schema_name, f"The path '{path}' is already attached to service(s): {', '.join(attachments)}.") # Make sure all the minio's data directory children are on same device. device_id = os.stat(path).st_dev for root, dirs, files in os.walk(path): for child in dirs + files: abs_path = os.path.join(root, child) if os.stat(abs_path).st_dev != device_id: verrors.add( schema_name, (f"All the children of MinIO data directory should be on " f"same device as root: path={abs_path} device={os.stat(abs_path).st_dev}") ) break def migrate(values: dict) -> dict: # minio user / group ID uid = gid = 473 verrors = ValidationErrors() k8s_config = get_kubernetes_config() if values["appVolumeMounts"]["export"]["hostPathEnabled"]: host_path = values["appVolumeMounts"]["export"]["hostPath"] else: app_dataset = values["appVolumeMounts"]["export"]["datasetName"] host_path = os.path.join( "/mnt", k8s_config['dataset'], "releases", values["release_name"], "volumes/ix_volumes", app_dataset ) current_config = get_configured_user_group(host_path) if current_config["uid"] == uid and current_config["gid"] == gid: return values validate_host_path(host_path, values['release_name'], verrors) verrors.check() # chown the host path acltool = subprocess.run([ "/usr/bin/nfs4xdr_winacl", "-a", "chown", "-O", str(uid), "-G", str(gid), "-r", "-c", host_path, "-p", host_path], check=False, capture_output=True ) if acltool.returncode != 0: raise CallError(f"acltool [chown] on path {host_path} failed with error: [{acltool.stderr.decode().strip()}]") return values if __name__ == "__main__": if len(sys.argv) != 2: exit(1) if os.path.exists(sys.argv[1]): with open(sys.argv[1], "r") as f: print(json.dumps(migrate(json.loads(f.read()))))