Update tripleo launch heat for HeatPodLauncher

Updates the "openstack tripleo launch heat" command to support launching
ephemeral Heat with any of the now supported options: native, container,
or pod. The command also can be used to start the Heat process and reuse
an existing environment with --heat-dir.

Change-Id: Ie665bc518751343fd4824f10c6efa6b5ca1c991d
Signed-off-by: James Slagle <jslagle@redhat.com>
This commit is contained in:
James Slagle 2021-02-08 17:27:33 -05:00
parent 6ab4d15863
commit d8e4878e2e
3 changed files with 250 additions and 47 deletions

View File

@ -27,7 +27,10 @@ import tempfile
import jinja2
from oslo_utils import timeutils
from tripleoclient import constants
from tripleoclient.constants import (DEFAULT_HEAT_CONTAINER,
DEFAULT_HEAT_API_CONTAINER,
DEFAULT_HEAT_ENGINE_CONTAINER,
DEFAULT_TEMPLATES_DIR)
log = logging.getLogger(__name__)
@ -115,13 +118,15 @@ class HeatBaseLauncher(object):
# The init function will need permission to touch these files
# and chown them accordingly for the heat user
def __init__(
self, api_port=8006,
all_container_image=constants.DEFAULT_HEAT_CONTAINER,
api_container_image=constants.DEFAULT_HEAT_API_CONTAINER,
engine_container_image=constants.DEFAULT_HEAT_ENGINE_CONTAINER,
user='heat', heat_dir='/var/log/heat-launcher', use_tmp_dir=True):
def __init__(self, api_port=8006,
all_container_image=DEFAULT_HEAT_CONTAINER,
api_container_image=DEFAULT_HEAT_API_CONTAINER,
engine_container_image=DEFAULT_HEAT_ENGINE_CONTAINER,
user='heat',
heat_dir='/var/log/heat-launcher',
use_tmp_dir=True,
rm_heat=False,
skip_heat_pull=False):
self.api_port = api_port
self.all_container_image = all_container_image
self.api_container_image = api_container_image
@ -130,6 +135,11 @@ class HeatBaseLauncher(object):
self.host = "127.0.0.1"
self.db_dump_path = os.path.join(
self.heat_dir, 'heat-db-dump.sql')
self.skip_heat_pull = skip_heat_pull
if rm_heat:
self.kill_heat(None)
self.rm_heat()
if os.path.isdir(self.heat_dir):
# This one may fail but it's just cleanup.
@ -293,6 +303,9 @@ class HeatContainerLauncher(HeatBaseLauncher):
self.host = "127.0.0.1"
def _fetch_container_image(self):
if self.skip_heat_pull:
log.info("Skipping container image pull.")
return
# force pull of latest container image
cmd = ['podman', 'pull', self.all_container_image]
log.debug(' '.join(cmd))
@ -370,6 +383,12 @@ class HeatContainerLauncher(HeatBaseLauncher):
# We don't want to hear from this command..
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def rm_heat(self, pid):
cmd = ['podman', 'rm', 'heat_all']
log.debug(' '.join(cmd))
# We don't want to hear from this command..
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
class HeatNativeLauncher(HeatBaseLauncher):
@ -408,6 +427,9 @@ class HeatPodLauncher(HeatContainerLauncher):
'-l', 's0', self.heat_dir])
def _fetch_container_image(self):
if self.skip_heat_pull:
log.info("Skipping container image pull.")
return
# force pull of latest container image
for image in self.api_container_image, self.engine_container_image:
log.info("Pulling conatiner image {}.".format(image))
@ -584,7 +606,7 @@ class HeatPodLauncher(HeatContainerLauncher):
return int(multiprocessing.cpu_count() / 2)
def _write_heat_config(self):
heat_config_tmpl_path = os.path.join(constants.DEFAULT_TEMPLATES_DIR,
heat_config_tmpl_path = os.path.join(DEFAULT_TEMPLATES_DIR,
"ephemeral-heat",
"heat.conf.j2")
with open(heat_config_tmpl_path) as tmpl:
@ -602,7 +624,7 @@ class HeatPodLauncher(HeatContainerLauncher):
conf.write(heat_config)
def _write_heat_pod(self):
heat_pod_tmpl_path = os.path.join(constants.DEFAULT_TEMPLATES_DIR,
heat_pod_tmpl_path = os.path.join(DEFAULT_TEMPLATES_DIR,
"ephemeral-heat",
"heat-pod.yaml.j2")
with open(heat_pod_tmpl_path) as tmpl:

View File

@ -52,6 +52,7 @@ from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from heatclient.exc import HTTPNotFound
from osc_lib import exceptions as oscexc
from osc_lib import utils as osc_lib_utils
from osc_lib.i18n import _
from oslo_concurrency import processutils
from six.moves import configparser
@ -60,13 +61,20 @@ from heatclient import exc as hc_exc
from six.moves.urllib import error as url_error
from six.moves.urllib import request
from tenacity import retry
from tenacity.stop import stop_after_attempt, stop_after_delay
from tenacity.wait import wait_fixed
from tripleo_common.utils import stack as stack_utils
from tripleo_common import update
from tripleoclient import constants
from tripleoclient import exceptions
from tripleoclient import heat_launcher
LOG = logging.getLogger(__name__ + ".utils")
_local_orchestration_client = None
_heat_pid = None
class Pushd(object):
@ -2541,3 +2549,94 @@ def write_user_environment(env_map, abs_env_path, tht_root,
LOG.debug("Writing user environment %s" % user_env_path)
f.write(contents)
return user_env_path
def launch_heat(launcher=None, restore_db=False):
global _local_orchestration_client
global _heat_pid
if _local_orchestration_client:
print("returning cached")
return _local_orchestration_client
if not launcher:
launcher = get_heat_launcher()
_heat_pid = 0
if launcher.heat_type == 'native':
_heat_pid = os.fork()
if _heat_pid == 0:
launcher.check_database()
launcher.check_message_bus()
launcher.heat_db_sync(restore_db)
launcher.launch_heat()
# Wait for the API to be listening
heat_api_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_heat_api_port(heat_api_socket, launcher.host, int(launcher.api_port))
_local_orchestration_client = local_orchestration_client(
launcher.host, launcher.api_port)
return _local_orchestration_client
@retry(stop=(stop_after_delay(10) | stop_after_attempt(10)),
wait=wait_fixed(0.5))
def test_heat_api_port(heat_api_socket, host, port):
heat_api_socket.connect((host, port))
def get_heat_launcher(heat_type, *args, **kwargs):
if heat_type == 'native':
return heat_launcher.HeatNativeLauncher(*args, **kwargs)
elif heat_type == 'container':
return heat_launcher.HeatContainerLauncher(*args, **kwargs)
else:
return heat_launcher.HeatPodLauncher(*args, **kwargs)
def local_orchestration_client(host="127.0.0.1", api_port=8006):
"""Returns a local orchestration service client"""
API_VERSIONS = {
'1': 'heatclient.v1.client.Client',
}
heat_client = osc_lib_utils.get_client_class(
'tripleoclient',
'1',
API_VERSIONS)
LOG.debug('Instantiating local_orchestration client for '
'host %s, port %s: %s',
host, api_port, heat_client)
endpoint = 'http://%s:%s/v1/admin' % (host, api_port)
client = heat_client(
endpoint=endpoint,
username='admin',
password='fake',
region_name='regionOne',
token='fake',
)
for v in ('OS_USER_DOMAIN_NAME',
'OS_PROJECT_DOMAIN_NAME',
'OS_PROJECT_NAME'):
os.environ.pop(v, None)
os.environ['OS_AUTH_TYPE'] = "none"
os.environ['OS_ENDPOINT'] = endpoint
return client
def kill_heat(launcher, backup_db=True):
global _heat_pid
if _heat_pid:
LOG.debug("Attempting to kill heat pid %s" % _heat_pid)
launcher.kill_heat(_heat_pid, backup_db)
def rm_heat(launcher, backup_db=False):
launcher.rm_heat(backup_db)

View File

@ -21,13 +21,15 @@ import os
from cliff import command
from osc_lib.i18n import _
from tripleoclient.constants import DEFAULT_HEAT_CONTAINER
from tripleoclient.constants import (DEFAULT_HEAT_CONTAINER,
DEFAULT_HEAT_API_CONTAINER,
DEFAULT_HEAT_ENGINE_CONTAINER)
from tripleoclient import exceptions
from tripleoclient import heat_launcher
from tripleoclient import utils
class LaunchHeat(command.Command):
"""Launch all-in-one Heat process and run in the foreground."""
"""Launch ephemeral Heat process."""
log = logging.getLogger(__name__ + ".Deploy")
auth_required = False
@ -41,32 +43,23 @@ class LaunchHeat(command.Command):
when cleanup is requested.
"""
if self.heat_pid:
self.heat_launch.kill_heat(self.heat_pid)
pid, ret = os.waitpid(self.heat_pid, 0)
self.heat_pid = None
self.log.info("Attempting to kill ephemeral heat")
if parsed_args.heat_type == "native":
if self.heat_pid:
self.log.info("Using heat pid: %s" % self.heat_pid)
self.heat_launcher.kill_heat(self.heat_pid)
pid, ret = os.waitpid(self.heat_pid, 0)
self.heat_pid = None
else:
self.log.info("No heat pid set, can't kill.")
else:
self.heat_launcher.kill_heat(None, backup_db=True)
return 0
def _launch_heat(self, parsed_args):
# we do this as root to chown config files properly for docker, etc.
if parsed_args.heat_native is not None and \
parsed_args.heat_native.lower() == "false":
self.heat_launch = heat_launcher.HeatContainerLauncher(
parsed_args.heat_api_port,
parsed_args.heat_container_image,
parsed_args.heat_user,
parsed_args.heat_dir)
else:
self.heat_launch = heat_launcher.HeatNativeLauncher(
parsed_args.heat_api_port,
parsed_args.heat_container_image,
parsed_args.heat_user,
parsed_args.heat_dir)
self.heat_launch.heat_db_sync()
self.heat_launch.launch_heat()
self.log.info("Launching Heat %s" % parsed_args.heat_type)
utils.launch_heat(self.heat_launcher, parsed_args.restore_db)
return 0
def get_parser(self, prog_name):
@ -90,7 +83,9 @@ class LaunchHeat(command.Command):
'Defaults to current user. '
'If the configuration files /etc/heat/heat.conf or '
'/usr/share/heat/heat-dist.conf exist, the user '
'must have read access to those files.')
'must have read access to those files.\n'
'This option is ignored when using --heat-type=container '
'or --heat-type=pod')
)
parser.add_argument(
'--heat-container-image', metavar='<HEAT_CONTAINER_IMAGE>',
@ -100,16 +95,22 @@ class LaunchHeat(command.Command):
'process. Defaults to: {}'.format(DEFAULT_HEAT_CONTAINER))
)
parser.add_argument(
'--heat-native',
dest='heat_native',
nargs='?',
default=None,
const="true",
help=_('Execute the heat-all process natively on this host. '
'This option requires that the heat-all binaries '
'be installed locally on this machine. '
'This option is enabled by default which means heat-all is '
'executed on the host OS directly.')
'--heat-container-api-image',
metavar='<HEAT_CONTAINER_API_IMAGE>',
dest='heat_container_api_image',
default=DEFAULT_HEAT_API_CONTAINER,
help=_('The container image to use when launching the heat-api '
'process. Only used when --heat-type=pod. '
'Defaults to: {}'.format(DEFAULT_HEAT_API_CONTAINER))
)
parser.add_argument(
'--heat-container-engine-image',
metavar='<HEAT_CONTAINER_ENGINE_IMAGE>',
dest='heat_container_engine_image',
default=DEFAULT_HEAT_ENGINE_CONTAINER,
help=_('The container image to use when launching the heat-engine '
'process. Only used when --heat-type=pod. '
'Defaults to: {}'.format(DEFAULT_HEAT_ENGINE_CONTAINER))
)
parser.add_argument(
'--kill', '-k',
@ -125,12 +126,82 @@ class LaunchHeat(command.Command):
default=os.path.join(os.getcwd(), 'heat-launcher'),
help=_("Directory to use for file storage and logs of the "
"running heat process. Defaults to 'heat-launcher' "
"in the current directory.")
"in the current directory. Can be set to an already "
"existing directory to reuse the environment from a "
"previos Heat process.")
)
parser.add_argument(
'--rm-heat',
action='store_true',
default=False,
help=_('If specified and --heat-type is container or pod '
'any existing container or pod of a previous '
'ephemeral Heat process will be deleted first. '
'Ignored if --heat-type is native.')
)
parser.add_argument(
'--skip-heat-pull',
action='store_true',
default=False,
help=_('When --heat-type is pod or container, assume '
'the container image has already been pulled ')
)
parser.add_argument(
'--restore-db',
action='store_true',
default=False,
help=_('Restore a database dump if it exists '
'within the directory specified by --heat-dir')
)
heat_type_group = parser.add_mutually_exclusive_group()
heat_type_group.add_argument(
'--heat-native',
dest='heat_native',
action='store_true',
default=False,
help=_('(DEPRECATED): Execute the heat-all process natively on '
'this host. '
'This option requires that the heat-all binaries '
'be installed locally on this machine. '
'This option is enabled by default which means heat-all is '
'executed on the host OS directly.\n'
'Conflicts with --heat-type, which deprecates '
'--heat-native.')
)
heat_type_group.add_argument(
'--heat-type',
dest='heat_type',
default='native',
choices=['native', 'container', 'pod'],
help=_('Type of ephemeral Heat process to launch. One of:\n'
'native: Execute heat-all directly on the host.\n'
'container: Execute heat-all in a container.\n'
'pod: Execute separate heat api and engine processes in '
'a podman pod.')
)
return parser
def take_action(self, parsed_args):
self._configure_logging(parsed_args)
self.log.debug("take_action(%s)" % parsed_args)
if parsed_args.heat_native:
heat_type = "native"
else:
heat_type = parsed_args.heat_type
self.heat_launcher = utils.get_heat_launcher(
heat_type, parsed_args.heat_api_port,
parsed_args.heat_container_image,
parsed_args.heat_container_api_image,
parsed_args.heat_container_engine_image,
parsed_args.heat_user,
parsed_args.heat_dir,
False,
False,
parsed_args.rm_heat,
parsed_args.skip_heat_pull)
if parsed_args.kill:
if self._kill_heat(parsed_args) != 0:
msg = _('Heat kill failed.')
@ -141,3 +212,14 @@ class LaunchHeat(command.Command):
msg = _('Heat launch failed.')
self.log.error(msg)
raise exceptions.DeploymentError(msg)
def _configure_logging(self, parsed_args):
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
self.log.addHandler(handler)
if self.app_args.verbose_level >= 2:
handler.setLevel(logging.DEBUG)
else:
handler.setLevel(logging.INFO)