module tripleo_baremetal_check_existing

This module takes a list of instances and returns a list of uuids for
instances which exist, and a list of instances which were not found.

Change-Id: I056b346ae7ff55ab6e27c3d3248820eca13dda8d
Story: 2007212
Task: 38457
This commit is contained in:
Steve Baker 2020-02-14 03:32:01 +00:00 committed by Kevin Carter (cloudnull)
parent 603660efe8
commit c68275cf70
4 changed files with 308 additions and 0 deletions

View File

@ -0,0 +1,14 @@
=========================================
Module - tripleo_baremetal_check_existing
=========================================
This module provides for the following ansible plugin:
* tripleo_baremetal_check_existing
.. ansibleautoplugin::
:module: tripleo_ansible/ansible_plugins/modules/tripleo_baremetal_check_existing.py
:documentation: true
:examples: true

View File

@ -17,6 +17,7 @@
import jsonschema
import metalsmith
from metalsmith import sources
@ -229,6 +230,58 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
return instances, env
def check_existing(instances, provisioner, baremetal):
validate_instances(instances)
# Due to the name shadowing we should import other way
import importlib
sdk = importlib.import_module('openstack')
not_found = []
found = []
for request in instances:
ident = request.get('name', request['hostname'])
try:
instance = provisioner.show_instance(ident)
# TODO(dtantsur): replace Error with a specific exception
except (sdk.exceptions.ResourceNotFound,
metalsmith.exceptions.Error):
not_found.append(request)
except Exception as exc:
message = ('Failed to request instance information for %s'
% ident)
raise BaremetalDeployException(
"%s. %s: %s" % (message, type(exc).__name__, exc)
)
else:
# NOTE(dtantsur): metalsmith can match instances by node names,
# provide a safeguard to avoid conflicts.
if (instance.hostname
and instance.hostname != request['hostname']):
error = ("Requested hostname %s was not found, but the "
"deployed node %s has a matching name. Refusing "
"to proceed to avoid confusing results. Please "
"either rename the node or use a different "
"hostname") % (request['hostname'], instance.uuid)
raise BaremetalDeployException(error)
if (not instance.allocation
and instance.state == metalsmith.InstanceState.ACTIVE
and 'name' in request):
# Existing node is missing an allocation record,
# so create one without triggering allocation
baremetal.create_allocation(
resource_class=request.get('resource_class', 'baremetal'),
name=request['hostname'],
node=request['name']
)
found.append(instance)
return found, not_found
def build_hostname_format(hostname_format, role_name):
if not hostname_format:
hostname_format = '%stackname%-{}-%index%'.format(

View File

@ -0,0 +1,148 @@
#!/usr/bin/python
# Copyright 2020 Red Hat, Inc.
# All Rights Reserved.
#
# 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 absolute_import
__metaclass__ = type
from ansible.module_utils import baremetal_deploy as bd
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.openstack import openstack_cloud_from_module
from ansible.module_utils.openstack import openstack_full_argument_spec
from ansible.module_utils.openstack import openstack_module_kwargs
import metalsmith
import yaml
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: tripleo_baremetal_check_existing
short_description: Given a list of instances, build a list of found and
not found instances
version_added: "2.9"
author: "Steve Baker (@stevebaker)"
description:
- Takes a baremetal deployment description of roles and node instances
and transforms that into an instance list and a heat environment file
for deployed-server.
options:
instances:
description:
- List of instances to be filtered into found and not found.
Only the name and hostname are used for finding.
required: true
type: list
elements: dict
'''
RETURN = '''
instances:
description: List of instances which actually exist
returned: changed
type: list
sample: [
{
"hostname": "overcloud-controller-0",
"image": {
"href": "overcloud-full"
}
},
{
"hostname": "overcloud-controller-1",
"image": {
"href": "overcloud-full"
}
}
]
not_found:
description: List of instances which were not found
returned: changed
type: list
sample: [
{
"hostname": "overcloud-controller-2",
"image": {
"href": "overcloud-full"
}
}
]
'''
EXAMPLES = '''
- name: Find existing instances
tripleo_baremetal_check_existing:
instances:
- name: node-1
hostname: overcloud-controller-0
- name: node-2
hostname: overcloud-novacompute-0
register: tripleo_baremetal_existing
'''
def main():
argument_spec = openstack_full_argument_spec(
**yaml.safe_load(DOCUMENTATION)['options']
)
module_kwargs = openstack_module_kwargs()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
**module_kwargs
)
sdk, cloud = openstack_cloud_from_module(module)
provisioner = metalsmith.Provisioner(cloud_region=cloud.config)
try:
found, not_found = bd.check_existing(
instances=module.params['instances'],
provisioner=provisioner,
baremetal=cloud.baremetal
)
msg = ''
if found:
msg += ('Found existing instances: %s. '
% ', '.join([i.uuid for i in found]))
if not_found:
msg += ('Instance(s) %s do not exist. '
% ', '.join(r['hostname'] for r in not_found))
instances = [{
'name': i.node.name or i.uuid,
'hostname': i.hostname,
'id': i.uuid,
} for i in found]
module.exit_json(
changed=False,
msg=msg,
instances=instances,
not_found=not_found
)
except Exception as e:
module.fail_json(msg=str(e))
if __name__ == '__main__':
main()

View File

@ -13,8 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import metalsmith
import mock
from openstack import exceptions as sdk_exc
from tripleo_ansible.tests import base
# load baremetal_deploy so the next import works
base.load_module_utils('baremetal_deploy')
@ -670,3 +675,91 @@ class TestExpandRoles(base.TestCase):
self.assertIn('Compute: number of instance entries 4 '
'cannot be greater than count 3',
str(exc))
class TestCheckExistingInstances(base.TestCase):
def test_success(self):
pr = mock.Mock()
baremetal = mock.Mock()
instances = [
{'hostname': 'host1',
'image': {'href': 'overcloud-full'}},
{'hostname': 'host3',
'image': {'href': 'overcloud-full'}},
{'hostname': 'host2', 'resource_class': 'compute',
'capabilities': {'answer': '42'},
'image': {'href': 'overcloud-full'}}
]
existing = mock.MagicMock(hostname='host2', allocation=None)
existing.uuid = 'aaaa'
pr.show_instance.side_effect = [
sdk_exc.ResourceNotFound(""),
metalsmith.exceptions.Error(""),
existing,
]
found, not_found = bd.check_existing(instances, pr, baremetal)
self.assertEqual([existing], found)
self.assertEqual([{
'hostname': 'host1',
'image': {'href': 'overcloud-full'},
}, {
'hostname': 'host3',
'image': {'href': 'overcloud-full'},
}], not_found)
pr.show_instance.assert_has_calls([
mock.call(host) for host in ['host1', 'host3', 'host2']
])
def test_existing_no_allocation(self):
pr = mock.Mock()
baremetal = mock.Mock()
instances = [
{'name': 'server2', 'resource_class': 'compute',
'hostname': 'host2',
'capabilities': {'answer': '42'},
'image': {'href': 'overcloud-full'}}
]
existing = mock.MagicMock(
hostname='host2', allocation=None,
state=metalsmith.InstanceState.ACTIVE)
existing.uuid = 'aaaa'
pr.show_instance.return_value = existing
found, not_found = bd.check_existing(instances, pr, baremetal)
baremetal.create_allocation.assert_called_once_with(
name='host2', node='server2', resource_class='compute')
self.assertEqual([], not_found)
self.assertEqual([existing], found)
pr.show_instance.assert_called_once_with('server2')
def test_hostname_mismatch(self):
pr = mock.Mock()
instances = [
{'hostname': 'host1',
'image': {'href': 'overcloud-full'}},
]
pr.show_instance.return_value.hostname = 'host2'
exc = self.assertRaises(
bd.BaremetalDeployException, bd.check_existing,
instances, pr, mock.Mock())
self.assertIn("hostname host1 was not found", str(exc))
pr.show_instance.assert_called_once_with('host1')
def test_unexpected_error(self):
pr = mock.Mock()
instances = [
{'image': {'href': 'overcloud-full'},
'hostname': 'host%d' % i} for i in range(3)
]
pr.show_instance.side_effect = RuntimeError('boom')
exc = self.assertRaises(
bd.BaremetalDeployException, bd.check_existing,
instances, pr, mock.Mock())
self.assertIn("for host0", str(exc))
self.assertIn("RuntimeError: boom", str(exc))
pr.show_instance.assert_called_once_with('host0')