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:
parent
38ca18253e
commit
3179511633
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
class NotFoundException(Exception):
|
||||
pass
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue