CLI: implement deployment commands

These are to replace the current
 kolla-mesos-deploy
 kolla-mesos-cleanup

Make scripts that redirect to the new commands.

partial blueprint per-service-cli
Change-Id: Id730a1480b97e1059a7d04dd066b62156a028d20
This commit is contained in:
Angus Salkeld 2016-03-15 17:10:24 +10:00 committed by Andrey Pavlov
parent d30f449f4c
commit 000b508b53
10 changed files with 108 additions and 461 deletions

View File

@ -0,0 +1,56 @@
# 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 cliff import command
from cliff import show
from oslo_config import cfg
from oslo_log import log
from kolla_mesos import cleanup
from kolla_mesos.common import cli_utils
from kolla_mesos import deployment
from kolla_mesos import service
CONF = cfg.CONF
CONF.import_opt('workers', 'kolla_mesos.config.multiprocessing_cli')
LOG = log.getLogger(__name__)
class Run(command.Command):
"""Run the services in the configured profile."""
def take_action(self, parsed_args):
deployment.run_deployment()
deployment.write_openrc('%s-openrc' % CONF.kolla.deployment_id)
class Kill(command.Command):
"""Kill all the running services."""
def take_action(self, parsed_args):
for serv in service.list_services():
service.kill_service(serv['service'])
class Cleanup(command.Command):
"""Delete all created resources."""
def take_action(self, parsed_args):
cleanup.cleanup()
class Show(show.ShowOne):
"""Show the deployment configuration."""
def take_action(self, parsed_args):
conf_opts = deployment.get_deployment()
return cli_utils.dict2columns(conf_opts, id_col='deployment_id')

View File

@ -1,37 +0,0 @@
#!/usr/bin/env python
# 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 sys
from oslo_config import cfg
from oslo_log import log as logging
from kolla_mesos import cleanup
CONF = cfg.CONF
CONF.import_opt('workers', 'kolla_mesos.config.multiprocessing_cli')
LOG = logging.getLogger()
logging.register_options(CONF)
def main():
CONF(sys.argv[1:], project='kolla-mesos')
logging.setup(CONF, 'kolla-mesos')
cleanup.cleanup()
if __name__ == '__main__':
main()

View File

