Make validation inputs configurable via Mistral

The validations have certain values (e.g. the amount of RAM necessary
for the undercloud node) configurable, but these were not exposed
through Mistral.

This adds a new `--inputs` parameter to the `run-validation` script
which takes a path to a YAML or JSON file with the additional
inputs (i.e. Ansible extra-args) defined.

And the `run_validation` action now optionally takes an `inputs`
dictionary which gets passed to `run-validation`.

Closes-Bug: #1625547
Implements: blueprint validation-framework
Signed-off-by: Gael Chamoulaud <gchamoul@redhat.com>

Change-Id: I8944cf7133d47869d26974fd123cd93c98425f17
Co-authored: Tomas Sedovic <tsedovic@redhat.com>
This commit is contained in:
Gael Chamoulaud 2019-03-19 16:45:05 +01:00
parent 3db41939a3
commit 5762772fdf
7 changed files with 94 additions and 9 deletions

View File

@ -3,6 +3,27 @@
set -e set -e
set -o pipefail set -o pipefail
SCRIPT_NAME=$(basename $0)
OPTS=`getopt -o i: -l inputs: -n $SCRIPT_NAME -- "$@"`
if [ $? != 0 ]; then
echo "Terminating..." >&2
exit 1
fi
EXTRA_VARS_FILE=""
# Note the quotes around `$OPTS': they are essential!
eval set -- "$OPTS"
while true ; do
case "$1" in
-i | --inputs ) EXTRA_VARS_FILE="$2" ; shift 2 ;;
-- ) shift ; break ;;
* ) echo "Error: unsupported option $1." ; exit 1 ;;
esac
done
VALIDATION_FILE=$1 VALIDATION_FILE=$1
IDENTITY_FILE=$2 IDENTITY_FILE=$2
PLAN_NAME=$3 PLAN_NAME=$3
@ -22,6 +43,15 @@ if [[ -z "$IDENTITY_FILE" ]]; then
exit 1 exit 1
fi fi
if [[ -z "$EXTRA_VARS_FILE" ]]; then
EXTRA_VARS_ARGS=""
elif [[ -r "$EXTRA_VARS_FILE" ]]; then
EXTRA_VARS_ARGS="--extra-vars @$EXTRA_VARS_FILE"
else
echo "Can not find the inputs file at $EXTRA_VARS_FILE"
exit 1
fi
# Make sure ssh is not asking interactively for hosts it can't check the key # Make sure ssh is not asking interactively for hosts it can't check the key
# authenticity # authenticity
export ANSIBLE_HOST_KEY_CHECKING=False export ANSIBLE_HOST_KEY_CHECKING=False
@ -55,4 +85,4 @@ if [ -z "${OS_PASSWORD:-}" ]; then
export OS_AUTH_URL=${OS_AUTH_URL/%v3/v2.0} export OS_AUTH_URL=${OS_AUTH_URL/%v3/v2.0}
fi fi
ansible-playbook $VALIDATION_FILE ansible-playbook $EXTRA_VARS_ARGS $VALIDATION_FILE

View File

@ -4,8 +4,13 @@ Defaults:mistral !requiretty
mistral ALL = (validations) NOPASSWD:SETENV: /usr/bin/run-validation mistral ALL = (validations) NOPASSWD:SETENV: /usr/bin/run-validation
mistral ALL = NOPASSWD: /usr/bin/chown -h validations\: /tmp/validations_identity_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \ mistral ALL = NOPASSWD: /usr/bin/chown -h validations\: /tmp/validations_identity_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \
!/usr/bin/chown /tmp/validations_identity_* *, !/usr/bin/chown /tmp/validations_identity_*..* !/usr/bin/chown /tmp/validations_identity_* *, !/usr/bin/chown /tmp/validations_identity_*..*
mistral ALL = NOPASSWD: /usr/bin/chown -h validations\: /tmp/validations_inputs_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \
/usr/bin/chown validations\: /tmp/validations_inputs_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \
!/usr/bin/chown /tmp/validations_inputs_* *, !/usr/bin/chown /tmp/validations_inputs_*..*
mistral ALL = NOPASSWD: /usr/bin/rm -f /tmp/validations_identity_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \ mistral ALL = NOPASSWD: /usr/bin/rm -f /tmp/validations_identity_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \
!/usr/bin/rm /tmp/validations_identity_* *, !/usr/bin/rm /tmp/validations_identity_*..* !/usr/bin/rm /tmp/validations_identity_* *, !/usr/bin/rm /tmp/validations_identity_*..*
mistral ALL = NOPASSWD: /usr/bin/rm -f /tmp/validations_inputs_[A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_][A-Za-z0-9_], \
!/usr/bin/rm /tmp/validations_inputs_* *, !/usr/bin/rm /tmp/validations_inputs_*..*
mistral ALL = NOPASSWD: /bin/nova-manage cell_v2 discover_hosts * mistral ALL = NOPASSWD: /bin/nova-manage cell_v2 discover_hosts *
mistral ALL = NOPASSWD: /usr/bin/tar --xattrs --ignore-failed-read -C / -cf /var/tmp/undercloud-backup-*.tar * mistral ALL = NOPASSWD: /usr/bin/tar --xattrs --ignore-failed-read -C / -cf /var/tmp/undercloud-backup-*.tar *
mistral ALL = NOPASSWD: /usr/bin/chown mistral. /var/tmp/undercloud-backup-*/filesystem-*.tar mistral ALL = NOPASSWD: /usr/bin/chown mistral. /var/tmp/undercloud-backup-*/filesystem-*.tar

