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
This commit is contained in:
Sergey Reshetnyak 2017-01-11 21:47:48 +03:00
parent 38ca18253e
commit 3179511633
8 changed files with 339 additions and 13 deletions

235
fuel_ccp/action.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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()

2
fuel_ccp/exceptions.py Normal file
View File

@ -0,0 +1,2 @@
class NotFoundException(Exception):
pass

View File

@ -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():

View File

@ -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)
}

View File

@ -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