From 317951163311f07c48752e88d02ee924022b8e73 Mon Sep 17 00:00:00 2001 From: Sergey Reshetnyak Date: Wed, 11 Jan 2017 21:47:48 +0300 Subject: [PATCH] Add ccp action support This patch adds support of actions on existing ccp deployment. For example actions can run tempest, rotation fernet tokens and so on. Documentation will be added in another patchset. Change-Id: If45f1bfb823f2182b0e79ca269c6b0e95066d053 --- fuel_ccp/action.py | 235 +++++++++++++++++++++++++++++++++++++++ fuel_ccp/cli.py | 75 +++++++++++++ fuel_ccp/common/utils.py | 13 ++- fuel_ccp/deploy.py | 10 +- fuel_ccp/exceptions.py | 2 + fuel_ccp/kubernetes.py | 7 +- fuel_ccp/templates.py | 6 +- setup.cfg | 4 + 8 files changed, 339 insertions(+), 13 deletions(-) create mode 100644 fuel_ccp/action.py create mode 100644 fuel_ccp/exceptions.py diff --git a/fuel_ccp/action.py b/fuel_ccp/action.py new file mode 100644 index 00000000..e140ac73 --- /dev/null +++ b/fuel_ccp/action.py @@ -0,0 +1,235 @@ +import json +import os +import uuid + +import yaml + +from fuel_ccp.common import utils +from fuel_ccp import config +from fuel_ccp.config import images as config_images +from fuel_ccp import exceptions +from fuel_ccp import kubernetes +from fuel_ccp import templates + + +CONF = config.CONF + + +class Action(object): + def __init__(self, **kwargs): + self.name = kwargs.pop("name") + self.component = kwargs.pop("component") + self.component_dir = kwargs.pop("component_dir") + self.image = kwargs.pop("image") + self.command = kwargs.pop("command") + self.dependencies = kwargs.pop("dependencies", ()) + self.files = kwargs.pop("files", ()) + + if kwargs: + key_names = ", ".join(kwargs.keys()) + raise ValueError("Invalid keys '%s' for '%s' action" % ( + key_names, self.name)) + + @property + def k8s_name(self): + if not hasattr(self, "_k8s_name"): + self._k8s_name = "%s-%s" % (self.name, str(uuid.uuid4())[:8]) + return self._k8s_name + + def validate(self): + pass + + def run(self): + self._create_configmap() + self._create_job() + + # configmap methods + + def _create_configmap(self): + data = { + "config": CONF.configs._json(sort_keys=True), + "workflow": self._get_workflow() + } + data.update(self._get_file_templates()) + + cm = templates.serialize_configmap(self.k8s_name, data) + kubernetes.process_object(cm) + + def _get_workflow(self): + wf = { + "name": self.name, + "dependencies": self.dependencies, + "job": { + "command": self.command + }, + "files": [] + } + for f in self.files: + wf["files"].append({ + "name": f["content"], + "path": f["path"], + "perm": f.get("perm"), + "user": f.get("user") + }) + return json.dumps({"workflow": wf}) + + def _get_file_templates(self): + # TODO(sreshetniak): use imports and add macros CM + data = {} + for f in self.files: + template_path = os.path.join(self.component_dir, + "service", "files", + f["content"]) + with open(template_path) as filedata: + data[f["content"]] = filedata.read() + return data + + # job methods + + def _create_job(self): + cont_spec = { + "name": self.k8s_name, + "image": config_images.image_spec(self.image), + "imagePullPolicy": CONF.kubernetes.image_pull_policy, + "command": templates.get_start_cmd(self.name), + "volumeMounts": [ + { + "name": "config-volume", + "mountPath": "/etc/ccp" + }, + { + "name": "start-script", + "mountPath": "/opt/ccp_start_script/bin" + } + ], + "env": templates.serialize_env_variables({}), + "restartPolicy": "Never" + } + config_volume_items = [ + { + "key": "config", + "path": "globals/globals.json" + }, + { + "key": "workflow", + "path": "role/%s.json" % self.name + } + ] + for f in self.files: + config_volume_items.append({ + "key": f["content"], + "path": "files/%s" % f["content"] + }) + pod_spec = { + "metadata": { + "name": self.k8s_name + }, + "spec": { + "containers": [cont_spec], + "restartPolicy": "Never", + "volumes": [ + { + "name": "config-volume", + "configMap": { + "name": self.k8s_name, + "items": config_volume_items + } + }, + { + "name": "start-script", + "configMap": { + "name": templates.SCRIPT_CONFIG, + "items": [ + { + "key": templates.SCRIPT_CONFIG, + "path": "start_script.py" + } + ] + } + } + ] + } + } + job_spec = templates.serialize_job( + name=self.k8s_name, + spec=pod_spec, + component_name=self.component, + app_name=self.name) + job_spec["metadata"]["labels"].update({"ccp-action": "true"}) + kubernetes.process_object(job_spec) + + +class ActionStatus(object): + @classmethod + def get_actions(cls, action_name): + selector = "ccp-action=true" + if action_name: + selector += "," + "app=%s" % action_name + actions = [] + for job in kubernetes.list_cluster_jobs(selector): + actions.append(cls(job)) + return actions + + def __init__(self, k8s_job): + self.name = k8s_job.name + self.component = k8s_job.labels["ccp-component"] + self.date = k8s_job.obj["metadata"]["creationTimestamp"] + self.restarts = k8s_job.obj["status"].get("failed", 0) + self.active = k8s_job.obj["status"].get("active", 0) + + @property + def status(self): + if self.restarts: + return "fail" + if self.active: + return "wip" + return "ok" + + +def list_actions(): + """List of available actions. + + :returns: list -- list of all available actions + """ + actions = [] + for repo in utils.get_repositories_paths(): + component_name = utils.get_component_name_from_repo_path(repo) + action_path = os.path.join(repo, "service", "actions") + if not os.path.isdir(action_path): + continue + for filename in os.listdir(action_path): + if filename.endswith(".yaml"): + with open(os.path.join(action_path, filename)) as f: + data = yaml.load(f) + for action_dict in data.get("actions", ()): + actions.append(Action(component=component_name, + component_dir=repo, + **action_dict)) + return actions + + +def get_action(action_name): + """Get action by name. + + :returns: Action -- action object + :raises: fuel_ccp.exceptions.NotFoundException + """ + for action in list_actions(): + if action_name == action.name: + return action + raise exceptions.NotFoundException("Action with name '%s' not found" % ( + action_name)) + + +def run_action(action_name): + """Run action. + + :raises: fuel_ccp.exceptions.NotFoundException + """ + action = get_action(action_name) + action.validate() + action.run() + + +def list_action_status(action_name=None): + return ActionStatus.get_actions(action_name) diff --git a/fuel_ccp/cli.py b/fuel_ccp/cli.py index 6faed310..9070a876 100644 --- a/fuel_ccp/cli.py +++ b/fuel_ccp/cli.py @@ -8,8 +8,10 @@ from cliff import app from cliff import command from cliff import commandmanager from cliff import lister +from cliff import show import fuel_ccp +from fuel_ccp import action from fuel_ccp import build from fuel_ccp import cleanup from fuel_ccp.common import utils @@ -250,6 +252,79 @@ class DomainsList(BaseCommand, lister.Lister): return ('Ingress Domain',), zip(domains_list) +# action commands + +class ActionList(BaseCommand, lister.Lister): + """Get list of available actions""" + + def get_parser(self, *args, **kwargs): + parser = super(ActionList, self).get_parser(*args, **kwargs) + return parser + + def take_action(self, parsed_args): + if CONF.repositories.clone: + do_fetch() + config.load_component_defaults() + actions = action.list_actions() + return ("Name", "Component"), [(a.name, a.component) for a in actions] + + +class ActionShow(BaseCommand, show.ShowOne): + """Show action""" + + def get_parser(self, *args, **kwargs): + parser = super(ActionShow, self).get_parser(*args, **kwargs) + parser.add_argument("action", + help="Show details of the action") + return parser + + def take_action(self, parsed_args): + action_obj = action.get_action(parsed_args.action) + return ( + ("Name", + "Component", + "Image"), + (action_obj.name, + action_obj.component, + action_obj.image)) + + +class ActionStatus(BaseCommand, lister.Lister): + """Show list of executed actions""" + + def get_parser(self, *args, **kwargs): + parser = super(ActionStatus, self).get_parser(*args, **kwargs) + parser.add_argument("action", + nargs="?", + help="Show action status") + return parser + + def take_action(self, parsed_args): + return ( + ("Name", + "Component", + "Date", + "Status", + "Restarts"), + ((a.name, a.component, a.date, a.status, a.restarts) + for a in action.list_action_status(parsed_args.action)) + ) + + +class ActionRun(BaseCommand): + """Run action""" + + def get_parser(self, *args, **kwargs): + parser = super(ActionRun, self).get_parser(*args, **kwargs) + parser.add_argument("action", + help="Run action") + return parser + + def take_action(self, parsed_args): + config.load_component_defaults() + action.run_action(parsed_args.action) + + def signal_handler(signo, frame): sys.exit(-signo) diff --git a/fuel_ccp/common/utils.py b/fuel_ccp/common/utils.py index b7fc7545..1d8cb780 100644 --- a/fuel_ccp/common/utils.py +++ b/fuel_ccp/common/utils.py @@ -105,6 +105,14 @@ def get_repositories_exports(repos_names=None): return exports +def get_component_name_from_repo_path(path): + REPO_NAME_PREFIX = "fuel-ccp-" + name = os.path.basename(path) + if name.startswith(REPO_NAME_PREFIX): + name = name[len(REPO_NAME_PREFIX):] + return name + + def get_deploy_components_info(rendering_context=None): if rendering_context is None: rendering_context = CONF.configs._dict @@ -114,10 +122,7 @@ def get_deploy_components_info(rendering_context=None): service_dir = os.path.join(repo, "service") if not os.path.isdir(service_dir): continue - component_name = os.path.basename(repo) - REPO_NAME_PREFIX = "fuel-ccp-" - if component_name.startswith(REPO_NAME_PREFIX): - component_name = component_name[len(REPO_NAME_PREFIX):] + component_name = get_component_name_from_repo_path(repo) component = { "name": component_name, diff --git a/fuel_ccp/deploy.py b/fuel_ccp/deploy.py index ff903e1d..ab765e4e 100644 --- a/fuel_ccp/deploy.py +++ b/fuel_ccp/deploy.py @@ -308,16 +308,18 @@ def _create_globals_configmap(config): return kubernetes.process_object(cm) -def _create_start_script_configmap(): +def get_start_script(): start_scr_path = os.path.join(CONF.repositories.path, CONF.repositories.entrypoint_repo_name, "fuel_ccp_entrypoint", "start_script.py") with open(start_scr_path) as f: - start_scr_data = f.read() + return f.read() + +def create_start_script_configmap(): data = { - templates.SCRIPT_CONFIG: start_scr_data + templates.SCRIPT_CONFIG: get_start_script() } cm = templates.serialize_configmap(templates.SCRIPT_CONFIG, data) return kubernetes.process_object(cm) @@ -560,7 +562,7 @@ def deploy_components(components_map, components): _create_namespace(CONF.configs) _create_globals_configmap(CONF.configs) - start_script_cm = _create_start_script_configmap() + start_script_cm = create_start_script_configmap() # Create cm with jinja config templates shared across all repositories. templates_files = utils.get_repositories_exports() diff --git a/fuel_ccp/exceptions.py b/fuel_ccp/exceptions.py new file mode 100644 index 00000000..0e573506 --- /dev/null +++ b/fuel_ccp/exceptions.py @@ -0,0 +1,2 @@ +class NotFoundException(Exception): + pass diff --git a/fuel_ccp/kubernetes.py b/fuel_ccp/kubernetes.py index 90995e56..df029830 100644 --- a/fuel_ccp/kubernetes.py +++ b/fuel_ccp/kubernetes.py @@ -157,11 +157,14 @@ def list_cluster_pods(service=None): selector=str(selector)) -def list_cluster_jobs(): +def list_cluster_jobs(selector=None): + ccp_selector = "ccp=true" + if selector: + ccp_selector += "," + selector client = get_client() return pykube.Job.objects(client).filter( namespace=CONF.kubernetes.namespace, - selector="ccp=true") + selector=ccp_selector) def list_cluster_services(): diff --git a/fuel_ccp/templates.py b/fuel_ccp/templates.py index edd2a320..4165bb5b 100644 --- a/fuel_ccp/templates.py +++ b/fuel_ccp/templates.py @@ -17,7 +17,7 @@ ENTRYPOINT_PATH = "/opt/ccp_start_script/bin/start_script.py" PYTHON_PATH = "/usr/bin/python" -def _get_start_cmd(role_name): +def get_start_cmd(role_name): return ["dumb-init", PYTHON_PATH, ENTRYPOINT_PATH, "provision", role_name] @@ -139,7 +139,7 @@ def serialize_daemon_container_spec(container): "name": container["name"], "image": images.image_spec(container["image"]), "imagePullPolicy": CONF.kubernetes.image_pull_policy, - "command": _get_start_cmd(container["name"]), + "command": get_start_cmd(container["name"]), "volumeMounts": serialize_volume_mounts(container), "readinessProbe": { "exec": { @@ -168,7 +168,7 @@ def serialize_job_container_spec(container, job): "name": job["name"], "image": images.image_spec(container["image"]), "imagePullPolicy": CONF.kubernetes.image_pull_policy, - "command": _get_start_cmd(job["name"]), + "command": get_start_cmd(job["name"]), "volumeMounts": serialize_volume_mounts(container, job), "env": serialize_env_variables(container) } diff --git a/setup.cfg b/setup.cfg index e035b296..fd662e13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,10 @@ packages = console_scripts = ccp = fuel_ccp.cli:main ccp.cli = + action_list = fuel_ccp.cli:ActionList + action_run = fuel_ccp.cli:ActionRun + action_show = fuel_ccp.cli:ActionShow + action_status = fuel_ccp.cli:ActionStatus build = fuel_ccp.cli:Build cleanup = fuel_ccp.cli:Cleanup deploy = fuel_ccp.cli:Deploy