View File

@ -121,16 +121,19 @@ class ListGroupsAction(base.TripleOAction):
class RunValidationAction(base.TripleOAction): class RunValidationAction(base.TripleOAction):
"""Run the given validation""" """Run the given validation"""
def __init__(self, validation, plan=constants.DEFAULT_CONTAINER_NAME): def __init__(self, validation, plan=constants.DEFAULT_CONTAINER_NAME,
inputs=None):
super(RunValidationAction, self).__init__() super(RunValidationAction, self).__init__()
self.validation = validation self.validation = validation
self.plan = plan self.plan = plan
self.inputs = inputs if inputs else {}
def run(self, context): def run(self, context):
mc = self.get_workflow_client(context) mc = self.get_workflow_client(context)
swift = self.get_object_client(context) swift = self.get_object_client(context)
identity_file = None identity_file = None
inputs_file = None
# Make sure the ssh_keys environment exists # Make sure the ssh_keys environment exists
try: try:
@ -146,11 +149,13 @@ class RunValidationAction(base.TripleOAction):
try: try:
private_key = env.variables['private_key'] private_key = env.variables['private_key']
identity_file = utils.write_identity_file(private_key) identity_file = utils.write_identity_file(private_key)
inputs_file = utils.write_inputs_file(self.inputs)
stdout, stderr = utils.run_validation(swift, stdout, stderr = utils.run_validation(swift,
self.validation, self.validation,
identity_file, identity_file,
self.plan, self.plan,
inputs_file,
context) context)
return_value = {'stdout': stdout, 'stderr': stderr} return_value = {'stdout': stdout, 'stderr': stderr}
mistral_result = {"data": return_value} mistral_result = {"data": return_value}
@ -164,6 +169,8 @@ class RunValidationAction(base.TripleOAction):
finally: finally:
if identity_file: if identity_file:
utils.cleanup_identity_file(identity_file) utils.cleanup_identity_file(identity_file)
if inputs_file:
utils.cleanup_inputs_file(inputs_file)
return actions.Result(**mistral_result) return actions.Result(**mistral_result)

View File