@ -1,296 +0,0 @@
#!/usr/bin/env python
# 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 datetime
import json
import os
import signal
import sys
import tempfile
import time
from oslo_config import cfg
from oslo_log import log as logging
import retrying
import shutil
import yaml
from kolla_mesos import chronos
from kolla_mesos import cleanup
from kolla_mesos.common import file_utils
from kolla_mesos.common import jinja_utils
from kolla_mesos.common import zk_utils
from kolla_mesos import configuration
from kolla_mesos import deployment
from kolla_mesos import exception
from kolla_mesos import marathon
from kolla_mesos import service
signal.signal(signal.SIGINT, signal.SIG_DFL)
CONF = cfg.CONF
CONF.import_group('kolla', 'kolla_mesos.config.kolla')
CONF.import_group('profiles', 'kolla_mesos.config.profiles')
CONF.import_group('zookeeper', 'kolla_mesos.config.zookeeper')
CONF.import_group('marathon', 'kolla_mesos.config.marathon')
CONF.import_group('chronos', 'kolla_mesos.config.chronos')
CONF.import_opt('update', 'kolla_mesos.config.deploy_cli')
CONF.import_opt('force', 'kolla_mesos.config.deploy_cli')
LOG = logging.getLogger()
logging.register_options(CONF)
class KollaWorker(object):
def __init__(self):
self.base_dir = os.path.abspath(file_utils.find_base_dir())
self.config_dir = os.path.join(self.base_dir, 'config')
LOG.debug("Kolla-Mesos base directory: %s" % self.base_dir)
self.required_vars = {}
self.marathon_client = marathon.Client()
self.chronos_client = chronos.Client()
self.start_time = time.time()
def setup_working_dir(self):
"""Creates a working directory for use while building"""
ts = datetime.datetime.fromtimestamp(
self.start_time
).strftime('%Y-%m-%d_%H-%M-%S_')
self.temp_dir = tempfile.mkdtemp(prefix='kolla-' + ts)
LOG.debug('Created output dir: %s' % self.temp_dir)
def get_projects(self):
projects = set(getattr(CONF.profiles, CONF.kolla.profile))
LOG.debug('Projects are: %s' % projects)
return projects
def get_jinja_vars(self):
# order for per-project variables (each overrides the previous):
# 1. /etc/kolla/globals.yml and passwords.yml
# 2. config/all.yml
# 3. config/<project>/defaults/main.yml
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(self.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)
for proj in self.get_projects():
proj_yml_name = os.path.join(self.config_dir, proj,
'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)
jvars.update({
'deployment_id': self.deployment_id,
'node_config_directory': '',
'timestamp': str(time.time())
})
configuration.apply_deployment_vars(jvars)
configuration.get_marathon_framework(jvars)
return jvars
def gen_deployment_id(self):
if CONF.kolla.deployment_id is None:
LOG.error('You must set "kolla.deployment_id".')
sys.exit(1)
self.deployment_id = CONF.kolla.deployment_id
LOG.info('Deployment ID: %s' % self.deployment_id)
def process_service_config(self, zk, proj, conf_path,
jinja_vars, kolla_config):
conf = yaml.load(jinja_utils.jinja_render(conf_path, jinja_vars))
if 'service' in conf:
runner = service.MarathonApp(conf)
else:
runner = service.ChronosTask(conf)
base_node = os.path.join('kolla', self.deployment_id)
runner.write_to_zookeeper(zk, base_node)
runner.generate_deployment_files(kolla_config, jinja_vars,
self.temp_dir)
def _write_common_config_to_zookeeper(self, zk, jinja_vars):
# 1. At first write global tools to ZK. FIXME: Make it a common profile
conf_path = os.path.join(self.config_dir, 'common',
'common_config.yml.j2')
common_cfg = yaml.load(jinja_utils.jinja_render(conf_path, jinja_vars))
common_node = os.path.join('kolla', 'common')
for script in common_cfg:
script_node = os.path.join(common_node, script)
zk.ensure_path(script_node)
source_path = common_cfg[script]['source']
src_file = source_path
if not source_path.startswith('/'):
src_file = file_utils.find_file(source_path)
with open(src_file) as fp:
content = fp.read()
zk.set(script_node, content)
# 2. Add startup config
start_conf = os.path.join(self.config_dir,
'common/kolla-start-config.json')
# override container_config_directory
cont_conf_dir = 'zk://%s' % (CONF.zookeeper.host)
jinja_vars['container_config_directory'] = cont_conf_dir
jinja_vars['deployment_id'] = self.deployment_id
kolla_config = jinja_utils.jinja_render(start_conf, jinja_vars)
kolla_config = kolla_config.replace('"', '\\"').replace('\n', '')
return kolla_config
def write_config_to_zookeeper(self, zk):
jinja_vars = self.get_jinja_vars()
LOG.debug('Jinja_vars is: %s' % jinja_vars)
self.required_vars = jinja_vars
for var in jinja_vars:
if jinja_vars[var] is None:
LOG.info('jinja_vars[%s] is None' % var)
if 'image' in var:
LOG.debug('%s is "%s"' % (var, jinja_vars[var]))
configuration.write_common_config_to_zookeeper(self.config_dir, zk,
jinja_vars)
kolla_config = configuration.get_start_config(self.config_dir,
jinja_vars)
# Now write services configs
for proj in self.get_projects():
conf_path = os.path.join(self.base_dir, 'services', proj)
LOG.info('Current project is %s' % conf_path)
for root, dirs, names in os.walk(conf_path):
[self.process_service_config(zk, proj,
os.path.join(root, name),
jinja_vars, kolla_config)
for name in names if name.endswith('.j2')]
def cleanup_temp_files(self):
"""Remove temp files"""
shutil.rmtree(self.temp_dir)
LOG.debug('Tmp file %s removed' % self.temp_dir)
def write_to_zookeeper(self):
with zk_utils.connection() as zk:
base_node = os.path.join('kolla', self.deployment_id)
if zk.exists(base_node) and CONF.force:
LOG.info('Deleting "%s" ZK node tree' % base_node)
zk.delete(base_node, recursive=True)
elif zk.exists(base_node) and not CONF.force:
LOG.info('"%s" ZK node tree is already exists. If you'
' want to delete it, use --force' % base_node)
sys.exit(1)
self.write_config_to_zookeeper(zk)
filter_out = ['groups', 'hostvars', 'kolla_config',
'inventory_hostname']
for var in self.required_vars:
if (var in filter_out):
LOG.debug('Var "%s" with value "%s" is filtered out' %
(var, self.required_vars[var]))
continue
var_value = self.required_vars[var]
if isinstance(self.required_vars[var], dict):
var_value = json.dumps(self.required_vars[var])
var_path = os.path.join(base_node, 'variables', var)
zk.ensure_path(var_path)
zk.set(var_path, "" if var_value is None else str(var_value))
LOG.debug('Updated "%s" node in zookeeper.' % var_path)
def _start_marathon_app(self, app_resource):
if CONF.update:
LOG.info('Applications upgrade is not implemented yet!')
else:
try:
return self.marathon_client.add_app(app_resource)
except exception.MarathonRollback as e:
cleanup.cleanup()
self.write_to_zookeeper()
raise e
def _start_chronos_job(self, job_resource):
if CONF.update:
LOG.info('Bootstrap tasks for upgrade are not implemented yet!')
else:
try:
return self.chronos_client.add_job(job_resource)
except exception.ChronosRollback as e:
cleanup.cleanup()
self.write_to_zookeeper()
raise e
@retrying.retry(stop_max_attempt_number=5)
def start(self):
# find all marathon files and run.
# find all cronos files and run.
for root, dirs, names in os.walk(self.temp_dir):
for name in names:
app_path = os.path.join(root, name)
with open(app_path, 'r') as app_file:
app_resource = json.load(app_file)
if 'marathon' in name:
deployment_id = {'KOLLA_DEPLOYMENT_ID': self.deployment_id}
app_resource['env'].update(deployment_id)
self._start_marathon_app(app_resource)
LOG.info('Marathon app "%s" is started' %
app_resource['id'])
else:
deployment_id = {'name': 'KOLLA_DEPLOYMENT_ID',
'value': self.deployment_id}
app_resource['environmentVariables'].append(deployment_id)
self._start_chronos_job(app_resource)
LOG.info('Chronos job "%s" is started' %
app_resource['name'])
def print_summary(self):
LOG.info('=' * 80)
LOG.info('Openstack deployed, check urls below for more info.')
LOG.info('Marathon: %s', CONF.marathon.host)
LOG.info('Mesos: %s', CONF.marathon.host.replace('8080', '5050'))
LOG.info('Chronos: %s', CONF.chronos.host)
def main():
CONF(sys.argv[1:], project='kolla-mesos')
logging.setup(CONF, 'kolla-mesos')
kolla = KollaWorker()
kolla.setup_working_dir()
kolla.gen_deployment_id()
kolla.write_to_zookeeper()
deployment.write_openrc('openrc')
kolla.start()
kolla.print_summary()
# kolla.cleanup_temp_files()
if __name__ == '__main__':
main()

View File

@ -25,10 +25,12 @@ from kolla_mesos.common import utils
VERSION = '1.0'
CONF = cfg.CONF
CONF.import_group('kolla', 'kolla_mesos.config.kolla')
CONF.import_group('profiles', 'kolla_mesos.config.profiles')
CONF.import_group('zookeeper', 'kolla_mesos.config.zookeeper')
CONF.import_group('marathon', 'kolla_mesos.config.marathon')
CONF.import_group('chronos', 'kolla_mesos.config.chronos')
CONF.import_group('mesos', 'kolla_mesos.config.mesos')
CONF.import_opt('workers', 'kolla_mesos.config.multiprocessing_cli')
cli_opts = [
cfg.StrOpt('service-dir',

View File

@ -10,7 +10,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os.path
import os
from oslo_config import cfg
from oslo_log import log
@ -19,8 +19,14 @@ from kolla_mesos.common import file_utils
from kolla_mesos.common import jinja_utils
from kolla_mesos.common import zk_utils
from kolla_mesos import configuration
from kolla_mesos import service
CONF = cfg.CONF
CONF.import_group('kolla', 'kolla_mesos.config.kolla')
CONF.import_group('profiles', 'kolla_mesos.config.profiles')
CONF.import_group('zookeeper', 'kolla_mesos.config.zookeeper')
CONF.import_group('marathon', 'kolla_mesos.config.marathon')
CONF.import_group('chronos', 'kolla_mesos.config.chronos')
LOG = log.getLogger(__name__)
@ -36,3 +42,28 @@ def write_openrc(out_filename):
with open(out_filename, 'w') as f:
f.write(content)
LOG.info('Written OpenStack env to "%s"', out_filename)
def run_deployment():
for proj in set(getattr(CONF.profiles, CONF.kolla.profile)):
for fn in os.listdir(os.path.join(CONF.service_dir, proj)):
if fn.endswith('.j2') and os.path.isfile(fn):
service.run_service(fn, CONF.service_dir)
def get_deployment():
conf_opts = {}
conf_opts['deployment_id'] = CONF.kolla.deployment_id
conf_opts['profile'] = CONF.kolla.profile
conf_opts['service_dir'] = CONF.service_dir
conf_opts['image-name-template'] = '%s/%s-%s-{{ image }}:%s' % (
CONF.kolla.namespace, CONF.kolla.base, CONF.kolla.install_type,
CONF.kolla.tag)
conf_opts['marathon'] = CONF.marathon.host
conf_opts['mesos'] = CONF.marathon.host.replace('8080', '5050')
conf_opts['chronos'] = CONF.chronos.host
conf_opts['zookeeper'] = CONF.zookeeper.host
if False:
# TODO(asalkeld) if horizon is running and ready, get the url.
conf_opts['horizon'] = ''
return conf_opts

View File

@ -13,6 +13,7 @@
import itertools
import json
import os.path
import time
from kazoo import exceptions
from oslo_config import cfg
@ -478,6 +479,8 @@ def _load_variables_from_file(service_dir, project_name):
jvars.update({'deployment_id': CONF.kolla.deployment_id})
# override node_config_directory to empty
jvars.update({'node_config_directory': ''})
# Add timestamp
jvars.update({'timestamp': str(time.time())})
config.apply_deployment_vars(jvars)
config.get_marathon_framework(jvars)
return jvars

View File

@ -1,125 +0,0 @@
# 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 mock
from oslo_config import cfg
import requests_mock
import yaml
from kolla_mesos.cmd import deploy
from kolla_mesos.tests import base
CONF = cfg.CONF
CONF.import_group('kolla', 'kolla_mesos.config.kolla')
CONF.import_group('mesos', 'kolla_mesos.config.mesos')
YAML_SERVICES_CONFIG = """
name: openstack/cinder/cinder-api
container:
image: "cinder-api:a.b.c"
service:
daemon:
dependencies: [cinder-tasks/create_user,
keystone-tasks/running,
rabbitmq/daemon]
command: /usr/bin/cinder-api
files:
cinder.conf.j2:
source: ["config/cinder/templates/cinder.conf.j2",
"/etc/kolla-mesos/config/cinder/cinder-api.conf"]
dest: /etc/cinder/cinder.conf
owner: cinder
perm: "0600"
"""
class TestClient(base.BaseTestCase):
@requests_mock.mock()
def setUp(self, req_mock):
super(TestClient, self).setUp()
CONF.set_override('host', 'http://localhost:4400', group='chronos')
CONF.set_override('timeout', 5, group='chronos')
CONF.set_override('host', 'http://127.0.0.1:8080', group='marathon')
CONF.set_override('host', 'http://127.0.0.1:5050', group='mesos')
req_mock.get('http://127.0.0.1:8080/v2/info', json={
'version': '0.11.0'
})
self.worker = deploy.KollaWorker()
def test_create_worker(self):
self.assertIsInstance(self.worker, deploy.KollaWorker)
@mock.patch('kolla_mesos.cmd.deploy.tempfile')
def test_setup_working_dir(self, mock_tmpfile):
self.worker.setup_working_dir()
parameters = mock_tmpfile.mkdtemp.call_args[1]
self.assertTrue(parameters['prefix'].startswith('kolla'))
def test_gen_deployment_id(self):
CONF.set_override('deployment_id', 'test', group='kolla')
self.worker.gen_deployment_id()
self.assertEqual('test', self.worker.deployment_id)
@mock.patch('kolla_mesos.cmd.deploy.sys')
def test_gen_deployment_id_without_parameters(self, mock_sys):
CONF.set_override('deployment_id', None, group='kolla')
self.worker.gen_deployment_id()
mock_sys.exit.assert_called_once_with(1)
@mock.patch('kolla_mesos.cmd.deploy.yaml')
@mock.patch('kolla_mesos.cmd.deploy.zk_utils')
@mock.patch.object(deploy.KollaWorker,
'_write_common_config_to_zookeeper')
def test_write_to_zookeeper(self, mock_common, mock_utils, mock_yaml):
CONF.set_override('force', True)
self.worker.get_jinja_vars = mock.MagicMock(
return_value={'image': 'test1',
'test2': '',
'controller_nodes': '1',
'compute_nodes': '1'})
mock_yaml.load = mock.MagicMock(
return_value=yaml.load(YAML_SERVICES_CONFIG))
mock_common.return_value = ''
self.worker.setup_working_dir()
self.worker.gen_deployment_id()
self.worker.write_to_zookeeper()
self.assertTrue(len(mock_utils.mock_calls) > 1)
expected_calls = ['set', 'delete', 'ensure_path', 'exists']
for call in mock_utils.mock_calls:
methods = str(call).split('.')
if len(methods) > 3:
function_name = methods[3].split('(')[0]
self.assertIn(function_name, expected_calls)
@mock.patch('kolla_mesos.cmd.deploy.json')
@mock.patch('kolla_mesos.cmd.deploy.open')
@mock.patch('kolla_mesos.cmd.deploy.os')
def test_start(self, mock_os, mock_open, mock_json):
self.worker._start_chronos_job = mock.MagicMock()
self.worker._start_marathon_app = mock.MagicMock()
mock_os.path = mock.MagicMock()
mock_os.walk = mock.MagicMock(
return_value=[('/', ('tmp1', 'tmp2'), ('file1', )),
('/', ('mdir', ), ('marathon', 'marathon2'))])
self.worker.setup_working_dir()
self.worker.gen_deployment_id()
self.worker.start()
self.assertEqual(1, self.worker._start_chronos_job.call_count)
self.assertEqual(2, self.worker._start_marathon_app.call_count)

View File

@ -28,11 +28,11 @@ data_files =
share/kolla-mesos/services = services/*
scripts =
tools/kolla-mesos-ansible
tools/kolla-mesos-deploy
tools/kolla-mesos-cleanup
[entry_points]
console_scripts =
kolla-mesos-deploy = kolla_mesos.cmd.deploy:main
kolla-mesos-cleanup = kolla_mesos.cmd.cleanup:main
kolla-mesos-update = kolla_mesos.cmd.update:main
kolla-mesos = kolla_mesos.cmd.shell:main
@ -52,6 +52,10 @@ kolla_mesos.cli =
config set = kolla_mesos.cli.config:ConfigSet
definition validate = kolla_mesos.cli.service_definition:Validate
definition inspect = kolla_mesos.cli.service_definition:Inspect
deployment run = kolla_mesos.cli.deployment:Run
deployment kill = kolla_mesos.cli.deployment:Kill
deployment cleanup = kolla_mesos.cli.deployment:Cleanup
deployment show = kolla_mesos.cli.deployment:Show
oslo.config.opts =
kolla_mesos = kolla_mesos.opts:list_opts

4
tools/kolla-mesos-cleanup Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "This command is deprecated, redirecting to kolla-mesos deployment cleanup"
kolla-mesos deployment cleanup

5
tools/kolla-mesos-deploy Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
echo "This command is deprecated, redirecting to kolla-mesos deployment run"
kolla-mesos deployment run
kolla-mesos deployment show