Added support for upgrade and deploy per service

This adds a --services flag to the kollacli upgrade command.
A list of services can be provided, delimited by commas.

This also adds support for deploy to an individual or set
of services through the api.  Since it may not always work
correctly due to ansible playbooks it is meant to be used
by advanced users that make sure it behaves the way they
want it to.

Also fixing up / commenting out some tests that don't
function at this point.  They will be re-written once
the destroy changes are finished

Also moved verification of host specific deploy only
to compute nodes into the CLI so that advanced use of
the host deploy can be done through the API to
non-compute nodes.

Change-Id: Ia214627e45664d0958a73cb8c940efacd690d5ea
Jira-Issue:OSTACKDEV-148
This commit is contained in:
Borne Mace 2016-09-27 15:58:27 -07:00
parent d02b381b48
commit ea4ee6db65
7 changed files with 122 additions and 52 deletions

View File

@ -24,8 +24,8 @@ from kollacli.common.utils import safe_decode
class AsyncApi(object):
def async_deploy(self, hostnames=[],
serial_flag=False, verbose_level=1):
# type: (List[str], bool, int) -> Job
serial_flag=False, verbose_level=1, servicenames=[]):
# type: (List[str], bool, int, List[str]) -> Job
"""Deploy.
Deploy containers to hosts.
@ -36,6 +36,8 @@ class AsyncApi(object):
:type serial_flag: boolean
:param verbose_level: the higher the number, the more verbose
:type verbose_level: integer
:param servicenames: services to deploy. If empty, then deploy all.
:type servicenames: list of strings
:return: Job object
:rtype: Job
"""
@ -43,18 +45,23 @@ class AsyncApi(object):
empty_ok=True, none_ok=True)
check_arg(serial_flag, u._('Serial flag'), bool)
check_arg(verbose_level, u._('Verbose level'), int)
check_arg(servicenames, u._('Service names'), list,
empty_ok=True, none_ok=True)
hostnames = safe_decode(hostnames)
servicenames = safe_decode(servicenames)
ansible_job = actions.deploy(hostnames,
serial_flag, verbose_level)
serial_flag, verbose_level, servicenames)
return Job(ansible_job)
def async_upgrade(self, verbose_level=1):
# type: (int) -> Job
def async_upgrade(self, verbose_level=1, servicenames=[]):
# type: (int, List[str]) -> Job
"""Upgrade.
:param verbose_level: the higher the number, the more verbose
:type verbose_level: integer
:param servicenames: services to upgrade. If empty, then upgrade all.
:type servicenames: list of strings
:return: Job object
:rtype: Job
@ -62,7 +69,11 @@ class AsyncApi(object):
"openstack_release."
"""
check_arg(verbose_level, u._('Verbose level'), int)
ansible_job = actions.upgrade(verbose_level)
check_arg(servicenames, u._('Service names'), list,
empty_ok=True, none_ok=True)
servicenames = safe_decode(servicenames)
ansible_job = actions.upgrade(verbose_level, servicenames)
return Job(ansible_job)
def async_host_destroy(self, hostnames, destroy_type, verbose_level=1,

View File

@ -58,6 +58,22 @@ class Deploy(Command):
raise CommandError(u._('Timeout value is not a number.'))
timeout_target = time.time() + (60 * timeout)
# if we are doing a targeted host deploy make sure we are doing it
# to only compute nodes
if hosts:
invalid_host_list = []
compute_group = CLIENT.group_get(['compute'])[0]
compute_hosts = compute_group.get_hosts()
for host in hosts:
if host not in compute_hosts:
invalid_host_list.append(host)
if len(invalid_host_list) > 0:
raise CommandError(
u._('Invalid hosts for host targeted deploy. '
'Hosts must be in the compute group only.'
'Invalid hosts: {hosts}')
.format(hosts=invalid_host_list))
job = CLIENT.async_deploy(hosts, serial_flag,
verbose_level)

View File

@ -31,12 +31,19 @@ class Upgrade(Command):
"""Upgrade."""
def get_parser(self, prog_name):
parser = super(Upgrade, self).get_parser(prog_name)
parser.add_argument('--services', nargs='?',
metavar='<service_list>',
help=u._('Upgrade service list'))
return parser
def take_action(self, parsed_args):
services = None
try:
if parsed_args.services:
service_list = parsed_args.services.strip()
services = service_list.split(',')
verbose_level = self.app.options.verbose_level
job = CLIENT.async_upgrade(verbose_level)
job = CLIENT.async_upgrade(verbose_level, services)
status = job.wait()
if verbose_level > 2:
LOG.info('\n\n' + 80 * '=')

View File

@ -69,7 +69,7 @@ def destroy_hosts(hostnames, destroy_type,
def deploy(hostnames=[],
serial_flag=False, verbose_level=1):
serial_flag=False, verbose_level=1, servicenames=[]):
playbook = AnsiblePlaybook()
kolla_home = get_kolla_home()
playbook.playbook_path = os.path.join(kolla_home,
@ -77,8 +77,8 @@ def deploy(hostnames=[],
playbook.extra_vars = 'action=deploy'
playbook.hosts = hostnames
playbook.serial = serial_flag
playbook.verbose_level = verbose_level
playbook.services = servicenames
_run_deploy_rules(playbook)
@ -100,11 +100,12 @@ def precheck(hostnames, verbose_level=1):
playbook.hosts = hostnames
playbook.print_output = True
playbook.verbose_level = verbose_level
job = playbook.run()
return job
def upgrade(verbose_level=1):
def upgrade(verbose_level=1, servicenames=[]):
playbook = AnsiblePlaybook()
kolla_home = get_kolla_home()
playbook.playbook_path = os.path.join(kolla_home,
@ -112,6 +113,8 @@ def upgrade(verbose_level=1):
playbook.extra_vars = 'action=upgrade'
playbook.print_output = True
playbook.verbose_level = verbose_level
playbook.services = servicenames
job = playbook.run()
return job
@ -131,23 +134,6 @@ def _run_deploy_rules(playbook):
'\nEmpty passwords: '
'{keys}').format(etc=get_kolla_etc(), keys=empty_keys))
# if we are doing a targeted host deploy make sure we are doing it
# to only compute nodes
if playbook.hosts:
inventory.validate_hostnames(playbook.hosts)
host_groups = inventory.get_host_groups()
invalid_host_list = []
for host in playbook.hosts:
groups = host_groups.get(host, None)
if not groups or len(groups) != 1 or 'compute' not in groups:
invalid_host_list.append(host)
if len(invalid_host_list) > 0:
raise InvalidArgument(
u._('Invalid hosts for host targeted deploy. '
'Hosts must be in the compute group only.'
'Invalid hosts: {hosts}')
.format(hosts=invalid_host_list))
# cannot have both groups and hosts
if playbook.hosts and playbook.groups:
raise InvalidArgument(

View File

@ -81,6 +81,7 @@ class AnsibleJob(object):
'ansible job: {cmd} ')
.format(lock=get_ansible_lock_path(), cmd=self._command))
LOG.debug('playbook command: %s' % self._command)
# create and open named pipe, must be owned by kolla group
os.mkfifo(self._fifo_path)
_, grp_id = get_admin_uids()

View File

@ -13,7 +13,9 @@
# under the License.
#
from common import KollaCliTest
from common import TestConfig
from kollacli.api.client import ClientApi
from kollacli.common.allinone import AllInOne
from kollacli.common.ansible import job
from kollacli.common.inventory import Inventory
@ -21,6 +23,8 @@ from kollacli.common.inventory import Inventory
import json
import unittest
CLIENT = ClientApi()
class TestFunctional(KollaCliTest):
@ -146,6 +150,26 @@ class TestFunctional(KollaCliTest):
msg = self.run_cli_cmd('deploy --timeout .001', expect_error=True)
self.assertIn('timed out', msg)
# full host deploy to non-compute host. this can only be done
# through the api (cli test below makes sure it fails in cli)
msg = ''
try:
test_config = TestConfig()
test_config.load()
hostnames = test_config.get_hostnames()
CLIENT.host_add(hostnames)
job = CLIENT.async_deploy(hostnames=[hostnames[0]])
job.wait()
msg = job.get_console_output()
self.assertEqual(job.get_status(), 0,
'error performing whole host deploy %s' % msg)
except Exception as e:
self.assertEqual(0, 1,
'unexpected exception in host deploy %s, %s'
% (e.message, msg))
finally:
CLIENT.host_remove(hostnames)
# run compute host deploy to invalid host
err_msg = 'Status: unreachable'
msg = ''
@ -169,6 +193,27 @@ class TestFunctional(KollaCliTest):
# but it will go through the client code paths.
self.run_cli_cmd('upgrade -v')
msg = ''
hostnames = []
# run rabbitmq service deploy
try:
test_config = TestConfig()
test_config.load()
hostnames = test_config.get_hostnames()
CLIENT.host_add(hostnames)
job = CLIENT.async_upgrade(servicenames=['rabbitmq'])
job.wait()
msg = job.get_console_output()
self.assertEqual(job.get_status(), 0,
'error performing service specific deploy %s'
% msg)
except Exception as e:
self.assertEqual(0, 1,
'unexpected exception in service deploy: %s, %s'
% (e.message, msg))
finally:
CLIENT.host_remove(hostnames)
def test_deserialize(self):
# create a dummy ansible job
j = job.AnsibleJob('', 123, True, '')

View File

@ -33,13 +33,14 @@ ENABLED_SERVICES = [
# after deploy
EXPECTED_CONTAINERS_1 = [
'rabbitmq', 'rabbitmq_data'
'rabbitmq'
]
# TODO(bmace) needs to be refactored after destroy change
# after destroy --includedata
EXPECTED_CONTAINERS_2 = [
'rabbitmq_data'
]
# EXPECTED_CONTAINERS_2 = [
# 'rabbitmq_data'
# ]
class TestFunctional(KollaCliTest):
@ -140,27 +141,30 @@ class TestFunctional(KollaCliTest):
'is not running on host: %s ' % hostname +
'after deploy.')
# destroy non-data services (via --stop flag)
# this should leave only data containers running
self.log.info('Start destroy #2, do not include data')
job = CLIENT.async_host_destroy(hostnames, destroy_type='stop',
include_data=False)
self._process_job(job, 'destroy #2', is_physical_host)
if is_physical_host:
docker_ps = test_config.run_remote_cmd('docker ps', hostname)
for service in CLIENT.service_get_all():
if service.name not in ENABLED_SERVICES:
self.assertNotIn(service.name, docker_ps,
'disabled service: %s ' % service.name +
'is running on host: %s ' % hostname +
'after destroy (no data).')
for servicename in EXPECTED_CONTAINERS_2:
self.assertIn(servicename, docker_ps,
'enabled service: %s ' % servicename +
'is not running on host: %s ' % hostname +
'after destroy (no data).')
# TODO(bmace) Invalid until new destroy changes are committed.
# Data containers no longer exist.
# The tests will need to check for data volumes.
# # destroy non-data services (via --stop flag)
# # this should leave only data containers running
# self.log.info('Start destroy #2, do not include data')
# job = CLIENT.async_host_destroy(hostnames, destroy_type='stop',
# include_data=False)
# self._process_job(job, 'destroy #2', is_physical_host)
#
# if is_physical_host:
# docker_ps = test_config.run_remote_cmd('docker ps', hostname)
# for service in CLIENT.service_get_all():
# if service.name not in ENABLED_SERVICES:
# self.assertNotIn(service.name, docker_ps,
# 'disabled service: %s ' % service.name +
# 'is running on host: %s ' % hostname +
# 'after destroy (no data).')
# for servicename in EXPECTED_CONTAINERS_2:
# self.assertIn(servicename, docker_ps,
# 'enabled service: %s ' % servicename +
# 'is not running on host: %s ' % hostname +
# 'after destroy (no data).')
#
self.log.info('Start destroy #3, include data')
job = CLIENT.async_host_destroy(hostnames, destroy_type='stop',
include_data=True)