@ -165,11 +165,17 @@ class RunValidationActionTest(base.TestCase):
@mock.patch( @mock.patch(
'tripleo_common.actions.base.TripleOAction.get_workflow_client') 'tripleo_common.actions.base.TripleOAction.get_workflow_client')
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client') @mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
@mock.patch('tripleo_common.utils.validations.write_inputs_file')
@mock.patch('tripleo_common.utils.validations.cleanup_inputs_file')
@mock.patch('tripleo_common.utils.validations.write_identity_file') @mock.patch('tripleo_common.utils.validations.write_identity_file')
@mock.patch('tripleo_common.utils.validations.cleanup_identity_file') @mock.patch('tripleo_common.utils.validations.cleanup_identity_file')
@mock.patch('tripleo_common.utils.validations.run_validation') @mock.patch('tripleo_common.utils.validations.run_validation')
def test_run(self, mock_run_validation, mock_cleanup_identity_file, def test_run(self, mock_run_validation,
mock_write_identity_file, mock_get_object_client, mock_cleanup_identity_file,
mock_write_identity_file,
mock_cleanup_inputs_file,
mock_write_inputs_file,
mock_get_object_client,
get_workflow_client_mock): get_workflow_client_mock):
mock_ctx = mock.MagicMock() mock_ctx = mock.MagicMock()
mistral = mock.MagicMock() mistral = mock.MagicMock()
@ -181,6 +187,7 @@ class RunValidationActionTest(base.TestCase):
swiftclient = mock.MagicMock(url='http://swift:8080/v1/AUTH_test') swiftclient = mock.MagicMock(url='http://swift:8080/v1/AUTH_test')
mock_get_object_client.return_value = swiftclient mock_get_object_client.return_value = swiftclient
mock_write_identity_file.return_value = 'identity_file_path' mock_write_identity_file.return_value = 'identity_file_path'
mock_write_inputs_file.return_value = 'inputs_file_path'
mock_run_validation.return_value = 'output', 'error' mock_run_validation.return_value = 'output', 'error'
action = validations.RunValidationAction('validation') action = validations.RunValidationAction('validation')
expected = actions.Result( expected = actions.Result(
@ -196,18 +203,26 @@ class RunValidationActionTest(base.TestCase):
'validation', 'validation',
'identity_file_path', 'identity_file_path',
constants.DEFAULT_CONTAINER_NAME, constants.DEFAULT_CONTAINER_NAME,
'inputs_file_path',
mock_ctx) mock_ctx)
mock_cleanup_identity_file.assert_called_once_with( mock_cleanup_identity_file.assert_called_once_with(
'identity_file_path') 'identity_file_path')
mock_cleanup_inputs_file.assert_called_once_with('inputs_file_path')
@mock.patch( @mock.patch(
'tripleo_common.actions.base.TripleOAction.get_workflow_client') 'tripleo_common.actions.base.TripleOAction.get_workflow_client')
@mock.patch('tripleo_common.utils.validations.write_inputs_file')
@mock.patch('tripleo_common.utils.validations.cleanup_inputs_file')
@mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client') @mock.patch('tripleo_common.actions.base.TripleOAction.get_object_client')
@mock.patch('tripleo_common.utils.validations.write_identity_file') @mock.patch('tripleo_common.utils.validations.write_identity_file')
@mock.patch('tripleo_common.utils.validations.cleanup_identity_file') @mock.patch('tripleo_common.utils.validations.cleanup_identity_file')
@mock.patch('tripleo_common.utils.validations.run_validation') @mock.patch('tripleo_common.utils.validations.run_validation')
def test_run_failing(self, mock_run_validation, mock_cleanup_identity_file, def test_run_failing(self, mock_run_validation,
mock_write_identity_file, mock_get_object_client, mock_cleanup_identity_file,
mock_write_identity_file,
mock_get_object_client,
mock_cleanup_inputs_file,
mock_write_inputs_file,
get_workflow_client_mock): get_workflow_client_mock):
mock_ctx = mock.MagicMock() mock_ctx = mock.MagicMock()
mistral = mock.MagicMock() mistral = mock.MagicMock()
@ -219,6 +234,7 @@ class RunValidationActionTest(base.TestCase):
swiftclient = mock.MagicMock(url='http://swift:8080/v1/AUTH_test') swiftclient = mock.MagicMock(url='http://swift:8080/v1/AUTH_test')
mock_get_object_client.return_value = swiftclient mock_get_object_client.return_value = swiftclient
mock_write_identity_file.return_value = 'identity_file_path' mock_write_identity_file.return_value = 'identity_file_path'
mock_write_inputs_file.return_value = 'inputs_file_path'
mock_run_validation.side_effect = ProcessExecutionError( mock_run_validation.side_effect = ProcessExecutionError(
stdout='output', stderr='error') stdout='output', stderr='error')
action = validations.RunValidationAction('validation') action = validations.RunValidationAction('validation')
@ -235,6 +251,8 @@ class RunValidationActionTest(base.TestCase):
'validation', 'validation',
'identity_file_path', 'identity_file_path',
constants.DEFAULT_CONTAINER_NAME, constants.DEFAULT_CONTAINER_NAME,
'inputs_file_path',
mock_ctx) mock_ctx)
mock_cleanup_identity_file.assert_called_once_with( mock_cleanup_identity_file.assert_called_once_with(
'identity_file_path') 'identity_file_path')
mock_cleanup_inputs_file.assert_called_once_with('inputs_file_path')

View File

