python-tripleoclient/tripleoclient/v1/undercloud_deploy.py

500 lines
18 KiB
Python

# Copyright 2016 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.
#
from __future__ import print_function
import argparse
import glob
import logging
import netaddr
import os
import pwd
import subprocess
import sys
import tempfile
import time
import traceback
import yaml
try:
from urllib2 import HTTPError
from urllib2 import URLError
from urllib2 import urlopen
except ImportError:
# python3
from urllib.error import HTTPError
from urllib.error import URLError
from urllib.request import urlopen
from cliff import command
from heatclient.common import event_utils
from heatclient.common import template_utils
from heatclient.common import utils as heat_utils
from openstackclient.i18n import _
from tripleoclient import constants
from tripleoclient import exceptions
from tripleoclient import heat_launcher
from tripleo_common.utils import passwords as password_utils
# For ansible download
from tripleo_common.utils import config
ANSIBLE_INVENTORY = """
[targets]
overcloud ansible_connection=local
[Undercloud]
overcloud
[{hostname}]
overcloud
"""
class DeployUndercloud(command.Command):
"""Deploy Undercloud (experimental feature)"""
log = logging.getLogger(__name__ + ".DeployUndercloud")
auth_required = False
heat_pid = None
def _get_hostname(self):
p = subprocess.Popen(["hostname", "-s"], stdout=subprocess.PIPE)
return p.communicate()[0].rstrip()
def _configure_puppet(self):
print('Configuring puppet modules symlinks ...')
src = constants.TRIPLEO_PUPPET_MODULES
dst = constants.PUPPET_MODULES
subprocess.check_call(['mkdir', '-p', dst])
tmp = tempfile.mkdtemp(dir=constants.PUPPET_BASE)
os.chmod(tmp, 0o755)
for obj in os.listdir(src):
tmpf = os.path.join(tmp, obj)
os.symlink(os.path.join(src, obj), tmpf)
os.rename(tmpf, os.path.join(dst, obj))
os.rmdir(tmp)
def _wait_local_port_ready(self, api_port):
count = 0
while count < 30:
time.sleep(1)
count += 1
try:
urlopen("http://127.0.0.1:%s/" % api_port, timeout=1)
except HTTPError as he:
if he.code == 300:
return True
pass
except URLError:
pass
return False
def _update_passwords_env(self, passwords=None):
pw_file = os.path.join(os.environ.get('HOME', ''),
'tripleo-undercloud-passwords.yaml')
stack_env = {'parameter_defaults': {}}
if os.path.exists(pw_file):
with open(pw_file) as pf:
stack_env = yaml.safe_load(pf.read())
pw = password_utils.generate_passwords(stack_env=stack_env)
stack_env['parameter_defaults'].update(pw)
if passwords:
# These passwords are the DefaultPasswords so we only
# update if they don't already exist in stack_env
for p, v in passwords.items():
if p not in stack_env['parameter_defaults']:
stack_env['parameter_defaults'][p] = v
with open(pw_file, 'w') as pf:
yaml.safe_dump(stack_env, pf, default_flow_style=False)
return pw_file
def _generate_hosts_parameters(self, parsed_args):
hostname = self._get_hostname()
domain = parsed_args.local_domain
data = {
'CloudName': hostname,
'CloudDomain': domain,
'CloudNameInternal': '%s.internalapi.%s' % (hostname, domain),
'CloudNameStorage': '%s.storage.%s' % (hostname, domain),
'CloudNameStorageManagement': ('%s.storagemgmt.%s'
% (hostname, domain)),
'CloudNameCtlplane': '%s.ctlplane.%s' % (hostname, domain),
}
return data
def _generate_portmap_parameters(self, ip_addr, cidr):
hostname = self._get_hostname()
data = {
'HostnameMap': {
'undercloud-undercloud-0': '%s' % hostname
},
'DeployedServerPortMap': {
('%s-ctlplane' % hostname): {
'fixed_ips': [{'ip_address': ip_addr}],
'subnets': [{'cidr': cidr}]
},
'control_virtual_ip': {
'fixed_ips': [{'ip_address': ip_addr}],
'subnets': [{'cidr': cidr}]
}
}
}
return data
def _kill_heat(self):
if self.heat_pid:
self.heat_launch.kill_heat(self.heat_pid)
pid, ret = os.waitpid(self.heat_pid, 0)
self.heat_pid = None
def _launch_heat(self, parsed_args):
# we do this as root to chown config files properly for docker, etc.
if parsed_args.heat_native:
self.heat_launch = heat_launcher.HeatNativeLauncher(
parsed_args.heat_api_port,
parsed_args.heat_container_image,
parsed_args.heat_user)
else:
self.heat_launch = heat_launcher.HeatDockerLauncher(
parsed_args.heat_api_port,
parsed_args.heat_container_image,
parsed_args.heat_user)
# NOTE(dprince): we launch heat with fork exec because
# we don't want it to inherit our args. Launching heat
# as a "library" would be cool... but that would require
# more refactoring. It runs a single process and we kill
# it always below.
self.heat_pid = os.fork()
if self.heat_pid == 0:
if parsed_args.heat_native:
try:
uid = pwd.getpwnam(parsed_args.heat_user).pw_uid
gid = pwd.getpwnam(parsed_args.heat_user).pw_gid
except KeyError:
raise exceptions.DeploymentError(
"Please create a %s user account before "
"proceeding." % parsed_args.heat_user)
os.setgid(gid)
os.setuid(uid)
self.heat_launch.heat_db_sync()
# Exec() never returns.
self.heat_launch.launch_heat()
# NOTE(dprince): we use our own client here because we set
# auth_required=False above because keystone isn't running when this
# command starts
tripleoclients = self.app.client_manager.tripleoclient
orchestration_client = \
tripleoclients.local_orchestration(parsed_args.heat_api_port)
return orchestration_client
def _setup_heat_environments(self, parsed_args):
tht_root = parsed_args.templates
# generate jinja templates
args = ['python', 'tools/process-templates.py', '--roles-data',
'roles_data_undercloud.yaml']
subprocess.check_call(args, cwd=tht_root)
print("Deploying templates in the directory {0}".format(
os.path.abspath(tht_root)))
self.log.debug("Creating Environment file")
environments = []
resource_registry_path = os.path.join(
tht_root, 'overcloud-resource-registry-puppet.yaml')
environments.insert(0, resource_registry_path)
# this will allow the user to overwrite passwords with custom envs
pw_file = self._update_passwords_env()
environments.insert(1, pw_file)
undercloud_env_path = os.path.join(
tht_root, 'environments', 'undercloud.yaml')
environments.append(undercloud_env_path)
# use deployed-server because we run os-collect-config locally
deployed_server_env = os.path.join(
tht_root, 'environments',
'config-download-environment.yaml')
environments.append(deployed_server_env)
# use deployed-server because we run os-collect-config locally
deployed_server_env = os.path.join(
tht_root, 'environments',
'deployed-server-noop-ctlplane.yaml')
environments.append(deployed_server_env)
if parsed_args.environment_files:
environments.extend(parsed_args.environment_files)
with tempfile.NamedTemporaryFile(delete=False) as tmp_env_file:
tmp_env = self._generate_hosts_parameters(parsed_args)
ip_nw = netaddr.IPNetwork(parsed_args.local_ip)
ip = str(ip_nw.ip)
cidr = str(ip_nw.netmask)
tmp_env.update(self._generate_portmap_parameters(ip, cidr))
with open(tmp_env_file.name, 'w') as env_file:
yaml.safe_dump({'parameter_defaults': tmp_env}, env_file,
default_flow_style=False)
environments.append(tmp_env_file.name)
return environments
def _deploy_tripleo_heat_templates(self, orchestration_client,
parsed_args):
"""Deploy the fixed templates in TripleO Heat Templates"""
environments = self._setup_heat_environments(parsed_args)
self.log.debug("Processing environment files")
env_files, env = (
template_utils.process_multiple_environments_and_files(
environments))
self.log.debug("Getting template contents")
template_path = os.path.join(parsed_args.templates, 'overcloud.yaml')
template_files, template = \
template_utils.get_template_contents(template_path)
files = dict(list(template_files.items()) + list(env_files.items()))
stack_name = parsed_args.stack
self.log.debug("Deploying stack: %s", stack_name)
self.log.debug("Deploying template: %s", template)
self.log.debug("Deploying environment: %s", env)
self.log.debug("Deploying files: %s", files)
stack_args = {
'stack_name': stack_name,
'template': template,
'environment': env,
'files': files,
}
if parsed_args.timeout:
stack_args['timeout_mins'] = parsed_args.timeout
self.log.info("Performing Heat stack create")
stack = orchestration_client.stacks.create(**stack_args)
stack_id = stack['stack']['id']
return stack_id
def _wait_for_heat_complete(self, orchestration_client, stack_id, timeout):
# Wait for the stack to go to COMPLETE.
timeout_t = time.time() + 60 * timeout
marker = None
event_log_context = heat_utils.EventLogContext()
kwargs = {
'sort_dir': 'asc',
'nested_depth': '6'
}
while True:
time.sleep(2)
events = event_utils.get_events(
orchestration_client,
stack_id=stack_id,
event_args=kwargs,
marker=marker)
if events:
marker = getattr(events[-1], 'id', None)
events_log = heat_utils.event_log_formatter(
events, event_log_context)
print(events_log)
status = orchestration_client.stacks.get(stack_id).status
if status == 'FAILED':
raise Exception('Stack create failed')
if status == 'COMPLETE':
break
if time.time() > timeout_t:
msg = 'Stack creation timeout: %d minutes elapsed' % (timeout)
raise Exception(msg)
def _download_ansible_playbooks(self, client):
stack_config = config.Config(client)
output_dir = os.environ.get('HOME')
print('** Downloading undercloud ansible.. **')
# python output buffering is making this seem to take forever..
sys.stdout.flush()
stack_config.download_config('undercloud', output_dir)
# Sadly the above writes the ansible config to a new directory each
# time. This finds the newest new entry.
ansible_dir = max(glob.iglob('%s/tripleo-*-config' % output_dir),
key=os.path.getctime)
# Write out the inventory file.
with open('%s/inventory' % ansible_dir, 'w') as f:
f.write(ANSIBLE_INVENTORY.format(hostname=self._get_hostname()))
print('** Downloaded undercloud ansible to %s **' % ansible_dir)
sys.stdout.flush()
return ansible_dir
# Never returns, calls exec()
def _launch_ansible(self, ansible_dir):
os.chdir(ansible_dir)
playbook_inventory = "%s/inventory" % (ansible_dir)
cmd = ['ansible-playbook', '-i', playbook_inventory,
'deploy_steps_playbook.yaml', '-e', 'role_name=Undercloud',
'-e', 'deploy_server_id=undercloud', '-e',
'bootstrap_server_id=undercloud']
print('Running Ansible: %s' % (' '.join(cmd)))
# execvp() doesn't return.
os.execvp(cmd[0], cmd)
def get_parser(self, prog_name):
parser = argparse.ArgumentParser(
description=self.get_description(),
prog=prog_name,
add_help=False
)
parser.add_argument(
'--templates', nargs='?', const=constants.TRIPLEO_HEAT_TEMPLATES,
help=_("The directory containing the Heat templates to deploy"),
)
parser.add_argument('--stack',
help=_("Stack name to create"),
default='undercloud')
parser.add_argument('-t', '--timeout', metavar='<TIMEOUT>',
type=int, default=30,
help=_('Deployment timeout in minutes.'))
parser.add_argument(
'-e', '--environment-file', metavar='<HEAT ENVIRONMENT FILE>',
action='append', dest='environment_files',
help=_('Environment files to be passed to the heat stack-create '
'or heat stack-update command. (Can be specified more than '
'once.)')
)
parser.add_argument(
'--heat-api-port', metavar='<HEAT_API_PORT>',
dest='heat_api_port',
default='8006',
help=_('Heat API port to use for the installers private'
' Heat API instance. Optional. Default: 8006.)')
)
parser.add_argument(
'--heat-user', metavar='<HEAT_USER>',
dest='heat_user',
default='heat',
help=_('User to execute the non-priveleged heat-all process. '
'Defaults to heat.')
)
parser.add_argument(
'--heat-container-image', metavar='<HEAT_CONTAINER_IMAGE>',
dest='heat_container_image',
default='tripleoupstream/centos-binary-heat-all',
help=_('The container image to use when launching the heat-all '
'process. Defaults to: '
'tripleoupstream/centos-binary-heat-all')
)
parser.add_argument(
'--heat-native',
action='store_true',
default=False,
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 off by default which means heat-all is '
'executed in a docker container.')
)
parser.add_argument(
'--local-ip', metavar='<LOCAL_IP>',
dest='local_ip',
help=_('Local IP/CIDR for undercloud traffic. Required.')
)
parser.add_argument(
'--local-domain', metavar='<LOCAL_DOMAIN>',
dest='local_domain',
default='undercloud',
help=_('Local domain for undercloud and its API endpoints')
)
parser.add_argument(
'-k',
'--keep-running',
action='store_true',
dest='keep_running',
help=_('Keep the process running on failures for debugging')
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
print("\nUndercloud deploy is an experimental developer focused "
"feature that does not yet replace "
"'openstack undercloud install'.")
if not parsed_args.local_ip:
print('Please set --local-ip to the correct ipaddress/cidr '
'for this machine.')
return
if not os.environ.get('HEAT_API_PORT'):
os.environ['HEAT_API_PORT'] = parsed_args.heat_api_port
# The main thread runs as root and we drop privs for forked
# processes below. Only the heat deploy/os-collect-config forked
# process runs as root.
if os.geteuid() != 0:
raise exceptions.DeploymentError("Please run as root.")
# configure puppet
self._configure_puppet()
try:
# Launch heat.
orchestration_client = self._launch_heat(parsed_args)
# Wait for heat to be ready.
self._wait_local_port_ready(parsed_args.heat_api_port)
# Deploy TripleO Heat templates.
stack_id = \
self._deploy_tripleo_heat_templates(orchestration_client,
parsed_args)
# Wait for complete..
self._wait_for_heat_complete(orchestration_client, stack_id,
parsed_args.timeout)
# download the ansible playbooks and execute them.
ansible_dir = \
self._download_ansible_playbooks(orchestration_client)
# Kill heat, we're done with it now.
self._kill_heat()
# Never returns.. We exec() it directly.
self._launch_ansible(ansible_dir)
except Exception as e:
print("Exception: %s" % e)
print(traceback.format_exception(*sys.exc_info()))
raise
finally:
# We only get here on error.
print('ERROR: Heat log files: %s' % (self.heat_launch.install_tmp))
self._kill_heat()
return 1