Skip to content

Commit

Permalink
module enhancement
Browse files Browse the repository at this point in the history
rename prefligt_check to preflight
implement jinja2 templating for modules

Signed-off-by: Jan-Marten Brüggemann <[email protected]>
  • Loading branch information
brueggemann committed Mar 20, 2024
1 parent 75eef77 commit cdb0c2e
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 22 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ cython_debug/
data.yaml
config.yaml
.ceph
.k8s
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions src/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions src/rookify/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-

import os
import rookify.modules

from types import MappingProxyType
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/rookify/modules/analyze_ceph/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions src/rookify/modules/example/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
4 changes: 2 additions & 2 deletions src/rookify/modules/migrate_osds/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}
Expand Down
94 changes: 84 additions & 10 deletions src/rookify/modules/module.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
"""
Expand Down Expand Up @@ -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

0 comments on commit cdb0c2e

Please sign in to comment.