@ -249,7 +249,7 @@ class RunValidationTest(base.TestCase):
result = validations.run_validation(mock_get_object_client(), result = validations.run_validation(mock_get_object_client(),
'validation', 'identity_file', 'validation', 'identity_file',
'plan', mock_ctx) 'plan', 'inputs_file', mock_ctx)
self.assertEqual('output', result) self.assertEqual('output', result)
mock_execute.assert_called_once_with( mock_execute.assert_called_once_with(
'/usr/bin/sudo', '-u', 'validations', '/usr/bin/sudo', '-u', 'validations',
@ -258,6 +258,7 @@ class RunValidationTest(base.TestCase):
'OS_AUTH_TOKEN=auth_token', 'OS_AUTH_TOKEN=auth_token',
'OS_TENANT_NAME=project_name', 'OS_TENANT_NAME=project_name',
'/usr/bin/run-validation', '/usr/bin/run-validation',
'--inputs', 'inputs_file',
'validation_path', 'validation_path',
'identity_file', 'identity_file',
'plan' 'plan'

View File

@ -140,7 +140,8 @@ def download_validation(swift, plan, validation):
return dst_path return dst_path
def run_validation(swift, validation, identity_file, plan, context): def run_validation(swift, validation, identity_file,
plan, inputs_file, context):
return processutils.execute( return processutils.execute(
'/usr/bin/sudo', '-u', 'validations', '/usr/bin/sudo', '-u', 'validations',
'OS_AUTH_URL={}'.format(context.auth_uri), 'OS_AUTH_URL={}'.format(context.auth_uri),
@ -148,6 +149,7 @@ def run_validation(swift, validation, identity_file, plan, context):
'OS_AUTH_TOKEN={}'.format(context.auth_token), 'OS_AUTH_TOKEN={}'.format(context.auth_token),
'OS_TENANT_NAME={}'.format(context.project_name), 'OS_TENANT_NAME={}'.format(context.project_name),
'/usr/bin/run-validation', '/usr/bin/run-validation',
'--inputs', inputs_file,
download_validation(swift, plan, validation), download_validation(swift, plan, validation),
identity_file, identity_file,
plan plan
@ -176,3 +178,20 @@ def pattern_validator(pattern, value):
if not re.match(pattern, value): if not re.match(pattern, value):
return False return False
return True return True
def write_inputs_file(inputs):
"""Serialise the validation inputs to a file on disk."""
fd, path = tempfile.mkstemp(prefix='validation_inputs_')
LOG.debug("Writing the validation inputs to %s", path)
with os.fdopen(fd, 'w') as tmp:
tmp.write(yaml.dump(inputs))
processutils.execute('/usr/bin/sudo', '/usr/bin/chown', 'validations:',
path)
return path
def cleanup_inputs_file(path):
"""Remove the temporary validation inputs file."""
LOG.debug("Cleaning up the validation inputs at %s", path)
processutils.execute('/usr/bin/sudo', '/usr/bin/rm', '-f', path)

View File

@ -9,6 +9,7 @@ workflows:
input: input:
- validation_name - validation_name
- plan: overcloud - plan: overcloud
- validation_inputs: {}
- queue_name: tripleo - queue_name: tripleo
tags: tags:
@ -32,7 +33,7 @@ workflows:
run_validation: run_validation:
on-success: send_message on-success: send_message
on-error: set_status_failed on-error: set_status_failed
action: tripleo.validations.run_validation validation=<% $.validation_name %> plan=<% $.plan %> action: tripleo.validations.run_validation validation=<% $.validation_name %> plan=<% $.plan %> inputs=<% $.validation_inputs %>
publish: publish:
status: SUCCESS status: SUCCESS
validation: <% $.validation_name %> validation: <% $.validation_name %>
@ -65,6 +66,7 @@ workflows:
input: input:
- validation_names: [] - validation_names: []
- plan: overcloud - plan: overcloud
- validation_inputs: {}
- queue_name: tripleo - queue_name: tripleo
tags: tags:
@ -91,6 +93,7 @@ workflows:
workflow: tripleo.validations.v1.run_validation workflow: tripleo.validations.v1.run_validation
input: input:
validation_name: <% $.validation %> validation_name: <% $.validation %>
validation_inputs: <% $.validation_inputs %>
plan: <% $.plan %> plan: <% $.plan %>
queue_name: <% $.queue_name %> queue_name: <% $.queue_name %>
with-items: validation in <% $.validation_names %> with-items: validation in <% $.validation_names %>
@ -121,6 +124,7 @@ workflows:
input: input:
- group_names: [] - group_names: []
- plan: overcloud - plan: overcloud
- validation_inputs: {}
- queue_name: tripleo - queue_name: tripleo
tags: tags:
@ -154,6 +158,7 @@ workflows:
workflow: tripleo.validations.v1.run_validation workflow: tripleo.validations.v1.run_validation
input: input:
validation_name: <% $.validation %> validation_name: <% $.validation %>
validation_inputs: <% $.validation_inputs %>
plan: <% $.plan %> plan: <% $.plan %>
queue_name: <% $.queue_name %> queue_name: <% $.queue_name %>
with-items: validation in <% $.validations.id %> with-items: validation in <% $.validations.id %>