diff --git a/.gitignore b/.gitignore index d8dd9a3..8174be4 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ cython_debug/ data.yaml config.yaml .ceph +.k8s diff --git a/requirements.txt b/requirements.txt index 9de726f..0d819fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,9 @@ fabric==3.2.2 google-auth==2.28.1 idna==3.6 invoke==2.2.0 +Jinja2==3.1.3 kubernetes==29.0.0 +MarkupSafe==2.1.5 oauthlib==3.2.2 paramiko==3.4.0 pyasn1==0.5.1 diff --git a/src/config.example.yaml b/src/config.example.yaml index 7e60f64..4ed0fdd 100644 --- a/src/config.example.yaml +++ b/src/config.example.yaml @@ -19,8 +19,14 @@ ssh: user: dragon kubernetes: - host: 192.168.22.10 - api_key: abc + config: ../.k8s/config + +rook: + cluster: + name: osism-ceph + namespace: rook-ceph + ceph: + image: quay.io/ceph/ceph:v18.2.1 migration_modules: - migrate_osds diff --git a/src/rookify/__main__.py b/src/rookify/__main__.py index bada9ba..cac2ed7 100644 --- a/src/rookify/__main__.py +++ b/src/rookify/__main__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import os import rookify.modules from types import MappingProxyType @@ -21,21 +22,34 @@ def main() -> None: except FileNotFoundError: pass + # Get absolute path of the rookify instance + rookify_path = os.path.dirname(__file__) + # Run preflight requirement modules for preflight_module in preflight_modules: + module_path = os.path.join( + rookify_path, "modules", preflight_module.MODULE_NAME + ) handler = preflight_module.HANDLER_CLASS( - config=MappingProxyType(config), data=MappingProxyType(module_data) + config=MappingProxyType(config), + data=MappingProxyType(module_data), + module_path=module_path, ) result = handler.run() module_data[preflight_module.MODULE_NAME] = result - # Run preflight checks and append handlers to list + # Run preflight and append handlers to list handlers = list() for migration_module in migration_modules: + module_path = os.path.join( + rookify_path, "modules", migration_module.MODULE_NAME + ) handler = migration_module.HANDLER_CLASS( - config=MappingProxyType(config), data=MappingProxyType(module_data) + config=MappingProxyType(config), + data=MappingProxyType(module_data), + module_path=module_path, ) - handler.preflight_check() + handler.preflight() handlers.append((migration_module, handler)) # Run migration modules diff --git a/src/rookify/modules/analyze_ceph/main.py b/src/rookify/modules/analyze_ceph/main.py index cc90105..6cb023f 100644 --- a/src/rookify/modules/analyze_ceph/main.py +++ b/src/rookify/modules/analyze_ceph/main.py @@ -6,7 +6,7 @@ class AnalyzeCephHandler(ModuleHandler): - def run(self) -> Dict[str, Any]: + def run(self) -> Any: commands = ["mon dump", "osd dump", "device ls", "fs dump", "node ls"] results: Dict[str, Any] = dict() diff --git a/src/rookify/modules/example/main.py b/src/rookify/modules/example/main.py index 2d0391e..e62394c 100644 --- a/src/rookify/modules/example/main.py +++ b/src/rookify/modules/example/main.py @@ -2,14 +2,14 @@ from ..module import ModuleHandler, ModuleException -from typing import Any, Dict +from typing import Any class ExampleHandler(ModuleHandler): - def preflight_check(self) -> None: + def preflight(self) -> None: # Do something for checking if all needed preconditions are met else throw ModuleException raise ModuleException("Example module was loaded, so aborting!") - def run(self) -> Dict[str, Any]: + def run(self) -> Any: # Run the migration tasks return {} diff --git a/src/rookify/modules/migrate_osds/main.py b/src/rookify/modules/migrate_osds/main.py index d6b4a34..5377671 100644 --- a/src/rookify/modules/migrate_osds/main.py +++ b/src/rookify/modules/migrate_osds/main.py @@ -6,12 +6,12 @@ class MigrateOSDsHandler(ModuleHandler): - def preflight_check(self) -> None: + def preflight(self) -> None: pass # result = self.ceph.mon_command("osd dump") # raise ModuleException('test error') - def run(self) -> Dict[str, Any]: + def run(self) -> Any: osd_config: Dict[str, Any] = dict() for node, osds in self._data["analyze_ceph"]["node"]["ls"]["osd"].items(): osd_config[node] = {"osds": {}} diff --git a/src/rookify/modules/module.py b/src/rookify/modules/module.py index 84208e5..8d1a75a 100644 --- a/src/rookify/modules/module.py +++ b/src/rookify/modules/module.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- +import os +import yaml import json import abc import rados import kubernetes import fabric +import jinja2 from typing import Any, Dict, List, Optional @@ -21,7 +24,7 @@ class __Ceph: def __init__(self, config: Dict[str, Any]): try: self.__ceph = rados.Rados( - conffile=config["conf_file"], conf={"keyring": config["keyring"]} + conffile=config["config"], conf={"keyring": config["keyring"]} ) self.__ceph.connect() except rados.ObjectNotFound as err: @@ -41,23 +44,61 @@ def mon_command( class __K8s: def __init__(self, config: Dict[str, Any]): - k8s_config = kubernetes.client.Configuration() - k8s_config.api_key = config["api_key"] - k8s_config.host = config["host"] + k8s_config = kubernetes.config.load_kube_config( + config_file=config["config"] + ) self.__client = kubernetes.client.ApiClient(k8s_config) + self.__dynamic_client: Optional[kubernetes.dynamic.DynamicClient] = None @property - def CoreV1Api(self) -> kubernetes.client.CoreV1Api: + def core_v1_api(self) -> kubernetes.client.CoreV1Api: return kubernetes.client.CoreV1Api(self.__client) @property - def AppsV1Api(self) -> kubernetes.client.AppsV1Api: + def apps_v1_api(self) -> kubernetes.client.AppsV1Api: return kubernetes.client.AppsV1Api(self.__client) @property - def NodeV1Api(self) -> kubernetes.client.NodeV1Api: + def node_v1_api(self) -> kubernetes.client.NodeV1Api: return kubernetes.client.NodeV1Api(self.__client) + @property + def custom_objects_api(self) -> kubernetes.client.CustomObjectsApi: + return kubernetes.client.CustomObjectsApi(self.__client) + + @property + def dynamic_client(self) -> kubernetes.dynamic.DynamicClient: + if not self.__dynamic_client: + self.__dynamic_client = kubernetes.dynamic.DynamicClient(self.__client) + return self.__dynamic_client + + def crd_api( + self, api_version: str, kind: str + ) -> kubernetes.dynamic.resource.Resource: + return self.dynamic_client.resources.get(api_version=api_version, kind=kind) + + def crd_api_apply( + self, manifest: Dict[Any, Any] + ) -> kubernetes.dynamic.resource.ResourceInstance: + """ + This applies a manifest for custom CRDs + See https://github.com/kubernetes-client/python/issues/1792 for more information + :param manifest: Dict of the kubernetes manifest + """ + api_version = manifest["apiVersion"] + kind = manifest["kind"] + resource_name = manifest["metadata"]["name"] + namespace = manifest["metadata"]["namespace"] + crd_api = self.crd_api(api_version=api_version, kind=kind) + + try: + crd_api.get(namespace=namespace, name=resource_name) + return crd_api.patch( + body=manifest, content_type="application/merge-patch+json" + ) + except kubernetes.dynamic.exceptions.NotFoundError: + return crd_api.create(body=manifest, namespace=namespace) + class __SSH: def __init__(self, config: Dict[str, Any]): self.__config = config @@ -82,21 +123,48 @@ def command(self, host: str, command: str) -> fabric.runners.Result: ).run(command, hide=True) return result - def __init__(self, config: Dict[str, Any], data: Dict[str, Any]): + class __Template: + def __init__(self, template_path: str): + self.__result_raw: Optional[str] = None + self.__result_yaml: Optional[Any] = None + self.__template_path: str = template_path + with open(template_path) as file: + self.__template = jinja2.Template(file.read()) + + def render(self, **variables: Any) -> None: + self.__result_raw = self.__template.render(**variables) + self.__result_yaml = None + + @property + def raw(self) -> str: + if not self.__result_raw: + raise ModuleException("Template was not rendered") + return self.__result_raw + + @property + def yaml(self) -> Any: + if not self.__result_yaml: + self.__result_yaml = yaml.safe_load(self.raw) + return self.__result_yaml + + def __init__(self, config: Dict[str, Any], data: Dict[str, Any], module_path: str): """ Construct a new 'ModuleHandler' object. - :param module_data: The config and results from modules + :param config: The global config file + :param data: The output of modules required by this module + :param module_path: The filesystem path of this module :return: returns nothing """ self._config = config self._data = data + self.__module_path = module_path self.__ceph: Optional[ModuleHandler.__Ceph] = None self.__k8s: Optional[ModuleHandler.__K8s] = None self.__ssh: Optional[ModuleHandler.__SSH] = None @abc.abstractmethod - def preflight_check(self) -> None: + def preflight(self) -> None: """ Run the modules preflight check """ @@ -128,3 +196,9 @@ def ssh(self) -> __SSH: if self.__ssh is None: self.__ssh = ModuleHandler.__SSH(self._config["ssh"]) return self.__ssh + + def load_template(self, filename: str, **variables: Any) -> __Template: + template_path = os.path.join(self.__module_path, "templates", filename) + template = ModuleHandler.__Template(template_path) + template.render(**variables) + return template