python utility to manage a tripleo based cloud
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

766 lines
28 KiB

# Copyright 2017 Red Hat, Inc.
#
# 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 configparser
import datetime
import glob
import grp
import json
import logging
import multiprocessing
import os
import pwd
import signal
import subprocess
import tarfile
import tempfile
import time
import jinja2
from oslo_utils import timeutils
from tenacity import retry, retry_if_exception_type
from tenacity.stop import stop_after_attempt, stop_after_delay
from tenacity.wait import wait_fixed
from tripleoclient.constants import (DEFAULT_HEAT_CONTAINER,
DEFAULT_HEAT_API_CONTAINER,
DEFAULT_HEAT_ENGINE_CONTAINER,
DEFAULT_TEMPLATES_DIR,
EPHEMERAL_HEAT_POD_NAME)
from tripleoclient.exceptions import HeatPodMessageQueueException
log = logging.getLogger(__name__)
NEXT_DAY = (timeutils.utcnow() + datetime.timedelta(days=2)).isoformat()
FAKE_TOKEN_RESPONSE = {
"token": {
"is_domain": False,
"methods": ["password"],
"roles": [{
"id": "4c8de39b96794ab28bf37a0b842b8bc8",
"name": "admin"
}],
"expires_at": NEXT_DAY,
"project": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "admin",
"name": "admin"
},
"catalog": [{
"endpoints": [{
"url": "http://127.0.0.1:%(heat_port)s/v1/admin",
"interface": "public",
"region": "regionOne",
"region_id": "regionOne",
"id": "2809305628004fb391b3d0254fb5b4f7"
}, {
"url": "http://127.0.0.1:%(heat_port)s/v1/admin",
"interface": "internal",
"region": "regionOne",
"region_id": "regionOne",
"id": "2809305628004fb391b3d0254fb5b4f7"
}, {
"url": "http://127.0.0.1:%(heat_port)s/v1/admin",
"interface": "admin",
"region": "regionOne",
"region_id": "regionOne",
"id": "2809305628004fb391b3d0254fb5b4f7"
}],
"type": "orchestration",
"id": "96a549e3961d45cabe883dd17c5835be",
"name": "heat"
}, {
"endpoints": [{
"url": "http://127.0.0.1/v3",
"interface": "public",
"region": "regionOne",
"region_id": "regionOne",
"id": "eca215878e404a2d9dcbcc7f6a027165"
}, {
"url": "http://127.0.0.1/v3",
"interface": "internal",
"region": "regionOne",
"region_id": "regionOne",
"id": "eca215878e404a2d9dcbcc7f6a027165"
}, {
"url": "http://127.0.0.1/v3",
"interface": "admin",
"region": "regionOne",
"region_id": "regionOne",
"id": "eca215878e404a2d9dcbcc7f6a027165"
}],
"type": "identity",
"id": "a785f0b7603042d1bf59237c71af2f15",
"name": "keystone"
}],
"user": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "8b7b4c094f934e8c83aa7fe12591dc6c",
"name": "admin"
},
"audit_ids": ["F6ONJ8fCT6i_CFTbmC0vBA"],
"issued_at": datetime.datetime.utcnow().isoformat()
}
}
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=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,
use_root=False,
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
self.engine_container_image = engine_container_image
self.heat_dir = os.path.abspath(heat_dir)
self.host = "127.0.0.1"
self.timestamp = time.time()
self.db_dump_path = os.path.join(self.heat_dir, 'heat-db.sql')
self.skip_heat_pull = skip_heat_pull
self.zipped_db_suffix = '.tar.bzip2'
self.log_dir = os.path.join(self.heat_dir, 'log')
if os.path.isdir(self.heat_dir):
if use_root:
# This one may fail but it's just cleanup.
p = subprocess.Popen(['umount', self.heat_dir],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
cmd_stdout, cmd_stderr = p.communicate()
retval = p.returncode
if retval != 0:
log.info('Cleanup unmount of %s failed (probably because '
'it was not mounted): %s' %
(self.heat_dir, cmd_stderr))
else:
log.info('umount of %s success' % (self.heat_dir))
else:
# Create the directory if it doesn't exist.
try:
os.makedirs(self.heat_dir, mode=0o700)
except Exception as e:
log.error('Creating temp directory "%s" failed: %s' %
(self.heat_dir, e))
raise Exception('Could not create temp directory %s: %s' %
(self.heat_dir, e))
if use_root:
# As an optimization we mount the tmp directory in a tmpfs (in
# memory) filesystem. Depending on your system this can cut the
# heat deployment times by half.
p = subprocess.Popen(['mount', '-t', 'tmpfs', '-o', 'size=500M',
'tmpfs', self.heat_dir],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
cmd_stdout, cmd_stderr = p.communicate()
retval = p.returncode
if retval != 0:
# It's ok if this fails, it will still work. It just won't
# be on tmpfs.
log.warning('Unable to mount tmpfs for logs and '
'database %s: %s' %
(self.heat_dir, cmd_stderr))
if use_tmp_dir:
self.install_dir = tempfile.mkdtemp(
prefix='%s/undercloud_deploy-' % self.heat_dir)
else:
self.install_dir = self.heat_dir
self.log_file = self._get_log_file_path()
self.sql_db = os.path.join(self.install_dir, 'heat.sqlite')
self.config_file = os.path.join(self.install_dir, 'heat.conf')
self.paste_file = os.path.join(self.install_dir, 'api-paste.ini')
self.token_file = os.path.join(self.install_dir, 'token_file.json')
self.policy_file = os.path.join(os.path.dirname(__file__),
'noauth_policy.json')
self.user = user
self._write_fake_keystone_token(self.api_port, self.token_file)
self._write_heat_config()
self._write_api_paste_config()
if use_root:
uid = int(self.get_heat_uid())
gid = int(self.get_heat_gid())
os.chown(self.install_dir, uid, gid)
os.chown(self.config_file, uid, gid)
os.chown(self.paste_file, uid, gid)
if rm_heat:
self.kill_heat(None)
self.rm_heat()
def _get_log_file_path(self):
return os.path.join(self.install_dir, 'heat.log')
def _write_heat_config(self):
# TODO(ksambor) It will be nice to have possibilities to configure heat
heat_config = '''
[DEFAULT]
log_file = %(log_file)s
transport_url = 'fake://'
rpc_poll_timeout = 60
rpc_response_timeout = 600
deferred_auth_method = password
num_engine_workers=1
convergence_engine = true
max_json_body_size = 8388608
heat_metadata_server_url=http://127.0.0.1:%(api_port)s/
default_deployment_signal_transport = HEAT_SIGNAL
max_nested_stack_depth = 10
keystone_backend = heat.engine.clients.os.keystone.fake_keystoneclient\
.FakeKeystoneClient
[noauth]
token_response = %(token_file)s
[heat_all]
enabled_services = api,engine
[heat_api]
workers = 1
bind_host = 127.0.0.1
bind_port = %(api_port)s
[database]
connection = sqlite:///%(sqlite_db)s.db
[paste_deploy]
flavor = noauth
api_paste_config = api-paste.ini
[oslo_policy]
policy_file = %(policy_file)s
[yaql]
memory_quota=900000
limit_iterators=9000
''' % {'sqlite_db': self.sql_db, 'log_file': self.log_file,
'api_port': self.api_port, 'policy_file': self.policy_file,
'token_file': self.token_file}
with open(self.config_file, 'w') as temp_file:
temp_file.write(heat_config)
def _write_api_paste_config(self):
heat_api_paste_config = '''
[pipeline:heat-api-noauth]
pipeline = faultwrap noauth context versionnegotiation apiv1app
[app:apiv1app]
paste.app_factory = heat.common.wsgi:app_factory
heat.app_factory = heat.api.openstack.v1:API
[filter:noauth]
paste.filter_factory = heat.common.noauth:filter_factory
[filter:context]
paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory
[filter:versionnegotiation]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:version_negotiation_filter
[filter:faultwrap]
paste.filter_factory = heat.common.wsgi:filter_factory
heat.filter_factory = heat.api.openstack:faultwrap_filter
'''
with open(self.paste_file, 'w') as temp_file:
temp_file.write(heat_api_paste_config)
def _write_fake_keystone_token(self, heat_api_port, config_file):
ks_token = json.dumps(FAKE_TOKEN_RESPONSE) % {'heat_port':
heat_api_port}
with open(config_file, 'w') as temp_file:
temp_file.write(ks_token)
def get_heat_uid(self):
return pwd.getpwnam(self.user).pw_uid
def get_heat_gid(self):
return grp.getgrnam(self.user).gr_gid
def check_database(self):
return True
def check_message_bus(self):
return True
def tar_file(self, file_path, cleanup=True):
tf_name = '{}-{}{}'.format(file_path, self.timestamp,
self.zipped_db_suffix)
tf = tarfile.open(tf_name, 'w:bz2')
tf.add(file_path, os.path.basename(file_path))
tf.close()
log.info("Created tarfile {}".format(tf_name))
if cleanup:
log.info("Deleting {}".format(file_path))
os.unlink(file_path)
def untar_file(self, tar_path, extract_dir):
tf = tarfile.open(tar_path, 'r:bz2')
tf.extractall(extract_dir)
class HeatContainerLauncher(HeatBaseLauncher):
heat_type = 'container'
def __init__(self, *args, **kwargs):
super(HeatContainerLauncher, self).__init__(*args, **kwargs)
self._fetch_container_image()
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))
try:
subprocess.check_output(cmd)
except subprocess.CalledProcessError as e:
raise Exception('Unable to fetch container image {}.'
'Error: {}'.format(self.all_container_image, e))
def launch_heat(self):
# run the heat-all process
cmd = [
'podman', 'run', '--rm',
'--name', 'heat_all',
'--user', self.user,
'--net', 'host',
'--volume', '%(conf)s:/etc/heat/heat.conf:ro' % {'conf':
self.config_file},
'--volume', '%(conf)s:/etc/heat/api-paste.ini:ro' % {
'conf': self.paste_file},
'--volume', '%(inst_tmp)s:%(inst_tmp)s:Z' % {'inst_tmp':
self.install_dir},
'--volume', '%(pfile)s:%(pfile)s:ro' % {'pfile':
self.policy_file},
self.all_container_image, 'heat-all'
]
log.debug(' '.join(cmd))
os.execvp('podman', cmd)
def heat_db_sync(self):
cmd = [
'podman', 'run', '--rm',
'--user', self.user,
'--volume', '%(conf)s:/etc/heat/heat.conf:Z' % {'conf':
self.config_file},
'--volume', '%(inst_tmp)s:%(inst_tmp)s:Z' % {'inst_tmp':
self.install_dir},
self.all_container_image,
'heat-manage', 'db_sync']
log.debug(' '.join(cmd))
subprocess.check_call(cmd)
def get_heat_uid(self):
cmd = [
'podman', 'run', '--rm',
self.all_container_image,
'getent', 'passwd', self.user
]
log.debug(' '.join(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
universal_newlines=True)
result = p.communicate()[0]
if result:
return result.split(':')[2]
raise Exception('Could not find heat uid')
def get_heat_gid(self):
cmd = [
'podman', 'run', '--rm',
self.all_container_image,
'getent', 'group', self.user
]
log.debug(' '.join(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
universal_newlines=True)
result = p.communicate()[0]
if result:
return result.split(':')[2]
raise Exception('Could not find heat gid')
def kill_heat(self, pid):
cmd = ['podman', 'stop', 'heat_all']
log.debug(' '.join(cmd))
# 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):
heat_type = 'native'
def __init__(self, *args, **kwargs):
super(HeatNativeLauncher, self).__init__(*args, **kwargs)
self.host = "127.0.0.1"
def launch_heat(self):
os.execvp('heat-all', ['heat-all', '--config-file', self.config_file])
def heat_db_sync(self):
subprocess.check_call(['heat-manage', '--config-file',
self.config_file, 'db_sync'])
def kill_heat(self, pid):
os.kill(pid, signal.SIGKILL)
class HeatPodLauncher(HeatContainerLauncher):
heat_type = 'pod'
def __init__(self, *args, **kwargs):
super(HeatPodLauncher, self).__init__(*args, **kwargs)
if not os.path.isdir(self.log_dir):
os.makedirs(self.log_dir)
self.host = self._get_ctlplane_ip()
self._chcon()
def _chcon(self):
subprocess.check_call(
['chcon', '-R', '-t', 'container_file_t',
'-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))
cmd = ['sudo', 'podman', 'pull', image]
log.debug(' '.join(cmd))
try:
subprocess.check_output(cmd)
except subprocess.CalledProcessError as e:
raise Exception('Unable to fetch container image {}.'
'Error: {}'.format(image, e))
def get_pod_state(self):
inspect = subprocess.run([
'sudo', 'podman', 'pod', 'inspect', '--format',
'"{{.State}}"', EPHEMERAL_HEAT_POD_NAME],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
return self._decode(inspect.stdout)
def launch_heat(self):
if "Running" in self.get_pod_state():
log.info("%s pod already running, skipping launch",
EPHEMERAL_HEAT_POD_NAME)
return
self._write_heat_pod()
subprocess.check_call([
'sudo', 'podman', 'play', 'kube',
os.path.join(self.heat_dir, 'heat-pod.yaml')
])
def heat_db_sync(self, restore_db=False):
if not self.database_exists():
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', '-e', 'create database heat'
])
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', '-e',
'create user if not exists '
'\'heat\'@\'%\' identified by \'heat\''
])
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', 'heat', '-e',
'grant all privileges on heat.* to \'heat\'@\'%\''
])
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', '-e', 'flush privileges;'
])
cmd = [
'sudo', 'podman', 'run', '--rm',
'--user', 'heat',
'--volume', '%(conf)s:/etc/heat/heat.conf:z' % {'conf':
self.config_file},
'--volume', '%(inst_tmp)s:%(inst_tmp)s:z' % {'inst_tmp':
self.install_dir},
self.api_container_image,
'heat-manage', 'db_sync']
log.debug(' '.join(cmd))
subprocess.check_call(cmd)
if restore_db:
self.do_restore_db()
def do_restore_db(self, db_dump_path=None):
if not db_dump_path:
db_dump_path = self.db_dump_path
# Find the latest dump from self.heat_dir
db_dumps = glob.glob(
'{}-*{}'.format
(db_dump_path,
self.zipped_db_suffix))
if not db_dumps:
raise Exception('No db backups found to restore in %s' %
self.heat_dir)
db_dump = max(db_dumps, key=os.path.getmtime)
self.untar_file(db_dump, self.heat_dir)
log.info("Restoring db from {}".format(db_dump))
try:
with open(db_dump_path) as f:
subprocess.run([
'sudo', 'podman', 'exec', '-i', '-u', 'root',
'mysql', 'mysql', 'heat'], stdin=f,
check=True)
finally:
os.unlink(db_dump_path)
def do_backup_db(self, db_dump_path=None):
if not db_dump_path:
db_dump_path = self.db_dump_path
if os.path.exists(db_dump_path):
raise Exception("Won't overwrite existing db dump at %s. "
"Remove it first." % db_dump_path)
log.info("Starting back up of heat db")
with open(db_dump_path, 'w') as out:
subprocess.run([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysqldump', 'heat'], stdout=out,
check=True)
self.tar_file(db_dump_path)
def pod_exists(self):
try:
subprocess.check_call(
['sudo', 'podman', 'pod', 'inspect', EPHEMERAL_HEAT_POD_NAME],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return True
except subprocess.CalledProcessError:
return False
def rm_heat(self, backup_db=True):
if self.database_exists():
if backup_db:
self.do_backup_db()
try:
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', 'heat', '-e',
'drop database heat'])
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root',
'mysql', 'mysql', '-e',
'drop user \'heat\'@\'%\''])
except subprocess.CalledProcessError:
pass
if self.pod_exists():
log.info("Removing pod: %s", EPHEMERAL_HEAT_POD_NAME)
subprocess.call([
'sudo', 'podman', 'pod', 'rm', '-f',
EPHEMERAL_HEAT_POD_NAME
])
config = self._read_heat_config()
log_file_path = os.path.join(self.log_dir,
config['DEFAULT']['log_file'])
if os.path.exists(log_file_path):
self.tar_file(log_file_path)
def stop_heat(self):
if self.pod_exists() and self.get_pod_state() != 'Exited':
log.info("Stopping pod: %s", EPHEMERAL_HEAT_POD_NAME)
subprocess.check_call([
'sudo', 'podman', 'pod', 'stop',
EPHEMERAL_HEAT_POD_NAME
])
log.info("Stopped pod: %s", EPHEMERAL_HEAT_POD_NAME)
def check_message_bus(self):
log.info("Checking that message bus (rabbitmq) is up")
try:
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root', 'rabbitmq',
'rabbitmqctl', 'list_queues'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return True
except subprocess.CalledProcessError as cpe:
log.error("The message bus (rabbitmq) does not seem "
"to be available")
log.error(cpe)
raise
def check_database(self):
log.info("Checking that database is up")
try:
subprocess.check_call([
'sudo', 'podman', 'exec', '-u', 'root', 'mysql',
'mysql', '-h', self._get_ctlplane_ip(), '-e',
'show databases;'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return True
except subprocess.CalledProcessError as cpe:
log.error("The database does not seem to be available")
log.error(cpe)
raise
def database_exists(self):
output = subprocess.check_output([
'sudo', 'podman', 'exec', '-u', 'root', 'mysql',
'mysql', '-e', 'show databases like "heat"'
])
return 'heat' in str(output)
def kill_heat(self, pid):
if self.pod_exists():
log.info("Killing pod: %s", EPHEMERAL_HEAT_POD_NAME)
subprocess.call([
'sudo', 'podman', 'pod', 'kill',
EPHEMERAL_HEAT_POD_NAME
])
log.info("Killed pod: %s", EPHEMERAL_HEAT_POD_NAME)
else:
log.info("Pod does not exist: %s", EPHEMERAL_HEAT_POD_NAME)
def _decode(self, encoded):
if not encoded:
return ""
decoded = encoded.decode('utf-8')
if decoded.endswith('\n'):
decoded = decoded[:-1]
return decoded
def _get_transport_url(self):
user = self._decode(subprocess.check_output(
['sudo', 'hiera', 'rabbitmq::default_user']))
password = self._decode(subprocess.check_output(
['sudo', 'hiera', 'rabbitmq::default_pass']))
fqdn_ctlplane = self._decode(subprocess.check_output(
['sudo', 'hiera', 'fqdn_ctlplane']))
port = self._decode(subprocess.check_output(
['sudo', 'hiera', 'rabbitmq::port']))
transport_url = "rabbit://%s:%s@%s:%s/?ssl=0" % \
(user, password, fqdn_ctlplane, port)
return transport_url
def _get_db_connection(self):
return ('mysql+pymysql://'
'heat:heat@{}/heat?read_default_file='
'/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'.format(
self._get_ctlplane_vip()))
def _get_ctlplane_vip(self):
return self._decode(subprocess.check_output(
['sudo', 'hiera', 'controller_virtual_ip']))
def _get_ctlplane_ip(self):
return self._decode(subprocess.check_output(
['sudo', 'hiera', 'ctlplane']))
def _get_num_engine_workers(self):
return int(multiprocessing.cpu_count() / 2)
@retry(retry=retry_if_exception_type(HeatPodMessageQueueException),
reraise=True,
stop=(stop_after_delay(10) | stop_after_attempt(10)),
wait=wait_fixed(0.5))
def wait_for_message_queue(self):
queue_name = 'engine.' + EPHEMERAL_HEAT_POD_NAME
output = subprocess.check_output([
'sudo', 'podman', 'exec', 'rabbitmq',
'rabbitmqctl', 'list_queues'])
if str(output).count(queue_name) < 1:
msg = "Message queue for ephemeral heat not created in time."
raise HeatPodMessageQueueException(msg)
def _get_log_file_path(self):
return 'heat-{}.log'.format(self.timestamp)
def _read_heat_config(self):
config = configparser.ConfigParser()
config.read(self.config_file)
return config
def _write_heat_config(self):
heat_config_tmpl_path = os.path.join(DEFAULT_TEMPLATES_DIR,
EPHEMERAL_HEAT_POD_NAME,
"heat.conf.j2")
with open(heat_config_tmpl_path) as tmpl:
heat_config_tmpl = jinja2.Template(tmpl.read())
config_vars = {
"transport_url": self._get_transport_url(),
"db_connection": self._get_db_connection(),
"api_port": self.api_port,
"num_engine_workers": self._get_num_engine_workers(),
"log_file": self.log_file,
}
heat_config = heat_config_tmpl.render(**config_vars)
with open(self.config_file, 'w') as conf:
conf.write(heat_config)
def _write_heat_pod(self):
heat_pod_tmpl_path = os.path.join(DEFAULT_TEMPLATES_DIR,
EPHEMERAL_HEAT_POD_NAME,
"heat-pod.yaml.j2")
with open(heat_pod_tmpl_path) as tmpl:
heat_pod_tmpl = jinja2.Template(tmpl.read())
pod_vars = {
"install_dir": self.install_dir,
"heat_dir": self.heat_dir,
"policy_file": self.policy_file,
"ctlplane_ip": self.host,
"api_port": self.api_port,
"api_image": self.api_container_image,
"engine_image": self.engine_container_image,
"heat_pod_name": EPHEMERAL_HEAT_POD_NAME
}
heat_pod = heat_pod_tmpl.render(**pod_vars)
heat_pod_path = os.path.join(self.heat_dir, "heat-pod.yaml")
with open(heat_pod_path, 'w') as conf:
conf.write(heat_pod)