CLI: implement service commands
- list - show - run - kill - scale - update - snapshot partial blueprint per-service-cli Change-Id: I7b3d7bf9fdb5d9a88bc6ef9a93500a36c506f330
This commit is contained in:
parent
ba4f1082a4
commit
4dfab4c7f6
|
@ -13,51 +13,122 @@
|
|||
from cliff import command
|
||||
from cliff import lister
|
||||
from cliff import show
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from kolla_mesos.common import cli_utils
|
||||
from kolla_mesos import service
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Run(command.Command):
|
||||
"""Run a service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Run, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('sending greeting')
|
||||
LOG.debug('debugging')
|
||||
self.app.stdout.write('hi!\n')
|
||||
service.run_service(parsed_args.service,
|
||||
CONF.service_dir)
|
||||
|
||||
|
||||
class Kill(command.Command):
|
||||
"""Kill a service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Kill, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('sending greeting')
|
||||
LOG.debug('debugging')
|
||||
LOG.stdout.write('hi!\n')
|
||||
service.kill_service(parsed_args.service)
|
||||
|
||||
|
||||
class Show(show.ShowOne):
|
||||
"""Show the live status of the task or service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Show, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('sending greeting')
|
||||
LOG.debug('debugging')
|
||||
self.app.stdout.write('hi!\n')
|
||||
data = service.get_service(parsed_args.service)
|
||||
return cli_utils.dict2columns(data, id_col='service')
|
||||
|
||||
|
||||
class List(lister.Lister):
|
||||
"""List all deployed services for this deployment_id."""
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('sending greeting')
|
||||
LOG.debug('debugging')
|
||||
self.app.stdout.write('hi!\n')
|
||||
apps = service.list_services()
|
||||
values = []
|
||||
cols = ('service', 'type', 'instances', 'tasksUnhealthy',
|
||||
'tasksHealthy', 'tasksRunning', 'tasksStaged', 'version')
|
||||
for app in apps:
|
||||
values.append([app[field] for field in cols])
|
||||
return (cols, values)
|
||||
|
||||
|
||||
class Scale(command.Command):
|
||||
"""Scale the service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Scale, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
parser.add_argument('instances')
|
||||
parser.add_argument('--force', action='store_true',
|
||||
default=False)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
service.scale_service(parsed_args.service,
|
||||
parsed_args.instances,
|
||||
parsed_args.force)
|
||||
|
||||
|
||||
class Log(command.Command):
|
||||
"""Dump the logs for this task or service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Show, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('sending greeting')
|
||||
LOG.debug('debugging')
|
||||
LOG.stdout.write('hi!\n')
|
||||
self.app.stdout.write(service.get_service_logs(parsed_args.service))
|
||||
|
||||
|
||||
class Snapshot(command.Command):
|
||||
"""Snapshot the service configuration and deployment file.
|
||||
|
||||
This will produce a tarball that can be later used with
|
||||
'kolla-mesos update <service> --snapshot <file>'
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Snapshot, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
parser.add_argument('output_dir')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
service.snapshot_service(parsed_args.service,
|
||||
parsed_args.output_dir)
|
||||
|
||||
|
||||
class Update(command.Command):
|
||||
"""Update the service configuration and deployment file."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Update, self).get_parser(prog_name)
|
||||
parser.add_argument('service')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
service.update_service(parsed_args.service, CONF.service_dir)
|
||||
|
|
|
@ -28,3 +28,20 @@ def show(column_names, data, align='c'):
|
|||
for column_name, data_row in six.moves.zip(column_names, data):
|
||||
table.add_row((column_name, data_row,))
|
||||
print(table.get_string())
|
||||
|
||||
|
||||
def dict2columns(data, id_col=None):
|
||||
"""Convert a dict-based object to two-column output.
|
||||
|
||||
Also make sure the id field is at top.
|
||||
"""
|
||||
if not data:
|
||||
return ({}, {})
|
||||
else:
|
||||
if id_col is not None and id_col in data:
|
||||
keys = [id_col]
|
||||
[keys.append(key) for key in sorted(data) if key != id_col]
|
||||
items = [(key, data[key]) for key in keys]
|
||||
else:
|
||||
items = sorted(data.items())
|
||||
return zip(*items)
|
||||
|
|
|
@ -33,6 +33,16 @@ def jinja_render(fullpath, global_config, extra=None):
|
|||
return myenv.get_template(os.path.basename(fullpath)).render(variables)
|
||||
|
||||
|
||||
def jinja_render_str(content, global_config, name='dafault_name', extra=None):
|
||||
variables = global_config
|
||||
if extra:
|
||||
variables.update(extra)
|
||||
|
||||
myenv = jinja2.Environment(loader=jinja2.DictLoader({name: content}))
|
||||
myenv.filters['bool'] = yaml_utils.str_to_bool
|
||||
return myenv.get_template(name).render(variables)
|
||||
|
||||
|
||||
def jinja_find_required_variables(fullpath):
|
||||
myenv = jinja2.Environment(loader=jinja2.FileSystemLoader(
|
||||
os.path.dirname(fullpath)))
|
||||
|
|
|
@ -30,7 +30,7 @@ CONF.import_group('kolla', 'kolla_mesos.config.kolla')
|
|||
CONF.import_group('kolla', 'kolla_mesos.config.zookeeper')
|
||||
|
||||
|
||||
def write_variables_zookeeper(zk, variables, base_node=None):
|
||||
def write_variables_zookeeper(zk, variables, base_node=None, overwrite=True):
|
||||
if base_node is None:
|
||||
base_node = os.path.join('kolla', CONF.kolla.deployment_id)
|
||||
filter_out = ['groups', 'hostvars', 'kolla_config',
|
||||
|
@ -44,6 +44,10 @@ def write_variables_zookeeper(zk, variables, base_node=None):
|
|||
if isinstance(variables[var], dict):
|
||||
var_value = json.dumps(variables[var])
|
||||
var_path = os.path.join(base_node, 'variables', var)
|
||||
if not overwrite and zk.exists(var_path):
|
||||
LOG.debug('NOT Updating "%s" node in zookeeper(overwrite=False).',
|
||||
var_path)
|
||||
return
|
||||
zk.ensure_path(var_path)
|
||||
zk.set(var_path, "" if var_value is None else var_value)
|
||||
LOG.debug('Updated "%s" node in zookeeper.' % var_path)
|
||||
|
@ -61,7 +65,8 @@ def get_start_config(config_dir, jinja_vars):
|
|||
return kolla_config
|
||||
|
||||
|
||||
def write_common_config_to_zookeeper(config_dir, zk, jinja_vars):
|
||||
def write_common_config_to_zookeeper(config_dir, zk, jinja_vars,
|
||||
overwrite=True):
|
||||
# 1. At first write global tools to ZK. FIXME: Make it a common profile
|
||||
conf_path = os.path.join(config_dir, 'common',
|
||||
'common_config.yml.j2')
|
||||
|
@ -69,6 +74,11 @@ def write_common_config_to_zookeeper(config_dir, zk, jinja_vars):
|
|||
common_node = os.path.join('kolla', 'common')
|
||||
for script in common_cfg:
|
||||
script_node = os.path.join(common_node, script)
|
||||
if not overwrite and zk.exists(script_node):
|
||||
LOG.debug('NOT Updating "%s" node in zookeeper(overwrite=False).',
|
||||
script_node)
|
||||
continue
|
||||
|
||||
zk.ensure_path(script_node)
|
||||
source_path = common_cfg[script]['source']
|
||||
src_file = source_path
|
||||
|
|
|
@ -43,3 +43,9 @@ class KollaNotFoundException(KollaException):
|
|||
def __init__(self, message, entity='file'):
|
||||
super(KollaNotFoundException, self).__init__(
|
||||
'The %s "%s" was not found' % (entity, message))
|
||||
|
||||
|
||||
class KollaNotSupportedException(KollaException):
|
||||
def __init__(self, operation='update', entity='chronos'):
|
||||
super(KollaNotFoundException, self).__init__(
|
||||
'Operation "%s" is not supported by "%s"' % (operation, entity))
|
||||
|
|
|
@ -13,20 +13,29 @@
|
|||
import json
|
||||
import os.path
|
||||
|
||||
from kazoo import exceptions
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from six.moves import configparser
|
||||
from six.moves import cStringIO
|
||||
import yaml
|
||||
|
||||
from kolla_mesos import chronos
|
||||
from kolla_mesos.common import file_utils
|
||||
from kolla_mesos.common import jinja_utils
|
||||
from kolla_mesos.common import utils
|
||||
from kolla_mesos.common import zk_utils
|
||||
from kolla_mesos import configuration as config
|
||||
from kolla_mesos import exception
|
||||
from kolla_mesos import marathon
|
||||
from kolla_mesos import service_definition
|
||||
|
||||
LOG = logging.getLogger()
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('kolla', 'kolla_mesos.config.kolla')
|
||||
CONF.import_group('zookeeper', 'kolla_mesos.config.zookeeper')
|
||||
CONF.import_group('marathon', 'kolla_mesos.config.marathon')
|
||||
CONF.import_group('chronos', 'kolla_mesos.config.chronos')
|
||||
|
||||
|
||||
class File(object):
|
||||
|
@ -84,6 +93,41 @@ class Runner(object):
|
|||
self._enabled = self._conf.get('enabled', True)
|
||||
if not self._enabled:
|
||||
LOG.warn('Service %s disabled', self._conf['name'])
|
||||
self.app_file = None
|
||||
self.app_def = None
|
||||
|
||||
def __new__(cls, conf):
|
||||
"""Create a new Runner of the appropriate class for its type."""
|
||||
if cls != Runner:
|
||||
# Call is already for a subclass, so pass it through
|
||||
RunnerClass = cls
|
||||
else:
|
||||
if 'service' in conf and 'task' not in conf:
|
||||
RunnerClass = MarathonApp
|
||||
else:
|
||||
RunnerClass = ChronosTask
|
||||
return super(Runner, cls).__new__(RunnerClass)
|
||||
|
||||
@classmethod
|
||||
def load_from_zk(cls, zk, service_name):
|
||||
variables = _load_variables_from_zk(zk)
|
||||
base_node = os.path.join('kolla', CONF.kolla.deployment_id)
|
||||
if '/' not in service_name:
|
||||
service_name = service_name.replace('-', '/')
|
||||
dest_node = os.path.join(base_node, service_name)
|
||||
try:
|
||||
conf_raw, _st = zk.get(dest_node)
|
||||
except Exception as te:
|
||||
LOG.error('%s -> %s' % (dest_node, te))
|
||||
raise exception.KollaNotFoundException(
|
||||
service_name, entity='running service definition')
|
||||
return Runner(yaml.load(
|
||||
jinja_utils.jinja_render_str(conf_raw, variables)))
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, service_file, variables):
|
||||
return Runner(yaml.load(
|
||||
jinja_utils.jinja_render(service_file, variables)))
|
||||
|
||||
def _list_commands(self):
|
||||
if 'service' in self._conf:
|
||||
|
@ -108,7 +152,8 @@ class Runner(object):
|
|||
def _apply_service_def(self, app_def):
|
||||
"""Apply the specifics from the service definition."""
|
||||
|
||||
def generate_deployment_files(self, kolla_config, jinja_vars, temp_dir):
|
||||
def generate_deployment_files(self, kolla_config, jinja_vars,
|
||||
temp_dir=None):
|
||||
if not self._enabled:
|
||||
return
|
||||
_, proj, service = self._conf['name'].split('/')
|
||||
|
@ -125,20 +170,106 @@ class Runner(object):
|
|||
'default.%s.j2' % self.type_name)
|
||||
content = jinja_utils.jinja_render(app_file, jinja_vars,
|
||||
extra=values)
|
||||
app_def = yaml.load(content)
|
||||
self._apply_service_def(app_def)
|
||||
dest_file = os.path.join(temp_dir, proj,
|
||||
'%s.%s' % (service, self.type_name))
|
||||
file_utils.mkdir_p(os.path.dirname(dest_file))
|
||||
LOG.info(dest_file)
|
||||
with open(dest_file, 'w') as f:
|
||||
f.write(json.dumps(app_def, indent=2))
|
||||
self.app_def = yaml.load(content)
|
||||
self._apply_service_def(self.app_def)
|
||||
if temp_dir is not None:
|
||||
self.app_file = os.path.join(temp_dir, proj,
|
||||
'%s.%s' % (service, self.type_name))
|
||||
file_utils.mkdir_p(os.path.dirname(self.app_file))
|
||||
LOG.info(self.app_file)
|
||||
with open(self.app_file, 'w') as f:
|
||||
f.write(json.dumps(self.app_def, indent=2))
|
||||
|
||||
def get_live_deployment_file(self):
|
||||
"""Get the current version of the deployment from Marathon."""
|
||||
raise exception.KollaNotSupportedException(
|
||||
operation='get_live_deployment_file', entity=self.type_name)
|
||||
|
||||
def run(self):
|
||||
raise exception.KollaNotSupportedException(operation='run',
|
||||
entity=self.type_name)
|
||||
|
||||
def update(self):
|
||||
raise exception.KollaNotSupportedException(operation='update',
|
||||
entity=self.type_name)
|
||||
|
||||
def scale(self, instances, force=False):
|
||||
raise exception.KollaNotSupportedException(operation='scale',
|
||||
entity=self.type_name)
|
||||
|
||||
def snapshot(self, zk, output_dir, variables):
|
||||
"""Produce the required files to revert the service.
|
||||
|
||||
The files should have the same basic structure as the original
|
||||
service definition:
|
||||
<output_dir>/<project>/<service>.yml.j2
|
||||
<output_dir>/<project>/templates/*
|
||||
<output_dir>/<project>/<service>.{marathon|chronos}
|
||||
<output_dir>/variables.yml
|
||||
"""
|
||||
|
||||
service_name = self._conf['name'].split('/')[-1]
|
||||
proj = self._conf['name'].split('/')[-2]
|
||||
proj_dir = os.path.join(output_dir, proj)
|
||||
file_utils.mkdir_p(proj_dir)
|
||||
|
||||
# variables
|
||||
# Note: if this exists, then the variables will be merged.
|
||||
var_path = os.path.join(output_dir, 'variables.yml')
|
||||
if os.path.exists(var_path):
|
||||
with open(var_path) as vfp:
|
||||
global_vars = yaml.load(vfp)
|
||||
LOG.info('global_vars: %s', yaml.dump(global_vars))
|
||||
global_vars.update(variables)
|
||||
else:
|
||||
global_vars = variables
|
||||
|
||||
with open(var_path, 'w') as vfp:
|
||||
vfp.write(yaml.dump(global_vars, default_flow_style=False))
|
||||
|
||||
# service definition and files
|
||||
with open(os.path.join(proj_dir, '%s.yml.j2' % service_name),
|
||||
'w') as sfp:
|
||||
sfp.write(json.dumps(self._conf))
|
||||
|
||||
# get and write the files
|
||||
files_node = os.path.join('kolla', CONF.kolla.deployment_id,
|
||||
self._conf['name'], 'files')
|
||||
if zk.exists(files_node):
|
||||
file_utils.mkdir_p(os.path.join(proj_dir, 'templates'))
|
||||
try:
|
||||
files = zk.get_children(files_node)
|
||||
except exceptions.NoNodeError:
|
||||
files = []
|
||||
for fn in files:
|
||||
with open(os.path.join(proj_dir, 'templates', fn), 'w') as fp:
|
||||
content, _st = zk.get(os.path.join(files_node, fn))
|
||||
fp.write(content)
|
||||
|
||||
# deployment file
|
||||
self.app_file = os.path.join(proj_dir, '%s.%s' % (service_name,
|
||||
self.type_name))
|
||||
self.app_def = self.get_live_deployment_file()
|
||||
with open(self.app_file, 'w') as dfp:
|
||||
dfp.write(json.dumps(self.app_def, indent=2))
|
||||
|
||||
def kill(self, zk=None):
|
||||
if zk:
|
||||
dest_node = os.path.join('kolla', CONF.kolla.deployment_id,
|
||||
self._conf['name'])
|
||||
zk.delete(dest_node, recursive=True)
|
||||
|
||||
|
||||
class MarathonApp(Runner):
|
||||
def __init__(self, conf):
|
||||
super(MarathonApp, self).__init__(conf)
|
||||
self.type_name = 'marathon'
|
||||
self._marathon_client = None
|
||||
|
||||
def _client(self):
|
||||
if self._marathon_client is None:
|
||||
self._marathon_client = marathon.Client()
|
||||
return self._marathon_client
|
||||
|
||||
def _apply_service_def(self, app_def):
|
||||
"""Apply the specifics from the service definition."""
|
||||
|
@ -170,11 +301,66 @@ class MarathonApp(Runner):
|
|||
app_def[opt] = utils.dict_update(app_def.get(opt),
|
||||
self._conf['service'][opt])
|
||||
|
||||
def run(self):
|
||||
self._client().add_app(self.app_def)
|
||||
LOG.info('Marathon app "%s" is started' %
|
||||
self.app_def['id'])
|
||||
|
||||
def update(self):
|
||||
self._client().update_job(self._get_app_id(), self.app_def)
|
||||
|
||||
def _get_app_id(self):
|
||||
return '/%s/%s' % (CONF.kolla.deployment_id, self._conf['name'])
|
||||
|
||||
def kill(self, zk=None):
|
||||
self._client().remove_app(self._get_app_id())
|
||||
super(MarathonApp, self).kill(zk)
|
||||
|
||||
def get_live_deployment_file(self):
|
||||
"""Get the current version of the deployment from Marathon."""
|
||||
return self._client().get_app(self._get_app_id())
|
||||
|
||||
def scale(self, instances, force=False):
|
||||
self._client().scale_app(self._get_app_id(),
|
||||
instances, force=force)
|
||||
|
||||
@staticmethod
|
||||
def _get_common_fields(app):
|
||||
dep_id = '/%s/' % CONF.kolla.deployment_id
|
||||
marathon_fields = ('instances', 'tasksUnhealthy', 'tasksHealthy',
|
||||
'tasksRunning', 'tasksStaged',
|
||||
'version', 'healthChecks')
|
||||
mini_app = dict((field, app[field]) for field in marathon_fields)
|
||||
mini_app.update({'service': app['id'].replace(dep_id, ''),
|
||||
'type': 'marathon'})
|
||||
return mini_app
|
||||
|
||||
def get_state(self, version=None):
|
||||
app = self._client().get_app(self._get_app_id(), version=version)
|
||||
mini_app = MarathonApp._get_common_fields(app)
|
||||
mini_app['image'] = app['container']['docker']['image']
|
||||
mini_app['privileged'] = app['container']['docker']['privileged']
|
||||
return mini_app
|
||||
|
||||
@staticmethod
|
||||
def list_all():
|
||||
dep_id = '/%s/' % CONF.kolla.deployment_id
|
||||
marathon_client = marathon.Client()
|
||||
apps = marathon_client.get_apps()
|
||||
return [MarathonApp._get_common_fields(app) for app in apps
|
||||
if app['id'].startswith(dep_id)]
|
||||
|
||||
|
||||
class ChronosTask(Runner):
|
||||
def __init__(self, conf):
|
||||
super(ChronosTask, self).__init__(conf)
|
||||
self.type_name = 'chronos'
|
||||
self._chronos_client = None
|
||||
|
||||
def _client(self):
|
||||
if self._chronos_client is None:
|
||||
self._chronos_client = chronos.Client()
|
||||
return self._chronos_client
|
||||
|
||||
def _apply_service_def(self, task_def):
|
||||
"""Apply the specifics from the service definition."""
|
||||
|
@ -200,3 +386,187 @@ class ChronosTask(Runner):
|
|||
cenv['value'] = value
|
||||
return
|
||||
chronos_env.append({"name": key, "value": value})
|
||||
|
||||
def run(self):
|
||||
self._client().add_job(self.app_def)
|
||||
LOG.info('Chronos job "%s" is started' %
|
||||
self.app_def['name'])
|
||||
|
||||
def _get_job_name(self):
|
||||
return '%s-%s' % (CONF.kolla.deployment_id,
|
||||
self._conf['name'].replace('/', '-'))
|
||||
|
||||
def update(self):
|
||||
self._client().update_job(self._get_job_name(), self.app_def)
|
||||
|
||||
def kill(self, zk=None):
|
||||
self._client().remove_job(self._get_job_name())
|
||||
super(ChronosTask, self).kill(zk)
|
||||
|
||||
@staticmethod
|
||||
def _job_to_public_format(job):
|
||||
dep_id = '%s-' % CONF.kolla.deployment_id
|
||||
mini_job = {'service': job['name'].replace(dep_id, ''),
|
||||
'type': 'chronos'}
|
||||
mini_job['tasksHealthy'] = job['successCount']
|
||||
mini_job['tasksUnhealthy'] = job['errorCount']
|
||||
mini_job['instances'] = 0 if job['disabled'] else 1
|
||||
mini_job['version'] = job['lastSuccess']
|
||||
mini_job['tasksStaged'] = 'N/A'
|
||||
mini_job['tasksRunning'] = 'N/A'
|
||||
return mini_job
|
||||
|
||||
def get_state(self, version=None):
|
||||
job = self._client().get_job(self._get_job_name())
|
||||
mini_job = ChronosTask._job_to_public_format(job)
|
||||
mini_job['image'] = job['container']['image']
|
||||
return mini_job
|
||||
|
||||
@staticmethod
|
||||
def list_all():
|
||||
client = chronos.Client()
|
||||
dep_id = '%s-' % CONF.kolla.deployment_id
|
||||
jobs = []
|
||||
for job in client.get_jobs():
|
||||
if job['name'].startswith(dep_id):
|
||||
jobs.append(ChronosTask._job_to_public_format(job))
|
||||
return jobs
|
||||
|
||||
|
||||
def _load_variables_from_zk(zk):
|
||||
path = os.path.join('/kolla', CONF.kolla.deployment_id, 'variables')
|
||||
variables = {}
|
||||
try:
|
||||
var_names = zk.get_children(path)
|
||||
except exceptions.NoNodeError:
|
||||
var_names = []
|
||||
for var in var_names:
|
||||
variables[str(var)], _stat = zk.get(os.path.join(path, var))
|
||||
# Add deployment_id
|
||||
variables.update({'deployment_id': CONF.kolla.deployment_id})
|
||||
# override node_config_directory to empty
|
||||
variables.update({'node_config_directory': ''})
|
||||
return variables
|
||||
|
||||
|
||||
def _load_variables_from_file(service_dir, project_name):
|
||||
config_dir = os.path.join(service_dir, '..', 'config')
|
||||
with open(file_utils.find_config_file('passwords.yml'), 'r') as gf:
|
||||
global_vars = yaml.load(gf)
|
||||
with open(file_utils.find_config_file('globals.yml'), 'r') as gf:
|
||||
global_vars.update(yaml.load(gf))
|
||||
# all.yml file uses some its variables to template itself by jinja2,
|
||||
# so its raw content is used to template the file
|
||||
all_yml_name = os.path.join(config_dir, 'all.yml')
|
||||
with open(all_yml_name) as af:
|
||||
raw_vars = yaml.load(af)
|
||||
raw_vars.update(global_vars)
|
||||
jvars = yaml.load(jinja_utils.jinja_render(all_yml_name, raw_vars))
|
||||
jvars.update(global_vars)
|
||||
|
||||
proj_yml_name = os.path.join(config_dir, project_name,
|
||||
'defaults', 'main.yml')
|
||||
if os.path.exists(proj_yml_name):
|
||||
proj_vars = yaml.load(jinja_utils.jinja_render(proj_yml_name,
|
||||
jvars))
|
||||
jvars.update(proj_vars)
|
||||
else:
|
||||
LOG.warning('Path missing %s' % proj_yml_name)
|
||||
# Add deployment_id
|
||||
jvars.update({'deployment_id': CONF.kolla.deployment_id})
|
||||
# override node_config_directory to empty
|
||||
jvars.update({'node_config_directory': ''})
|
||||
config.apply_deployment_vars(jvars)
|
||||
return jvars
|
||||
|
||||
|
||||
def _load_variables_from_snapshot(service_dir):
|
||||
var_path = os.path.join(service_dir, 'variables.yml')
|
||||
with open(var_path) as vfp:
|
||||
return yaml.load(vfp)
|
||||
|
||||
|
||||
def _build_runner(service_name, service_dir, variables=None):
|
||||
config_dir = os.path.join(service_dir, '..', 'config')
|
||||
base_node = os.path.join('kolla', CONF.kolla.deployment_id)
|
||||
filename = service_definition.find_service_file(service_name,
|
||||
service_dir)
|
||||
proj_name = filename.split('/')[-2]
|
||||
proj_yml_name = os.path.join(config_dir, proj_name,
|
||||
'defaults', 'main.yml')
|
||||
|
||||
# is this a snapshot or from original src?
|
||||
var_path = os.path.join(service_dir, 'variables.yml')
|
||||
is_snapshot = (os.path.exists(var_path) and
|
||||
not os.path.exists(proj_yml_name))
|
||||
|
||||
if variables is None:
|
||||
if not is_snapshot:
|
||||
variables = _load_variables_from_file(service_dir, proj_name)
|
||||
else:
|
||||
variables = _load_variables_from_snapshot(service_dir)
|
||||
|
||||
# 1. validate the definition with the given variables
|
||||
service_definition.validate(service_name, service_dir, variables)
|
||||
runner = Runner.load_from_file(filename, variables)
|
||||
with zk_utils.connection() as zk:
|
||||
# 2. write variables to zk (globally)
|
||||
config.write_variables_zookeeper(zk, variables,
|
||||
overwrite=not is_snapshot)
|
||||
# 3. write common config and start script
|
||||
config.write_common_config_to_zookeeper(config_dir, zk, variables,
|
||||
overwrite=not is_snapshot)
|
||||
|
||||
# 4. write files/config to zk
|
||||
runner.write_to_zookeeper(zk, base_node)
|
||||
|
||||
# 5. generate the deployment files
|
||||
kolla_config = config.get_start_config(config_dir, variables)
|
||||
runner.generate_deployment_files(kolla_config, variables)
|
||||
return runner
|
||||
|
||||
|
||||
# Public API below
|
||||
##################
|
||||
|
||||
def run_service(service_name, service_dir, variables=None):
|
||||
runner = _build_runner(service_name, service_dir, variables=variables)
|
||||
runner.run()
|
||||
|
||||
|
||||
def update_service(service_name, service_dir, variables=None):
|
||||
runner = _build_runner(service_name, service_dir, variables=variables)
|
||||
runner.update()
|
||||
|
||||
|
||||
def kill_service(service_name):
|
||||
with zk_utils.connection() as zk:
|
||||
runner = Runner.load_from_zk(zk, service_name)
|
||||
runner.kill(zk)
|
||||
|
||||
|
||||
def snapshot_service(service_name, output_dir):
|
||||
with zk_utils.connection() as zk:
|
||||
runner = Runner.load_from_zk(zk, service_name)
|
||||
variables = _load_variables_from_zk(zk)
|
||||
runner.snapshot(zk, output_dir, variables)
|
||||
|
||||
|
||||
def get_service(service_name, version=None):
|
||||
with zk_utils.connection() as zk:
|
||||
runner = Runner.load_from_zk(zk, service_name)
|
||||
return runner.get_state(version)
|
||||
|
||||
|
||||
def scale_service(service_name, instances, force=False):
|
||||
with zk_utils.connection() as zk:
|
||||
runner = Runner.load_from_zk(zk, service_name)
|
||||
return runner.scale(instances, force)
|
||||
|
||||
|
||||
def list_services():
|
||||
return ChronosTask.list_all() + MarathonApp.list_all()
|
||||
|
||||
|
||||
def get_service_logs(service_name):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from kolla_mesos.common import cli_utils
|
||||
from kolla_mesos.tests import base
|
||||
|
||||
|
||||
class DictToColTest(base.BaseTestCase):
|
||||
scenarios = [
|
||||
('normal',
|
||||
dict(id_col=None,
|
||||
input={'a': 'v1', 'b': 'v2'},
|
||||
expect=[('a', 'b'), ('v1', 'v2')])),
|
||||
('special',
|
||||
dict(id_col='b',
|
||||
input={'a': 'v1', 'b': 'v2'},
|
||||
expect=[('b', 'a'), ('v2', 'v1')])),
|
||||
]
|
||||
|
||||
def test_to_col(self):
|
||||
self.assertEqual(self.expect,
|
||||
cli_utils.dict2columns(self.input,
|
||||
id_col=self.id_col))
|
|
@ -0,0 +1,140 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import os.path
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from zake import fake_client
|
||||
|
||||
from kolla_mesos import service
|
||||
from kolla_mesos.tests import base
|
||||
|
||||
|
||||
class TestAPI(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAPI, self).setUp()
|
||||
self.service_dir = os.path.join(self.project_dir, 'services')
|
||||
self.client = fake_client.FakeClient()
|
||||
self.client.start()
|
||||
self.addCleanup(self.client.stop)
|
||||
self.addCleanup(self.client.close)
|
||||
cfg.CONF.set_override('deployment_id', 'did', group='kolla')
|
||||
|
||||
@mock.patch.object(service.config, 'apply_deployment_vars')
|
||||
@mock.patch.object(service.MarathonApp, 'run')
|
||||
def test_run_marathon(self, m_run, m_apply):
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.run_service('openstack/keystone/keystone-api',
|
||||
self.service_dir)
|
||||
m_run.assert_called_once_with()
|
||||
|
||||
# assert that we have the basics in zk
|
||||
exp_nodes = ['/kolla/common/kolla_mesos_start.py',
|
||||
'/kolla/did/variables/keystone_admin_port',
|
||||
'/kolla/did/openstack/keystone/keystone-api']
|
||||
for node in exp_nodes:
|
||||
self.assertTrue(self.client.exists(node), node)
|
||||
|
||||
@mock.patch.object(service.config, 'apply_deployment_vars')
|
||||
@mock.patch.object(service.ChronosTask, 'run')
|
||||
def test_run_chronos(self, m_run, m_apply):
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.run_service('openstack/keystone/keystone-init',
|
||||
self.service_dir)
|
||||
m_run.assert_called_once_with()
|
||||
|
||||
# assert that we have the basics in zk
|
||||
exp_nodes = ['/kolla/common/kolla_mesos_start.py',
|
||||
'/kolla/did/variables/keystone_admin_port',
|
||||
'/kolla/did/openstack/keystone/keystone_ansible_tasks']
|
||||
for node in exp_nodes:
|
||||
self.assertTrue(self.client.exists(node), node)
|
||||
|
||||
@mock.patch.object(service.MarathonApp, 'kill')
|
||||
def test_kill(self, m_kill):
|
||||
self.client.create('/kolla/did/openstack/nova/nova-api',
|
||||
json.dumps({'name': 'openstack/nova/nova-api',
|
||||
'service': {}}),
|
||||
makepath=True)
|
||||
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.kill_service('openstack/nova/nova-api')
|
||||
m_kill.assert_called_once_with(self.client)
|
||||
|
||||
@mock.patch.object(service.ChronosTask, 'get_state')
|
||||
@mock.patch.object(service.MarathonApp, 'get_state')
|
||||
def test_get_marathon(self, m_get_state, c_get_state):
|
||||
self.client.create('/kolla/did/openstack/nova/nova-api',
|
||||
json.dumps({'name': 'openstack/nova/nova-api',
|
||||
'service': {}}),
|
||||
makepath=True)
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.get_service('openstack/nova/nova-api')
|
||||
self.assertEqual([], c_get_state.mock_calls)
|
||||
m_get_state.assert_called_once_with(None)
|
||||
|
||||
@mock.patch.object(service.ChronosTask, 'get_state')
|
||||
@mock.patch.object(service.MarathonApp, 'get_state')
|
||||
def test_get_chronos(self, m_get_state, c_get_state):
|
||||
self.client.create('/kolla/did/openstack/nova/nova_init',
|
||||
json.dumps({'name': 'openstack/nova/nova_init',
|
||||
'task': {}}),
|
||||
makepath=True)
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.get_service('openstack-nova-nova_init')
|
||||
self.assertEqual([], m_get_state.mock_calls)
|
||||
c_get_state.assert_called_once_with(None)
|
||||
|
||||
@mock.patch.object(service.MarathonApp, 'scale')
|
||||
def test_scale_marathon(self, m_scale):
|
||||
self.client.create('/kolla/did/openstack/nova/nova-api',
|
||||
json.dumps({'name': 'openstack/nova/nova-api',
|
||||
'service': {}}),
|
||||
makepath=True)
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.scale_service('openstack/nova/nova-api', 2, force=False)
|
||||
m_scale.assert_called_once_with(2, False)
|
||||
|
||||
@mock.patch.object(service.config, 'apply_deployment_vars')
|
||||
@mock.patch.object(service.MarathonApp, 'update')
|
||||
def test_update_marathon(self, m_update, m_apply):
|
||||
self.client.create('/kolla/did/openstack/nova/nova-api',
|
||||
json.dumps({'name': 'openstack/nova/nova-api',
|
||||
'service': {}}),
|
||||
makepath=True)
|
||||
with mock.patch.object(service.zk_utils,
|
||||
'connection') as m_zk_c:
|
||||
m_zk_c.return_value.__enter__.return_value = self.client
|
||||
service.update_service('openstack/nova/nova-api', self.service_dir)
|
||||
m_update.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(service.ChronosTask, 'list_all')
|
||||
@mock.patch.object(service.MarathonApp, 'list_all')
|
||||
def test_list(self, m_list, c_list):
|
||||
service.list_services()
|
||||
c_list.assert_called_once_with()
|
||||
m_list.assert_called_once_with()
|
|
@ -42,6 +42,9 @@ kolla_mesos.cli =
|
|||
list = kolla_mesos.cli.service:List
|
||||
show = kolla_mesos.cli.service:Show
|
||||
log = kolla_mesos.cli.service:Log
|
||||
scale = kolla_mesos.cli.service:Scale
|
||||
snapshot = kolla_mesos.cli.service:Snapshot
|
||||
update = kolla_mesos.cli.service:Update
|
||||
chronos list = kolla_mesos.cli.chronos:List
|
||||
chronos show = kolla_mesos.cli.chronos:Show
|
||||
config list = kolla_mesos.cli.config:ConfigList
|
||||
|
|
Loading…
Reference in New Issue