diff --git a/doc/source/modules/modules-tripleo_unmanaged_populate_environment.rst b/doc/source/modules/modules-tripleo_unmanaged_populate_environment.rst new file mode 100644 index 000000000..ad632205a --- /dev/null +++ b/doc/source/modules/modules-tripleo_unmanaged_populate_environment.rst @@ -0,0 +1,14 @@ +=============================================== +Module - tripleo_unmanaged_populate_environment +=============================================== + + +This module provides for the following ansible plugin: + + * tripleo_unmanaged_populate_environment + + +.. ansibleautoplugin:: + :module: tripleo_ansible/ansible_plugins/modules/tripleo_unmanaged_populate_environment.py + :documentation: true + :examples: true diff --git a/tripleo_ansible/ansible_plugins/module_utils/baremetal_deploy.py b/tripleo_ansible/ansible_plugins/module_utils/baremetal_deploy.py index fc5ef5d04..9f61d24c6 100644 --- a/tripleo_ansible/ansible_plugins/module_utils/baremetal_deploy.py +++ b/tripleo_ansible/ansible_plugins/module_utils/baremetal_deploy.py @@ -85,6 +85,8 @@ _INSTANCE_SCHEMA = { 'items': {'type': 'string'} }, 'user_name': {'type': 'string'}, + 'managed': {'type': 'boolean'}, + 'management_ip': {'type': 'string'}, }, 'additionalProperties': False, } @@ -306,7 +308,12 @@ def check_existing(instances, provisioner, baremetal): not_found = [] found = [] + unmanaged = [] for request in instances: + if not request.get('managed', True): + unmanaged.append(request) + continue + ident = request.get('name', request['hostname']) try: @@ -346,7 +353,7 @@ def check_existing(instances, provisioner, baremetal): ) found.append(instance) - return found, not_found + return found, not_found, unmanaged def populate_environment(instance_uuids, provisioner, environment, @@ -390,7 +397,9 @@ def validate_instances(instances, schema): jsonschema.validate(instances, schema) hostnames = set() names = set() + fixed_ips = set() for inst in instances: + name = inst.get('hostname', inst.get('name')) # NOTE(dtantsur): validate image parameters get_source(inst) @@ -406,6 +415,22 @@ def validate_instances(instances, schema): inst['name']) names.add(inst['name']) + inst_ips = {net['fixed_ip'] for net in inst.get('networks', []) + if net.get('fixed_ip')} + if inst_ips.intersection(fixed_ips): + raise ValueError( + 'One or more IP address {ips} for Node {name} is requested ' + 'more than once'.format( + ips=', '.join(inst_ips.intersection(fixed_ips)), + name=name)) + fixed_ips.update(inst_ips) + + if not inst.get('managed', True): + if not inst_ips and not inst.get('management_ip'): + raise ValueError('Node %s that is managed: false requires ' + 'either a fixed IP address, or a management ' + 'ip address' % name) + def validate_roles(roles): jsonschema.validate(roles, _ROLES_INPUT_SCHEMA) diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_baremetal_check_existing.py b/tripleo_ansible/ansible_plugins/modules/tripleo_baremetal_check_existing.py index 568daecb3..e64f6939e 100644 --- a/tripleo_ansible/ansible_plugins/modules/tripleo_baremetal_check_existing.py +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_baremetal_check_existing.py @@ -114,7 +114,7 @@ def main(): provisioner = metalsmith.Provisioner(cloud_region=cloud.config) try: - found, not_found = bd.check_existing( + found, not_found, pre_provisioned = bd.check_existing( instances=module.params['instances'], provisioner=provisioner, baremetal=cloud.baremetal @@ -126,6 +126,9 @@ def main(): if not_found: msg += ('Instance(s) %s do not exist. ' % ', '.join(r['hostname'] for r in not_found)) + if pre_provisioned: + msg += ('Instance(s) %s are pre-provisioned. ' + % ', '.join(r['hostname'] for r in pre_provisioned)) instances = [{ 'name': i.node.name or i.uuid, @@ -136,7 +139,8 @@ def main(): changed=False, msg=msg, instances=instances, - not_found=not_found + not_found=not_found, + pre_provisioned=pre_provisioned ) except Exception as e: module.fail_json(msg=str(e)) diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_unmanaged_populate_environment.py b/tripleo_ansible/ansible_plugins/modules/tripleo_unmanaged_populate_environment.py new file mode 100644 index 000000000..2f1fa292a --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_unmanaged_populate_environment.py @@ -0,0 +1,179 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2020 OpenStack Foundation +# 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. + +import yaml + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.openstack import openstack_full_argument_spec +from ansible.module_utils.openstack import openstack_module_kwargs + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: tripleo_unmanaged_populate_environment + +short_description: Add unmanaged node to existing heat environment + +version_added: "2.8" + +description: + - "Add unmanaged node to existing heat environment" + +options: + environment: + description: + - Existing heat environment data to add to + type: dict + default: {} + instances: + description: + - List of unmanaged instances + required: true + type: list + elements: dict + node_port_map: + description: + - Structure with port data mapped by node and network, in the format + returned by the tripleo_overcloud_network_ports module. + type: dict + default: {} + ctlplane_network: + description: + - Name of control plane network + default: ctlplane + type: str +author: + - Harald Jensås +''' + +RETURN = ''' +parameter_defaults: + FooParam: foo + DeployedServerPortMap: + controller-0-ctlplane: + fixed_ips: + - ip_address': 1.1.1.1 + compute-0-ctlplane: + fixed_ips: + - ip_address': 1.1.1.2 + instance3-ctlplane: + fixed_ips: + - ip_address': 1.1.1.3 +resource_registry: + OS::Fake::Resource: /path/to/fake/resource.yaml +''' + +EXAMPLES = ''' +- name: Populate environment with network port data + tripleo_unmanaged_populate_environment: + ctlplane_network: ctlplane + environment: + parameter_defaults: + FooParam: foo + DeployedServerPortMap: + instance3-ctlplane: + fixed_ips: + - ip_address': 1.1.1.3 + resource_registry: + OS::Fake::Resource: /path/to/fake/resource.yaml + instances: + - hostname: controller-0 + managed: false + networks: + - network: ctlplane + fixed_ip: 1.1.1.1 + - hostname': compute-0 + managed: false + networks: + - network: ctlplane + fixed_ip: 1.1.1.2 + node_port_map: + controller-0: + ctlplane: + ip_address: 1.1.1.1 + ip_subnet: 1.1.1.1/24 + ip_address_uri: 1.1.1.1 + compute-0: + ctlplane: + ip_address: 1.1.1.2 + ip_subnet: 1.1.1.2/24 + ip_address_uri: 1.1.1.2 + register: environment +''' + + +def update_environment(environment, ctlplane_network, node_port_map, + instances): + parameter_defaults = environment.setdefault('parameter_defaults', {}) + port_map = parameter_defaults.setdefault('DeployedServerPortMap', {}) + for instance in instances: + if instance.get('managed', True): + continue + + hostname = instance['hostname'] + ip_address = node_port_map[hostname][ctlplane_network]['ip_address'] + ctlplane = {} + ctlplane['fixed_ips'] = [{'ip_address': ip_address}] + port_map['%s-%s' % (hostname, ctlplane_network)] = ctlplane + + +def run_module(): + result = dict( + success=False, + changed=False, + error="", + environment={}, + ) + + argument_spec = openstack_full_argument_spec( + **yaml.safe_load(DOCUMENTATION)['options'] + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=False, + **openstack_module_kwargs() + ) + + environment = result['environment'] = module.params['environment'] + instances = module.params['instances'] + node_port_map = module.params['node_port_map'] + ctlplane_network = module.params['ctlplane_network'] + + try: + update_environment(environment, ctlplane_network, node_port_map, + instances) + result['success'] = True + module.exit_json(**result) + + except Exception as err: + result['error'] = str(err) + result['msg'] = ("Error overcloud network provision failed!") + module.fail_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/tripleo_ansible/playbooks/cli-overcloud-node-provision.yaml b/tripleo_ansible/playbooks/cli-overcloud-node-provision.yaml index fab53b64b..6ecdc123b 100644 --- a/tripleo_ansible/playbooks/cli-overcloud-node-provision.yaml +++ b/tripleo_ansible/playbooks/cli-overcloud-node-provision.yaml @@ -134,6 +134,7 @@ concurrency: "{{ runtime_concurrency }}" instances: "{{ baremetal_instances.instances }}" provisioned_instances: "{{ baremetal_provisioned.instances + baremetal_existing.instances }}" + hostname_role_map: "{{ baremetal_instances.hostname_role_map }}" state: present register: instance_network_ports when: manage_network_ports|default(false) diff --git a/tripleo_ansible/tests/modules/test_tripleo_unmanaged_populate_environment.py b/tripleo_ansible/tests/modules/test_tripleo_unmanaged_populate_environment.py new file mode 100644 index 000000000..6a404706b --- /dev/null +++ b/tripleo_ansible/tests/modules/test_tripleo_unmanaged_populate_environment.py @@ -0,0 +1,73 @@ +# 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. + +import copy + +from tripleo_ansible.ansible_plugins.modules import ( + tripleo_unmanaged_populate_environment as plugin) +from tripleo_ansible.tests import base as tests_base + + +FAKE_INSTANCES = [ + {'hostname': 'instance1', + 'managed': False, + 'networks': [{'network': 'ctlplane', 'fixed_ip': '1.1.1.1'}]}, + {'hostname': 'instance2', + 'managed': False, + 'networks': [{'network': 'ctlplane', 'fixed_ip': '1.1.1.2'}]}, + {'hostname': 'instance3', + 'networks': [{'network': 'ctlplane', 'vif': True}]}, +] + +FAKE_ENVIRONMENT = { + 'parameter_defaults': { + 'FooParam': 'foo', + 'DeployedServerPortMap': { + 'instance3-ctlplane': { + 'fixed_ips': [{'ip_address': '1.1.1.3'}] + } + } + }, + 'resource_registry': { + 'OS::Fake::Resource': '/path/to/fake/resource.yaml' + }, +} + +FAKE_NODE_PORT_MAP = { + 'instance1': { + 'ctlplane': {'ip_address': '1.1.1.1'} + }, + 'instance2': { + 'ctlplane': {'ip_address': '1.1.1.2'} + }, + 'instance3': { + 'ctlplane': {'ip_address': '1.1.1.3'} + }, +} + + +class TestTripleoOvercloudNetworkPorts(tests_base.TestCase): + + def test_update_environment(self): + env = copy.deepcopy(FAKE_ENVIRONMENT) + plugin.update_environment(env, 'ctlplane', FAKE_NODE_PORT_MAP, + FAKE_INSTANCES) + expected = copy.deepcopy(FAKE_ENVIRONMENT) + expected['parameter_defaults']['DeployedServerPortMap'].update( + {'instance1-ctlplane': {'fixed_ips': [{'ip_address': '1.1.1.1'}]}, + 'instance2-ctlplane': {'fixed_ips': [{'ip_address': '1.1.1.2'}]}, + } + ) + self.assertEqual(expected, env) diff --git a/tripleo_ansible/tests/plugins/module_utils/test_baremetal_deploy.py b/tripleo_ansible/tests/plugins/module_utils/test_baremetal_deploy.py index e8d3b7412..d645d095d 100644 --- a/tripleo_ansible/tests/plugins/module_utils/test_baremetal_deploy.py +++ b/tripleo_ansible/tests/plugins/module_utils/test_baremetal_deploy.py @@ -957,7 +957,8 @@ class TestCheckExistingInstances(base.TestCase): metalsmith.exceptions.Error(""), existing, ] - found, not_found = bd.check_existing(instances, pr, baremetal) + found, not_found, unmanaged = bd.check_existing(instances, pr, + baremetal) self.assertEqual([existing], found) self.assertEqual([{ @@ -986,7 +987,8 @@ class TestCheckExistingInstances(base.TestCase): existing.uuid = 'aaaa' pr.show_instance.return_value = existing - found, not_found = bd.check_existing(instances, pr, baremetal) + found, not_found, unmanaged = bd.check_existing(instances, pr, + baremetal) baremetal.create_allocation.assert_called_once_with( name='host2', node='server2', resource_class='compute')