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:
parent
603660efe8
commit
c68275cf70
|
@ -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
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
|
||||||
|
import metalsmith
|
||||||
from metalsmith import sources
|
from metalsmith import sources
|
||||||
|
|
||||||
|
|
||||||
|
@ -229,6 +230,58 @@ def expand(roles, stack_name, expand_provisioned=True, default_image=None,
|
||||||
return instances, env
|
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):
|
def build_hostname_format(hostname_format, role_name):
|
||||||
if not hostname_format:
|
if not hostname_format:
|
||||||
hostname_format = '%stackname%-{}-%index%'.format(
|
hostname_format = '%stackname%-{}-%index%'.format(
|
||||||
|
|
|
@ -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()
|
|
@ -13,8 +13,13 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import metalsmith
|
||||||
|
import mock
|
||||||
|
from openstack import exceptions as sdk_exc
|
||||||
|
|
||||||
from tripleo_ansible.tests import base
|
from tripleo_ansible.tests import base
|
||||||
|
|
||||||
|
|
||||||
# load baremetal_deploy so the next import works
|
# load baremetal_deploy so the next import works
|
||||||
base.load_module_utils('baremetal_deploy')
|
base.load_module_utils('baremetal_deploy')
|
||||||
|
|
||||||
|
@ -670,3 +675,91 @@ class TestExpandRoles(base.TestCase):
|
||||||
self.assertIn('Compute: number of instance entries 4 '
|
self.assertIn('Compute: number of instance entries 4 '
|
||||||
'cannot be greater than count 3',
|
'cannot be greater than count 3',
|
||||||
str(exc))
|
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')
|
||||||
|
|
Loading…
Reference in New Issue