2015-04-02 13:20:11 +01:00
|
|
|
# Copyright 2015 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.
|
|
|
|
#
|
|
|
|
|
2015-10-15 10:25:13 +02:00
|
|
|
from __future__ import print_function
|
2016-07-08 11:19:23 +00:00
|
|
|
import csv
|
2017-03-16 16:29:51 +01:00
|
|
|
import datetime
|
2018-02-14 18:29:57 -08:00
|
|
|
import getpass
|
2017-10-12 17:22:18 +01:00
|
|
|
import glob
|
2015-04-02 13:20:11 +01:00
|
|
|
import hashlib
|
2015-04-22 10:41:55 +01:00
|
|
|
import logging
|
2015-05-14 15:26:48 +01:00
|
|
|
import os
|
2016-02-03 11:35:43 +00:00
|
|
|
import os.path
|
2017-06-04 20:33:17 +02:00
|
|
|
import simplejson
|
2015-04-02 13:20:11 +01:00
|
|
|
import six
|
2016-03-16 18:24:29 +01:00
|
|
|
import socket
|
2015-05-14 15:31:23 +01:00
|
|
|
import subprocess
|
2016-08-25 02:43:22 +02:00
|
|
|
import sys
|
2018-02-09 15:56:13 +01:00
|
|
|
import tempfile
|
2015-04-15 12:27:58 -04:00
|
|
|
import time
|
2016-07-08 11:19:23 +00:00
|
|
|
import yaml
|
2015-04-02 13:20:11 +01:00
|
|
|
|
2015-10-15 13:30:19 +13:00
|
|
|
from heatclient.common import event_utils
|
2018-02-09 15:56:13 +01:00
|
|
|
from heatclient.common import template_utils
|
2015-09-17 15:32:07 -04:00
|
|
|
from heatclient.exc import HTTPNotFound
|
2016-06-29 12:55:58 +01:00
|
|
|
from osc_lib.i18n import _
|
2018-02-22 18:21:09 +02:00
|
|
|
from oslo_concurrency import processutils
|
2015-09-18 10:15:30 -04:00
|
|
|
from six.moves import configparser
|
2015-09-17 15:32:07 -04:00
|
|
|
|
2018-02-09 15:56:13 +01:00
|
|
|
from heatclient import exc as hc_exc
|
2015-09-08 10:22:22 -04:00
|
|
|
from tripleoclient import exceptions
|
2015-04-13 12:00:02 +01:00
|
|
|
|
|
|
|
|
2016-03-16 18:24:29 +01:00
|
|
|
def bracket_ipv6(address):
|
|
|
|
"""Put a bracket around address if it is valid IPv6
|
|
|
|
|
|
|
|
Return it unchanged if it is a hostname or IPv4 address.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
socket.inet_pton(socket.AF_INET6, address)
|
|
|
|
return "[%s]" % address
|
|
|
|
except socket.error:
|
|
|
|
return address
|
|
|
|
|
|
|
|
|
2016-11-17 12:23:54 +00:00
|
|
|
def write_overcloudrc(stack_name, overcloudrcs, config_directory='.'):
|
|
|
|
"""Write the overcloudrc files"""
|
2015-09-18 10:15:30 -04:00
|
|
|
|
2016-11-17 12:23:54 +00:00
|
|
|
rcpath = os.path.join(config_directory, '%src' % stack_name)
|
2018-01-29 14:46:23 +00:00
|
|
|
rcv3path = os.path.join(config_directory, '%src.v3' % stack_name)
|
2016-11-17 12:23:54 +00:00
|
|
|
|
|
|
|
with open(rcpath, 'w') as rcfile:
|
|
|
|
rcfile.write(overcloudrcs['overcloudrc'])
|
2017-03-15 12:00:29 -04:00
|
|
|
os.chmod(rcpath, 0o600)
|
2016-11-17 12:23:54 +00:00
|
|
|
|
2018-01-29 14:46:23 +00:00
|
|
|
with open(rcv3path, 'w') as rcv3file:
|
|
|
|
rcv3file.write(overcloudrcs['overcloudrc.v3'])
|
|
|
|
os.chmod(rcv3path, 0o600)
|
2017-11-21 13:18:02 +00:00
|
|
|
return os.path.abspath(rcpath)
|
2015-09-18 10:15:30 -04:00
|
|
|
|
|
|
|
|
2017-11-21 14:27:47 +00:00
|
|
|
def store_cli_param(command_name, parsed_args):
|
2017-03-16 16:29:51 +01:00
|
|
|
"""write the cli parameters into an history file"""
|
|
|
|
|
2017-11-21 14:27:47 +00:00
|
|
|
# The command name is the part after "openstack" with spaces. Switching
|
|
|
|
# to "-" makes it easier to read. "openstack undercloud install" will be
|
|
|
|
# stored as "undercloud-install" for example.
|
|
|
|
command_name = command_name.replace(" ", "-")
|
|
|
|
|
2017-03-16 16:29:51 +01:00
|
|
|
history_path = os.path.join(os.path.expanduser("~"), '.tripleo')
|
|
|
|
if not os.path.exists(history_path):
|
|
|
|
try:
|
|
|
|
os.mkdir(history_path)
|
|
|
|
except OSError as e:
|
2017-11-17 10:34:53 +00:00
|
|
|
messages = "Unable to create TripleO history directory: " \
|
|
|
|
"{0}, {1}".format(history_path, e)
|
2017-03-16 16:29:51 +01:00
|
|
|
raise OSError(messages)
|
|
|
|
if os.path.isdir(history_path):
|
|
|
|
try:
|
|
|
|
with open(os.path.join(history_path,
|
|
|
|
'history'), 'a') as history:
|
|
|
|
args = parsed_args.__dict__.copy()
|
|
|
|
used_args = ', '.join('%s=%s' % (key, value)
|
|
|
|
for key, value in args.items())
|
|
|
|
history.write(' '.join([str(datetime.datetime.now()),
|
2017-11-21 14:27:47 +00:00
|
|
|
str(command_name), used_args, "\n"]))
|
2017-03-16 16:29:51 +01:00
|
|
|
except IOError as e:
|
|
|
|
messages = "Unable to write into TripleO history file: "
|
|
|
|
"{0}, {1}".format(history_path, e)
|
|
|
|
raise IOError(messages)
|
|
|
|
else:
|
|
|
|
raise exceptions.InvalidConfiguration("Target path %s is not a "
|
|
|
|
"directory" % history_path)
|
|
|
|
|
|
|
|
|
2015-09-18 10:15:30 -04:00
|
|
|
def create_tempest_deployer_input(config_name='tempest-deployer-input.conf'):
|
|
|
|
config = configparser.ConfigParser()
|
|
|
|
|
2016-11-07 16:59:49 +01:00
|
|
|
# Create required sections
|
|
|
|
for section in ('auth', 'compute', 'compute-feature-enabled', 'identity',
|
|
|
|
'image', 'network', 'object-storage', 'orchestration',
|
|
|
|
'volume', 'volume-feature-enabled'):
|
|
|
|
config.add_section(section)
|
|
|
|
|
2017-07-24 22:07:57 +02:00
|
|
|
# Dynamic credentials means tempest will create the required credentials if
|
|
|
|
# a test requires a new account to work, tempest will create one just for
|
|
|
|
# that test
|
|
|
|
config.set('auth', 'use_dynamic_credentials', 'true')
|
2016-10-11 07:26:53 +02:00
|
|
|
|
2015-09-18 10:15:30 -04:00
|
|
|
# Does the test environment support obtaining instance serial console
|
|
|
|
# output? (default: true)
|
|
|
|
# set in [nova.serial_console]->enabled
|
|
|
|
config.set('compute-feature-enabled', 'console_output', 'false')
|
|
|
|
|
|
|
|
# Role required for users to be able to manage stacks
|
|
|
|
# (default: 'heat_stack_owner')
|
|
|
|
# keystone role-list returns this role
|
2016-10-11 07:26:53 +02:00
|
|
|
config.set('orchestration', 'stack_owner_role', 'swiftoperator')
|
2015-09-18 10:15:30 -04:00
|
|
|
|
|
|
|
# Name of the backend1 (must be declared in cinder.conf)
|
|
|
|
# (default: 'BACKEND_1')
|
|
|
|
# set in [cinder]->enabled_backends
|
|
|
|
config.set('volume', 'backend1_name', 'tripleo_iscsi')
|
|
|
|
|
|
|
|
# Update bootable status of a volume Not implemented on icehouse
|
|
|
|
# (default: false)
|
|
|
|
# python-cinderclient supports set-bootable
|
|
|
|
config.set('volume-feature-enabled', 'bootable', 'true')
|
|
|
|
|
2016-11-07 16:59:49 +01:00
|
|
|
# Fix region value because TripleO is using non-standard value
|
|
|
|
for section in ('compute', 'identity', 'image', 'network',
|
|
|
|
'object-storage', 'orchestration', 'volume'):
|
|
|
|
config.set(section, 'region', 'regionOne')
|
|
|
|
|
2015-09-18 10:15:30 -04:00
|
|
|
with open(config_name, 'w+') as config_file:
|
|
|
|
config.write(config_file)
|
|
|
|
|
|
|
|
|
2015-10-15 13:30:19 +13:00
|
|
|
def wait_for_stack_ready(orchestration_client, stack_name, marker=None,
|
2017-03-27 22:48:49 +00:00
|
|
|
action='CREATE', verbose=False):
|
2015-04-15 12:27:58 -04:00
|
|
|
"""Check the status of an orchestration stack
|
|
|
|
|
|
|
|
Get the status of an orchestration stack and check whether it is complete
|
|
|
|
or failed.
|
|
|
|
|
|
|
|
:param orchestration_client: Instance of Orchestration client
|
|
|
|
:type orchestration_client: heatclient.v1.client.Client
|
|
|
|
|
|
|
|
:param stack_name: Name or UUID of stack to retrieve
|
|
|
|
:type stack_name: string
|
|
|
|
|
2015-10-15 13:30:19 +13:00
|
|
|
:param marker: UUID of the last stack event before the current action
|
|
|
|
:type marker: string
|
2015-04-15 12:27:58 -04:00
|
|
|
|
2015-10-15 13:30:19 +13:00
|
|
|
:param action: Current action to check the stack for COMPLETE
|
|
|
|
:type action: string
|
2015-04-15 12:27:58 -04:00
|
|
|
|
2015-10-15 13:30:19 +13:00
|
|
|
:param verbose: Whether to print events
|
|
|
|
:type verbose: boolean
|
|
|
|
"""
|
|
|
|
stack = get_stack(orchestration_client, stack_name)
|
|
|
|
if not stack:
|
|
|
|
return False
|
2018-03-02 15:34:14 +01:00
|
|
|
stack_name = "%s/%s" % (stack.stack_name, stack.id)
|
2015-04-15 12:27:58 -04:00
|
|
|
|
2016-08-25 02:43:22 +02:00
|
|
|
if verbose:
|
|
|
|
out = sys.stdout
|
|
|
|
else:
|
|
|
|
out = open(os.devnull, "w")
|
2017-03-27 22:48:49 +00:00
|
|
|
stack_status, msg = event_utils.poll_for_events(
|
|
|
|
orchestration_client, stack_name, action=action,
|
|
|
|
poll_period=5, marker=marker, out=out, nested_depth=2)
|
2016-08-25 02:43:22 +02:00
|
|
|
print(msg)
|
|
|
|
return stack_status == '%s_COMPLETE' % action
|
2015-04-15 12:27:58 -04:00
|
|
|
|
2015-04-22 10:41:55 +01:00
|
|
|
|
2015-10-15 10:25:13 +02:00
|
|
|
def nodes_in_states(baremetal_client, states):
|
|
|
|
"""List the introspectable nodes with the right provision_states."""
|
|
|
|
nodes = baremetal_client.node.list(maintenance=False, associated=False)
|
|
|
|
return [node for node in nodes if node.provision_state in states]
|
|
|
|
|
|
|
|
|
2015-04-22 10:41:55 +01:00
|
|
|
def wait_for_provision_state(baremetal_client, node_uuid, provision_state,
|
|
|
|
loops=10, sleep=1):
|
2015-09-17 20:02:45 -04:00
|
|
|
"""Wait for a given Provisioning state in Ironic
|
2015-05-14 15:24:26 +01:00
|
|
|
|
|
|
|
Updating the provisioning state is an async operation, we
|
|
|
|
need to wait for it to be completed.
|
|
|
|
|
|
|
|
:param baremetal_client: Instance of Ironic client
|
|
|
|
:type baremetal_client: ironicclient.v1.client.Client
|
|
|
|
|
|
|
|
:param node_uuid: The Ironic node UUID
|
|
|
|
:type node_uuid: str
|
|
|
|
|
|
|
|
:param provision_state: The provisioning state name to wait for
|
|
|
|
:type provision_state: str
|
|
|
|
|
|
|
|
:param loops: How many times to loop
|
|
|
|
:type loops: int
|
|
|
|
|
|
|
|
:param sleep: How long to sleep between loops
|
|
|
|
:type sleep: int
|
2015-10-15 10:25:13 +02:00
|
|
|
|
|
|
|
:raises exceptions.StateTransitionFailed: if node.last_error is set
|
2015-05-14 15:24:26 +01:00
|
|
|
"""
|
2015-04-22 10:41:55 +01:00
|
|
|
|
2015-11-26 15:19:22 +01:00
|
|
|
for _l in range(0, loops):
|
2015-04-22 10:41:55 +01:00
|
|
|
|
|
|
|
node = baremetal_client.node.get(node_uuid)
|
|
|
|
|
2015-05-14 15:31:53 +01:00
|
|
|
if node is None:
|
|
|
|
# The node can't be found in ironic, so we don't need to wait for
|
|
|
|
# the provision state
|
2015-10-15 10:25:13 +02:00
|
|
|
return
|
2015-04-22 10:41:55 +01:00
|
|
|
if node.provision_state == provision_state:
|
2015-10-15 10:25:13 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
# node.last_error should be None after any successful operation
|
|
|
|
if node.last_error:
|
|
|
|
raise exceptions.StateTransitionFailed(
|
|
|
|
"Error transitioning node %(uuid)s to provision state "
|
|
|
|
"%(state)s: %(error)s. Now in state %(actual)s." % {
|
|
|
|
'uuid': node_uuid,
|
|
|
|
'state': provision_state,
|
|
|
|
'error': node.last_error,
|
|
|
|
'actual': node.provision_state
|
|
|
|
}
|
|
|
|
)
|
2015-04-22 10:41:55 +01:00
|
|
|
|
|
|
|
time.sleep(sleep)
|
|
|
|
|
2015-10-15 10:25:13 +02:00
|
|
|
raise exceptions.Timeout(
|
|
|
|
"Node %(uuid)s did not reach provision state %(state)s. "
|
|
|
|
"Now in state %(actual)s." % {
|
|
|
|
'uuid': node_uuid,
|
|
|
|
'state': provision_state,
|
|
|
|
'actual': node.provision_state
|
|
|
|
}
|
|
|
|
)
|
2015-04-22 10:41:55 +01:00
|
|
|
|
|
|
|
|
2015-05-14 15:28:13 +01:00
|
|
|
def set_nodes_state(baremetal_client, nodes, transition, target_state,
|
|
|
|
skipped_states=()):
|
|
|
|
"""Make all nodes available in the baremetal service for a deployment
|
|
|
|
|
|
|
|
For each node, make it available unless it is already available or active.
|
|
|
|
Available nodes can be used for a deployment and an active node is already
|
|
|
|
in use.
|
|
|
|
|
|
|
|
:param baremetal_client: Instance of Ironic client
|
|
|
|
:type baremetal_client: ironicclient.v1.client.Client
|
|
|
|
|
|
|
|
:param nodes: List of Baremetal Nodes
|
|
|
|
:type nodes: [ironicclient.v1.node.Node]
|
|
|
|
|
|
|
|
:param transition: The state to set for a node. The full list of states
|
|
|
|
can be found in ironic.common.states.
|
|
|
|
:type transition: string
|
|
|
|
|
|
|
|
:param target_state: The expected result state for a node. For example when
|
|
|
|
transitioning to 'manage' the result is 'manageable'
|
|
|
|
:type target_state: string
|
|
|
|
|
|
|
|
:param skipped_states: A set of states to skip, for example 'active' nodes
|
|
|
|
are already deployed and the state can't always be
|
|
|
|
changed.
|
|
|
|
:type skipped_states: iterable of strings
|
2015-10-15 10:25:13 +02:00
|
|
|
|
|
|
|
:param error_states: Node states treated as error for this transition
|
|
|
|
:type error_states: collection of strings
|
|
|
|
|
|
|
|
:param error_message: Optional message to append to an error message
|
|
|
|
:param error_message: str
|
|
|
|
|
|
|
|
:raises exceptions.StateTransitionFailed: if a node enters any of the
|
|
|
|
states in error_states
|
|
|
|
|
|
|
|
:raises exceptions.Timeout: if a node takes too long to reach target state
|
2015-05-14 15:28:13 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__ + ".set_nodes_state")
|
|
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
|
|
if node.provision_state in skipped_states:
|
|
|
|
continue
|
|
|
|
|
|
|
|
log.debug(
|
2016-01-25 17:05:22 +00:00
|
|
|
"Setting provision state from '{0}' to '{1}' for Node {2}"
|
2015-05-14 15:28:13 +01:00
|
|
|
.format(node.provision_state, transition, node.uuid))
|
|
|
|
|
|
|
|
baremetal_client.node.set_provision_state(node.uuid, transition)
|
2015-10-15 10:25:13 +02:00
|
|
|
try:
|
|
|
|
wait_for_provision_state(baremetal_client, node.uuid, target_state)
|
|
|
|
except exceptions.StateTransitionFailed as e:
|
|
|
|
log.error("FAIL: State transition failed for Node {0}. {1}"
|
|
|
|
.format(node.uuid, e))
|
|
|
|
except exceptions.Timeout as e:
|
|
|
|
log.error("FAIL: Timeout waiting for Node {0}. {1}"
|
|
|
|
.format(node.uuid, e))
|
|
|
|
yield node.uuid
|
2015-05-14 15:31:23 +01:00
|
|
|
|
|
|
|
|
2015-09-17 15:32:07 -04:00
|
|
|
def get_overcloud_endpoint(stack):
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'KeystoneURL':
|
|
|
|
return output['output_value']
|
|
|
|
|
|
|
|
|
|
|
|
def get_service_ips(stack):
|
|
|
|
service_ips = {}
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
service_ips[output['output_key']] = output['output_value']
|
|
|
|
return service_ips
|
|
|
|
|
|
|
|
|
2016-03-30 10:13:48 +00:00
|
|
|
def get_endpoint_map(stack):
|
|
|
|
endpoint_map = {}
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'EndpointMap':
|
|
|
|
endpoint_map = output['output_value']
|
|
|
|
break
|
|
|
|
return endpoint_map
|
|
|
|
|
|
|
|
|
2017-03-10 12:12:13 +01:00
|
|
|
def get_role_data(stack):
|
|
|
|
role_data = {}
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'RoleData':
|
|
|
|
for role in output['output_value']:
|
|
|
|
role_data[role] = output['output_value'][role]
|
|
|
|
return role_data
|
|
|
|
|
|
|
|
|
2017-07-20 17:13:22 +01:00
|
|
|
def get_role_config(stack):
|
|
|
|
role_data = {}
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'RoleConfig':
|
|
|
|
for role in output['output_value']:
|
|
|
|
role_data[role] = output['output_value'][role]
|
|
|
|
return role_data
|
|
|
|
|
|
|
|
|
2017-10-24 09:29:57 -04:00
|
|
|
def get_role_net_hostname_map(stack):
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'RoleNetHostnameMap':
|
|
|
|
return output['output_value']
|
|
|
|
|
|
|
|
|
|
|
|
def get_hosts_entry(stack):
|
|
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
|
|
if output['output_key'] == 'HostsEntry':
|
|
|
|
return output['output_value']
|
|
|
|
|
|
|
|
|
2016-03-30 10:13:48 +00:00
|
|
|
def get_endpoint(key, stack):
|
|
|
|
endpoint_map = get_endpoint_map(stack)
|
|
|
|
if endpoint_map:
|
|
|
|
return endpoint_map[key]['host']
|
|
|
|
else:
|
|
|
|
return get_service_ips(stack).get(key + 'Vip')
|
|
|
|
|
|
|
|
|
2015-09-17 15:32:07 -04:00
|
|
|
def get_stack(orchestration_client, stack_name):
|
|
|
|
"""Get the ID for the current deployed overcloud stack if it exists.
|
|
|
|
|
|
|
|
Caller is responsible for checking if return is None
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
stack = orchestration_client.stacks.get(stack_name)
|
|
|
|
return stack
|
|
|
|
except HTTPNotFound:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-05-14 15:46:51 +01:00
|
|
|
def remove_known_hosts(overcloud_ip):
|
|
|
|
"""For a given IP address remove SSH keys from the known_hosts file"""
|
|
|
|
|
|
|
|
known_hosts = os.path.expanduser("~/.ssh/known_hosts")
|
|
|
|
|
|
|
|
if os.path.exists(known_hosts):
|
2015-08-20 16:41:25 +02:00
|
|
|
command = ['ssh-keygen', '-R', overcloud_ip, '-f', known_hosts]
|
2015-05-14 15:46:51 +01:00
|
|
|
subprocess.check_call(command)
|
2015-05-14 17:50:32 +01:00
|
|
|
|
|
|
|
|
2015-06-03 06:25:58 -04:00
|
|
|
def run_shell(cmd):
|
2015-06-22 23:45:14 +02:00
|
|
|
return subprocess.call([cmd], shell=True)
|
2015-08-06 13:46:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
def all_unique(x):
|
|
|
|
"""Return True if the collection has no duplications."""
|
|
|
|
return len(set(x)) == len(x)
|
2015-07-30 11:28:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
def file_checksum(filepath):
|
|
|
|
"""Calculate md5 checksum on file
|
|
|
|
|
|
|
|
:param filepath: Full path to file (e.g. /home/stack/image.qcow2)
|
|
|
|
:type filepath: string
|
|
|
|
|
|
|
|
"""
|
2015-09-28 23:18:48 +03:00
|
|
|
if not os.path.isfile(filepath):
|
|
|
|
raise ValueError("The given file {0} is not a regular "
|
|
|
|
"file".format(filepath))
|
2015-07-30 11:28:21 +02:00
|
|
|
checksum = hashlib.md5()
|
|
|
|
with open(filepath, 'rb') as f:
|
2015-09-28 23:18:48 +03:00
|
|
|
while True:
|
|
|
|
fragment = f.read(65536)
|
|
|
|
if not fragment:
|
|
|
|
break
|
2015-07-30 11:28:21 +02:00
|
|
|
checksum.update(fragment)
|
|
|
|
return checksum.hexdigest()
|
2015-08-26 13:42:31 +02:00
|
|
|
|
|
|
|
|
2015-09-09 14:02:32 +02:00
|
|
|
def ensure_run_as_normal_user():
|
|
|
|
"""Check if the command runs under normal user (EUID!=0)"""
|
|
|
|
if os.geteuid() == 0:
|
|
|
|
raise exceptions.RootUserExecution(
|
|
|
|
'This command cannot run under root user.'
|
|
|
|
' Switch to a normal user.')
|
2015-11-26 15:19:22 +01:00
|
|
|
|
|
|
|
|
2018-02-14 18:29:57 -08:00
|
|
|
def get_deployment_user():
|
|
|
|
"""Return the user name which is used to deploy the cloud"""
|
|
|
|
return getpass.getuser()
|
|
|
|
|
|
|
|
|
2015-11-26 15:19:22 +01:00
|
|
|
def capabilities_to_dict(caps):
|
|
|
|
"""Convert the Node's capabilities into a dictionary."""
|
|
|
|
if not caps:
|
|
|
|
return {}
|
|
|
|
return dict([key.split(':', 1) for key in caps.split(',')])
|
|
|
|
|
|
|
|
|
|
|
|
def dict_to_capabilities(caps_dict):
|
|
|
|
"""Convert a dictionary into a string with the capabilities syntax."""
|
|
|
|
return ','.join(["%s:%s" % (key, value)
|
|
|
|
for key, value in caps_dict.items()
|
|
|
|
if value is not None])
|
|
|
|
|
|
|
|
|
|
|
|
def node_get_capabilities(node):
|
|
|
|
"""Get node capabilities."""
|
|
|
|
return capabilities_to_dict(node.properties.get('capabilities'))
|
|
|
|
|
|
|
|
|
|
|
|
def node_add_capabilities(bm_client, node, **updated):
|
|
|
|
"""Add or replace capabilities for a node."""
|
|
|
|
caps = node_get_capabilities(node)
|
|
|
|
caps.update(updated)
|
|
|
|
converted_caps = dict_to_capabilities(caps)
|
|
|
|
node.properties['capabilities'] = converted_caps
|
|
|
|
bm_client.node.update(node.uuid, [{'op': 'add',
|
|
|
|
'path': '/properties/capabilities',
|
|
|
|
'value': converted_caps}])
|
|
|
|
return caps
|
|
|
|
|
|
|
|
|
|
|
|
def assign_and_verify_profiles(bm_client, flavors,
|
|
|
|
assign_profiles=False, dry_run=False):
|
|
|
|
"""Assign and verify profiles for given flavors.
|
|
|
|
|
|
|
|
:param bm_client: ironic client instance
|
|
|
|
:param flavors: map flavor name -> (flavor object, required count)
|
|
|
|
:param assign_profiles: whether to allow assigning profiles to nodes
|
|
|
|
:param dry_run: whether to skip applying actual changes (only makes sense
|
|
|
|
if assign_profiles is True)
|
|
|
|
:returns: tuple (errors count, warnings count)
|
|
|
|
"""
|
|
|
|
log = logging.getLogger(__name__ + ".assign_and_verify_profiles")
|
|
|
|
predeploy_errors = 0
|
|
|
|
predeploy_warnings = 0
|
|
|
|
|
|
|
|
# nodes available for deployment and scaling (including active)
|
|
|
|
bm_nodes = {node.uuid: node
|
|
|
|
for node in bm_client.node.list(maintenance=False,
|
|
|
|
detail=True)
|
|
|
|
if node.provision_state in ('available', 'active')}
|
|
|
|
# create a pool of unprocessed nodes and record their capabilities
|
|
|
|
free_node_caps = {uu: node_get_capabilities(node)
|
|
|
|
for uu, node in bm_nodes.items()}
|
|
|
|
|
|
|
|
# TODO(dtantsur): use command-line arguments to specify the order in
|
|
|
|
# which profiles are processed (might matter for assigning profiles)
|
2016-02-05 11:17:38 +00:00
|
|
|
profile_flavor_used = False
|
2015-11-26 15:19:22 +01:00
|
|
|
for flavor_name, (flavor, scale) in flavors.items():
|
|
|
|
if not scale:
|
|
|
|
log.debug("Skipping verification of flavor %s because "
|
|
|
|
"none will be deployed", flavor_name)
|
|
|
|
continue
|
|
|
|
|
|
|
|
profile = flavor.get_keys().get('capabilities:profile')
|
2016-02-05 11:17:38 +00:00
|
|
|
# If there's only a single flavor, then it's expected for it to have
|
|
|
|
# no profile assigned.
|
|
|
|
if not profile and len(flavors) > 1:
|
2015-11-26 15:19:22 +01:00
|
|
|
predeploy_errors += 1
|
|
|
|
log.error(
|
|
|
|
'Error: The %s flavor has no profile associated', flavor_name)
|
|
|
|
log.error(
|
|
|
|
'Recommendation: assign a profile with openstack flavor '
|
|
|
|
'set --property "capabilities:profile"="PROFILE_NAME" %s',
|
|
|
|
flavor_name)
|
|
|
|
continue
|
|
|
|
|
2016-02-05 11:17:38 +00:00
|
|
|
profile_flavor_used = True
|
|
|
|
|
2015-11-26 15:19:22 +01:00
|
|
|
# first collect nodes with known profiles
|
|
|
|
assigned_nodes = [uu for uu, caps in free_node_caps.items()
|
|
|
|
if caps.get('profile') == profile]
|
|
|
|
required_count = scale - len(assigned_nodes)
|
|
|
|
|
|
|
|
if required_count < 0:
|
|
|
|
log.warning('%d nodes with profile %s won\'t be used '
|
|
|
|
'for deployment now', -required_count, profile)
|
|
|
|
predeploy_warnings += 1
|
|
|
|
required_count = 0
|
|
|
|
elif required_count > 0 and assign_profiles:
|
|
|
|
# find more nodes by checking XXX_profile capabilities that are
|
|
|
|
# set by ironic-inspector or manually
|
|
|
|
capability = '%s_profile' % profile
|
|
|
|
more_nodes = [
|
|
|
|
uu for uu, caps in free_node_caps.items()
|
|
|
|
# use only nodes without a know profile
|
2015-12-11 21:06:32 +01:00
|
|
|
if not caps.get('profile') and
|
|
|
|
caps.get(capability, '').lower() in ('1', 'true') and
|
2015-11-26 15:19:22 +01:00
|
|
|
# do not assign profiles for active nodes
|
2015-12-11 21:06:32 +01:00
|
|
|
bm_nodes[uu].provision_state == 'available'
|
2015-11-26 15:19:22 +01:00
|
|
|
][:required_count]
|
|
|
|
assigned_nodes.extend(more_nodes)
|
|
|
|
required_count -= len(more_nodes)
|
|
|
|
|
|
|
|
for uu in assigned_nodes:
|
|
|
|
# make sure these nodes are not reused for other profiles
|
|
|
|
node_caps = free_node_caps.pop(uu)
|
|
|
|
# save profile for newly assigned nodes, but only if we
|
|
|
|
# succeeded in finding enough of them
|
|
|
|
if not required_count and not node_caps.get('profile'):
|
|
|
|
node = bm_nodes[uu]
|
|
|
|
if not dry_run:
|
|
|
|
node_add_capabilities(bm_client, node, profile=profile)
|
|
|
|
log.info('Node %s was assigned profile %s', uu, profile)
|
|
|
|
else:
|
|
|
|
log.debug('Node %s has profile %s', uu, profile)
|
|
|
|
|
|
|
|
if required_count > 0:
|
|
|
|
log.error(
|
|
|
|
"Error: only %s of %s requested ironic nodes are tagged "
|
|
|
|
"to profile %s (for flavor %s)",
|
|
|
|
scale - required_count, scale, profile, flavor_name
|
|
|
|
)
|
|
|
|
log.error(
|
|
|
|
"Recommendation: tag more nodes using ironic node-update "
|
|
|
|
"<NODE ID> replace properties/capabilities=profile:%s,"
|
|
|
|
"boot_option:local", profile)
|
|
|
|
predeploy_errors += 1
|
|
|
|
|
|
|
|
nodes_without_profile = [uu for uu, caps in free_node_caps.items()
|
|
|
|
if not caps.get('profile')]
|
2016-02-05 11:17:38 +00:00
|
|
|
if nodes_without_profile and profile_flavor_used:
|
2015-11-26 15:19:22 +01:00
|
|
|
predeploy_warnings += 1
|
|
|
|
log.warning(
|
|
|
|
"There are %d ironic nodes with no profile that will "
|
|
|
|
"not be used: %s", len(nodes_without_profile),
|
|
|
|
', '.join(nodes_without_profile)
|
|
|
|
)
|
|
|
|
|
|
|
|
return predeploy_errors, predeploy_warnings
|
|
|
|
|
|
|
|
|
2016-11-01 14:35:10 +01:00
|
|
|
def add_deployment_plan_arguments(parser, mark_as_depr=False):
|
2015-11-26 15:19:22 +01:00
|
|
|
"""Add deployment plan arguments (flavors and scales) to a parser"""
|
2016-09-28 15:24:14 +01:00
|
|
|
|
2016-11-01 14:35:10 +01:00
|
|
|
depr_warning = _(' (DEPRECATED. Use an environment file and set the '
|
|
|
|
'parameter %s. It will be removed after the "P" '
|
|
|
|
'release.)')
|
|
|
|
|
2016-09-28 15:24:14 +01:00
|
|
|
# TODO(d0ugal): Deprecated in Newton. Remove these in P.
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--control-scale', type=int,
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('New number of control nodes.')
|
|
|
|
+ (depr_warning % 'ControllerCount'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--compute-scale', type=int,
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('New number of compute nodes.')
|
|
|
|
+ (depr_warning % 'ComputeCount'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--ceph-storage-scale', type=int,
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('New number of ceph storage nodes.')
|
|
|
|
+ (depr_warning % 'CephStorageCount'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--block-storage-scale', type=int,
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('New number of cinder storage nodes.')
|
|
|
|
+ (depr_warning % 'BlockStorageCount'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--swift-storage-scale', type=int,
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('New number of swift storage nodes.')
|
|
|
|
+ (depr_warning % 'ObjectStorageCount'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--control-flavor',
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('Nova flavor to use for control nodes.')
|
|
|
|
+ (depr_warning % 'OvercloudControlFlavor'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--compute-flavor',
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('Nova flavor to use for compute nodes.')
|
|
|
|
+ (depr_warning % 'OvercloudComputeFlavor'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--ceph-storage-flavor',
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('Nova flavor to use for ceph storage nodes.')
|
|
|
|
+ (depr_warning % 'OvercloudCephStorageFlavor'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--block-storage-flavor',
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('Nova flavor to use for cinder storage nodes')
|
|
|
|
+ (depr_warning % 'OvercloudBlockStorageFlavor'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
parser.add_argument('--swift-storage-flavor',
|
2016-11-01 14:35:10 +01:00
|
|
|
help=_('Nova flavor to use for swift storage nodes')
|
|
|
|
+ (depr_warning % 'OvercloudSwiftStorageFlavor'
|
|
|
|
if mark_as_depr else ''))
|
2015-11-26 15:19:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
def get_roles_info(parsed_args):
|
|
|
|
"""Get flavor name and scale for all deployment roles.
|
|
|
|
|
|
|
|
:returns: dict role name -> (flavor name, scale)
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
'control': (parsed_args.control_flavor, parsed_args.control_scale),
|
|
|
|
'compute': (parsed_args.compute_flavor, parsed_args.compute_scale),
|
|
|
|
'ceph-storage': (parsed_args.ceph_storage_flavor,
|
|
|
|
parsed_args.ceph_storage_scale),
|
|
|
|
'block-storage': (parsed_args.block_storage_flavor,
|
|
|
|
parsed_args.block_storage_scale),
|
|
|
|
'swift-storage': (parsed_args.swift_storage_flavor,
|
|
|
|
parsed_args.swift_storage_scale)
|
|
|
|
}
|
2016-07-08 11:19:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _csv_to_nodes_dict(nodes_csv):
|
|
|
|
"""Convert CSV to a list of dicts formatted for os_cloud_config
|
|
|
|
|
|
|
|
Given a CSV file in the format below, convert it into the
|
|
|
|
structure expected by os_cloud_config JSON files.
|
|
|
|
|
|
|
|
pm_type, pm_addr, pm_user, pm_password, mac
|
|
|
|
"""
|
|
|
|
|
|
|
|
data = []
|
|
|
|
|
|
|
|
for row in csv.reader(nodes_csv):
|
|
|
|
node = {
|
|
|
|
"pm_user": row[2],
|
|
|
|
"pm_addr": row[1],
|
|
|
|
"pm_password": row[3],
|
|
|
|
"pm_type": row[0],
|
|
|
|
"mac": [
|
|
|
|
row[4]
|
|
|
|
]
|
|
|
|
}
|
2017-02-01 15:08:20 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
node['pm_port'] = row[5]
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
|
2016-07-08 11:19:23 +00:00
|
|
|
data.append(node)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
def parse_env_file(env_file, file_type=None):
|
|
|
|
if file_type == 'json' or env_file.name.endswith('.json'):
|
2017-06-04 20:33:17 +02:00
|
|
|
nodes_config = simplejson.load(env_file)
|
2016-07-08 11:19:23 +00:00
|
|
|
elif file_type == 'csv' or env_file.name.endswith('.csv'):
|
|
|
|
nodes_config = _csv_to_nodes_dict(env_file)
|
|
|
|
elif env_file.name.endswith('.yaml'):
|
|
|
|
nodes_config = yaml.safe_load(env_file)
|
|
|
|
else:
|
|
|
|
raise exceptions.InvalidConfiguration(
|
|
|
|
_("Invalid file extension for %s, must be json, yaml or csv") %
|
|
|
|
env_file.name)
|
|
|
|
|
|
|
|
if 'nodes' in nodes_config:
|
|
|
|
nodes_config = nodes_config['nodes']
|
|
|
|
|
|
|
|
return nodes_config
|
2016-10-07 18:29:46 +02:00
|
|
|
|
|
|
|
|
2016-11-22 18:17:46 -07:00
|
|
|
def prompt_user_for_confirmation(message, logger, positive_response='y'):
|
|
|
|
"""Prompt user for a y/N confirmation
|
|
|
|
|
|
|
|
Use this function to prompt the user for a y/N confirmation
|
|
|
|
with the provided message. The [y/N] should be included in
|
|
|
|
the provided message to this function to indicate the expected
|
|
|
|
input for confirmation. You can customize the positive response if
|
|
|
|
y/N is not a desired input.
|
|
|
|
|
|
|
|
:param message: Confirmation string prompt
|
|
|
|
:param logger: logger object used to write info logs
|
|
|
|
:param positive_response: Beginning character for a positive user input
|
2017-09-05 16:00:18 +08:00
|
|
|
:return: boolean true for valid confirmation, false for all others
|
2016-11-22 18:17:46 -07:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if not sys.stdin.isatty():
|
2017-03-23 19:09:43 +08:00
|
|
|
logger.error(_('User interaction required, cannot confirm.'))
|
2016-11-22 18:17:46 -07:00
|
|
|
return False
|
|
|
|
else:
|
|
|
|
sys.stdout.write(message)
|
|
|
|
prompt_response = sys.stdin.readline().lower()
|
|
|
|
if not prompt_response.startswith(positive_response):
|
2017-03-23 19:09:43 +08:00
|
|
|
logger.info(_(
|
2016-11-22 18:17:46 -07:00
|
|
|
'User did not confirm action so taking no action.'))
|
|
|
|
return False
|
2017-03-23 19:09:43 +08:00
|
|
|
logger.info(_('User confirmed action.'))
|
2016-11-22 18:17:46 -07:00
|
|
|
return True
|
|
|
|
except KeyboardInterrupt: # ctrl-c
|
2017-03-23 19:09:43 +08:00
|
|
|
logger.info(_(
|
2016-11-22 18:17:46 -07:00
|
|
|
'User did not confirm action (ctrl-c) so taking no action.'))
|
|
|
|
except EOFError: # ctrl-d
|
2017-03-23 19:09:43 +08:00
|
|
|
logger.info(_(
|
2016-11-22 18:17:46 -07:00
|
|
|
'User did not confirm action (ctrl-d) so taking no action.'))
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2016-10-07 18:29:46 +02:00
|
|
|
def replace_links_in_template_contents(contents, link_replacement):
|
|
|
|
"""Replace get_file and type file links in Heat template contents
|
|
|
|
|
|
|
|
If the string contents passed in is a Heat template, scan the
|
|
|
|
template for 'get_file' and 'type' occurences, and replace the
|
|
|
|
file paths according to link_replacement dict. (Key/value in
|
|
|
|
link_replacement are from/to, respectively.)
|
|
|
|
|
|
|
|
If the string contents don't look like a Heat template, return the
|
|
|
|
contents unmodified.
|
|
|
|
"""
|
|
|
|
|
|
|
|
template = {}
|
|
|
|
try:
|
|
|
|
template = yaml.safe_load(contents)
|
|
|
|
except yaml.YAMLError:
|
|
|
|
return contents
|
|
|
|
|
|
|
|
if not (isinstance(template, dict) and
|
|
|
|
template.get('heat_template_version')):
|
|
|
|
return contents
|
|
|
|
|
|
|
|
template = replace_links_in_template(template, link_replacement)
|
|
|
|
|
|
|
|
return yaml.safe_dump(template)
|
|
|
|
|
|
|
|
|
|
|
|
def replace_links_in_template(template_part, link_replacement):
|
|
|
|
"""Replace get_file and type file links in a Heat template
|
|
|
|
|
|
|
|
Scan the template for 'get_file' and 'type' occurences, and
|
|
|
|
replace the file paths according to link_replacement
|
|
|
|
dict. (Key/value in link_replacement are from/to, respectively.)
|
|
|
|
"""
|
|
|
|
|
|
|
|
def replaced_dict_value(key, value):
|
|
|
|
if ((key == 'get_file' or key == 'type') and
|
|
|
|
isinstance(value, six.string_types)):
|
|
|
|
return link_replacement.get(value, value)
|
|
|
|
else:
|
|
|
|
return replace_links_in_template(value, link_replacement)
|
|
|
|
|
|
|
|
def replaced_list_value(value):
|
|
|
|
return replace_links_in_template(value, link_replacement)
|
|
|
|
|
|
|
|
if isinstance(template_part, dict):
|
|
|
|
return {k: replaced_dict_value(k, v)
|
|
|
|
for k, v in six.iteritems(template_part)}
|
|
|
|
elif isinstance(template_part, list):
|
|
|
|
return map(replaced_list_value, template_part)
|
|
|
|
else:
|
|
|
|
return template_part
|
|
|
|
|
|
|
|
|
|
|
|
def relative_link_replacement(link_replacement, current_dir):
|
|
|
|
"""Generate a relative version of link_replacement dictionary.
|
|
|
|
|
|
|
|
Get a link_replacement dictionary (where key/value are from/to
|
|
|
|
respectively), and make the values in that dictionary relative
|
|
|
|
paths with respect to current_dir.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return {k: os.path.relpath(v, current_dir)
|
|
|
|
for k, v in six.iteritems(link_replacement)}
|
2017-10-12 17:22:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
def load_environment_directories(directories):
|
|
|
|
log = logging.getLogger(__name__ + ".load_environment_directories")
|
|
|
|
|
|
|
|
if os.environ.get('TRIPLEO_ENVIRONMENT_DIRECTORY'):
|
|
|
|
directories.append(os.environ.get('TRIPLEO_ENVIRONMENT_DIRECTORY'))
|
|
|
|
|
|
|
|
environments = []
|
|
|
|
for d in directories:
|
|
|
|
if os.path.exists(d) and d != '.':
|
|
|
|
log.debug("Environment directory: %s" % d)
|
|
|
|
for f in sorted(glob.glob(os.path.join(d, '*.yaml'))):
|
|
|
|
log.debug("Environment directory file: %s" % f)
|
|
|
|
if os.path.isfile(f):
|
|
|
|
environments.append(f)
|
|
|
|
return environments
|
2018-02-22 18:21:09 +02:00
|
|
|
|
|
|
|
|
2018-03-29 18:05:21 +02:00
|
|
|
def get_tripleo_ansible_inventory(inventory_file='', ssh_user='heat-admin'):
|
2018-02-22 18:21:09 +02:00
|
|
|
if not inventory_file:
|
|
|
|
inventory_file = '%s/%s' % (os.path.expanduser('~'),
|
|
|
|
'tripleo-ansible-inventory.yaml')
|
|
|
|
try:
|
|
|
|
processutils.execute(
|
|
|
|
'/usr/bin/tripleo-ansible-inventory',
|
2018-03-29 18:05:21 +02:00
|
|
|
'--ansible_ssh_user', ssh_user,
|
2018-02-22 18:21:09 +02:00
|
|
|
'--static-yaml-inventory', inventory_file)
|
|
|
|
except processutils.ProcessExecutionError as e:
|
|
|
|
message = "Failed to generate inventory: %s" % str(e)
|
|
|
|
raise exceptions.InvalidConfiguration(message)
|
|
|
|
if os.path.exists(inventory_file):
|
|
|
|
inventory = open(inventory_file, 'r').read()
|
|
|
|
return inventory
|
|
|
|
else:
|
|
|
|
raise exceptions.InvalidConfiguration(
|
|
|
|
"Inventory file %s can not be found." % inventory_file)
|
2018-03-08 13:58:24 +02:00
|
|
|
|
|
|
|
|
2018-02-09 15:56:13 +01:00
|
|
|
def process_multiple_environments(created_env_files, tht_root,
|
|
|
|
user_tht_root, cleanup=True):
|
|
|
|
log = logging.getLogger(__name__ + ".process_multiple_environments")
|
|
|
|
env_files = {}
|
|
|
|
localenv = {}
|
|
|
|
# Normalize paths for full match checks
|
|
|
|
user_tht_root = os.path.normpath(user_tht_root)
|
|
|
|
tht_root = os.path.normpath(tht_root)
|
|
|
|
for env_path in created_env_files:
|
|
|
|
log.debug("Processing environment files %s" % env_path)
|
|
|
|
abs_env_path = os.path.abspath(env_path)
|
|
|
|
if (abs_env_path.startswith(user_tht_root) and
|
|
|
|
((user_tht_root + '/') in env_path or
|
|
|
|
(user_tht_root + '/') in abs_env_path or
|
|
|
|
user_tht_root == abs_env_path or
|
|
|
|
user_tht_root == env_path)):
|
|
|
|
new_env_path = env_path.replace(user_tht_root + '/',
|
|
|
|
tht_root + '/')
|
|
|
|
log.debug("Redirecting env file %s to %s"
|
|
|
|
% (abs_env_path, new_env_path))
|
|
|
|
env_path = new_env_path
|
|
|
|
try:
|
|
|
|
files, env = template_utils.process_environment_and_files(
|
|
|
|
env_path=env_path)
|
|
|
|
except hc_exc.CommandError as ex:
|
|
|
|
# This provides fallback logic so that we can reference files
|
|
|
|
# inside the resource_registry values that may be rendered via
|
|
|
|
# j2.yaml templates, where the above will fail because the
|
|
|
|
# file doesn't exist in user_tht_root, but it is in tht_root
|
|
|
|
# See bug https://bugs.launchpad.net/tripleo/+bug/1625783
|
|
|
|
# for details on why this is needed (backwards-compatibility)
|
|
|
|
log.debug("Error %s processing environment file %s"
|
|
|
|
% (six.text_type(ex), env_path))
|
|
|
|
# Use the temporary path as it's possible the environment
|
|
|
|
# itself was rendered via jinja.
|
|
|
|
with open(env_path, 'r') as f:
|
|
|
|
env_map = yaml.safe_load(f)
|
|
|
|
env_registry = env_map.get('resource_registry', {})
|
|
|
|
env_dirname = os.path.dirname(os.path.abspath(env_path))
|
|
|
|
for rsrc, rsrc_path in six.iteritems(env_registry):
|
|
|
|
# We need to calculate the absolute path relative to
|
|
|
|
# env_path not cwd (which is what abspath uses).
|
|
|
|
abs_rsrc_path = os.path.normpath(
|
|
|
|
os.path.join(env_dirname, rsrc_path))
|
|
|
|
# If the absolute path matches user_tht_root, rewrite
|
|
|
|
# a temporary environment pointing at tht_root instead
|
|
|
|
if (abs_rsrc_path.startswith(user_tht_root) and
|
|
|
|
((user_tht_root + '/') in abs_rsrc_path or
|
|
|
|
abs_rsrc_path == user_tht_root)):
|
|
|
|
new_rsrc_path = abs_rsrc_path.replace(
|
|
|
|
user_tht_root + '/', tht_root + '/')
|
|
|
|
log.debug("Rewriting %s %s path to %s"
|
|
|
|
% (env_path, rsrc, new_rsrc_path))
|
|
|
|
env_registry[rsrc] = new_rsrc_path
|
|
|
|
else:
|
|
|
|
# Skip any resources that are mapping to OS::*
|
|
|
|
# resource names as these aren't paths
|
|
|
|
if not rsrc_path.startswith("OS::"):
|
|
|
|
env_registry[rsrc] = abs_rsrc_path
|
|
|
|
env_map['resource_registry'] = env_registry
|
|
|
|
f_name = os.path.basename(os.path.splitext(abs_env_path)[0])
|
|
|
|
with tempfile.NamedTemporaryFile(dir=tht_root,
|
|
|
|
prefix="env-%s-" % f_name,
|
|
|
|
suffix=".yaml",
|
|
|
|
mode="w",
|
|
|
|
delete=cleanup) as f:
|
|
|
|
log.debug("Rewriting %s environment to %s"
|
|
|
|
% (env_path, f.name))
|
|
|
|
f.write(yaml.safe_dump(env_map, default_flow_style=False))
|
|
|
|
f.flush()
|
|
|
|
files, env = template_utils.process_environment_and_files(
|
|
|
|
env_path=f.name)
|
|
|
|
if files:
|
|
|
|
log.debug("Adding files %s for %s" % (files, env_path))
|
|
|
|
env_files.update(files)
|
|
|
|
|
|
|
|
# 'env' can be a deeply nested dictionary, so a simple update is
|
|
|
|
# not enough
|
|
|
|
localenv = template_utils.deep_update(localenv, env)
|
|
|
|
return env_files, localenv
|
|
|
|
|
|
|
|
|
2018-03-08 13:58:24 +02:00
|
|
|
def run_update_ansible_action(log, clients, nodes, inventory, playbook,
|
2018-03-29 18:05:21 +02:00
|
|
|
queue, all_playbooks, action, ssh_user,
|
|
|
|
skip_tags=''):
|
2018-03-08 13:58:24 +02:00
|
|
|
playbooks = [playbook]
|
|
|
|
if playbook == "all":
|
|
|
|
playbooks = all_playbooks
|
|
|
|
for book in playbooks:
|
|
|
|
log.debug("Running ansible playbook %s " % book)
|
|
|
|
action.update_ansible(clients, nodes=nodes, inventory_file=inventory,
|
2018-03-12 13:11:03 +02:00
|
|
|
playbook=book, ansible_queue_name=queue,
|
2018-03-29 18:05:21 +02:00
|
|
|
node_user=ssh_user, skip_tags=skip_tags)
|