diff --git a/roles/freeipa_server_ecs/README.md b/roles/freeipa_server_ecs/README.md new file mode 100644 index 00000000..e3c1255c --- /dev/null +++ b/roles/freeipa_server_ecs/README.md @@ -0,0 +1,83 @@ +# freeipa_server_ecs + +Configure DNS zones and wildcard records for Cloudera ECS. + +This role configures DNS zones and wildcard records within a **FreeIPA** server, which is a key step for Cloudera on Premise **Embedded Container Service (ECS)**. It simplifies the process of setting up name resolution for applications and services within a specific domain by automatically creating a DNS zone and populating it with wildcard DNS records pointing to a single IP address. + +The role will: +- Authenticate to a FreeIPA server using the provided administrative credentials. +- Create a new DNS zone based on the `ipaserver_domain` and the `zone_name` defined in the `freeipa_dns_records` list. +- Add wildcard DNS records (`*` records) to the specified zone. +- Point these wildcard records to the target IP address defined in `freeipa_dns_records_address`. +- Optionally, skip a check for overlapping DNS zones if `dnszone_skip_overlap_check` is set to `true`. +- Execute all commands via the FreeIPA API, either on a client or server context. + +## Requirements + +- A running and accessible **FreeIPA server**. +- The `ipaadmin_principal` must have permissions to create DNS zones and records within the FreeIPA environment. +- Network connectivity from the Ansible controller (or the `ipaapi_context` host) to the FreeIPA server. + +## Dependencies + +None. + +## Parameters + +| Variable | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `ipaadmin_password` | `str` | `True` | | **FreeIPA** administrative password for authentication. | +| `ipaadmin_principal` | `str` | `False` | `admin` | **FreeIPA** administrative principal (user) for authentication. | +| `ipaserver_host` | `str` | `False` | `inventory_hostname` | Hostname or IP address of the **FreeIPA** server to connect to. Defaults to the current host. | +| `ipaserver_domain` | `str` | `True` | | The **FreeIPA** domain under which the DNS zone will be created (e.g., `example.internal`). | +| `freeipa_dns_records` | `list` of `dict` | `False` | `[{'zone_name': 'apps.{{ ipaserver_domain }}', 'record_name': '*', 'record_type': 'A'}, {'zone_name': '{{ ipaserver_domain }}', 'record_name': '*', 'record_type': 'A'}]` | A list of DNS records to create within the specified **FreeIPA** domain. Each dictionary defines a record with its `zone_name`, `record_name`, and `record_type`. Defaults to creating two wildcard A records. | +| `freeipa_dns_records_address` | `str` | `True` | | The target IP address for the DNS records defined in `freeipa_dns_records`. All records will point to this address. | +| `dnszone_skip_overlap_check` | `bool` | `False` | `false` | A flag to skip the overlap check when creating DNS zones, which can be useful in specific configurations but should be used with caution. | +| `ipaapi_context` | `str` | `False` | - | The **FreeIPA** role of the host where the DNS Zone creation command will be executed. Choices are `client` or `server`. | + +## Example Playbook + +```yaml +- hosts: ipaserver_host + tasks: + - name: Configure FreeIPA DNS for ECS with default wildcard records + ansible.builtin.import_role: + name: cloudera.exe.freeipa_server_ecs + vars: + ipaadmin_password: "MySuperSecretAdminPassword" # Use Ansible Vault for this + ipaserver_domain: "example.internal" + freeipa_dns_records_address: "10.0.0.100" + # The role will automatically create '*' records for 'apps.example.internal' and 'example.internal' + + - name: Configure a single custom DNS record for ECS + ansible.builtin.import_role: + name: cloudera.exe.freeipa_server_ecs + vars: + ipaadmin_password: "MySuperSecretAdminPassword" + ipaserver_domain: "example.internal" + freeipa_dns_records_address: "10.0.0.200" + freeipa_dns_records: + - zone_name: "custom.{{ ipaserver_domain }}" + record_name: "customapp" + record_type: "A" + dnszone_skip_overlap_check: true + ipaapi_context: "client" +``` + +## License + +``` +Copyright 2025 Cloudera, 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 + + https://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. +``` diff --git a/roles/freeipa_server_ecs/defaults/main.yml b/roles/freeipa_server_ecs/defaults/main.yml new file mode 100644 index 00000000..d4c647e6 --- /dev/null +++ b/roles/freeipa_server_ecs/defaults/main.yml @@ -0,0 +1,29 @@ +--- +# Copyright 2025 Cloudera, 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 +# +# https://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. + +ipaadmin_principal: admin +ipaadmin_password: "{{ undef(hint='Please define the FreeIPA adminstrator principal password') }}" +ipaserver_domain: "{{ undef(hint='Please define the FreeIPA server domain') }}" +ipaserver_host: "{{ inventory_hostname }}" +# ipaapi_context: + +dnszone_skip_overlap_check: false + +freeipa_dns_records: + - zone_name: "apps.{{ ipaserver_domain }}" + record_name: "*" + record_type: "A" + +freeipa_dns_records_address: "{{ undef(hint='Please define the FreeIPA DNS records target IP address for ECS') }}" diff --git a/roles/freeipa_server_ecs/meta/argument_specs.yml b/roles/freeipa_server_ecs/meta/argument_specs.yml new file mode 100644 index 00000000..7cb4290f --- /dev/null +++ b/roles/freeipa_server_ecs/meta/argument_specs.yml @@ -0,0 +1,73 @@ +--- +# Copyright 2025 Cloudera, 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 +# +# https://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. + +argument_specs: + main: + short_description: Configure DNS zones and wildcard records for ECS + description: + - Ensures that DNS zones and wildcard records are set up in FreeIPA for ECS. + - Creates a specified DNS zone and adds wildcard DNS records. + author: Cloudera Labs + version_added: 3.0.0 + options: + ipaadmin_password: + description: FreeIPA admin password used for authentication. + required: true + ipaadmin_principal: + description: FreeIPA admin principal used for authentication. + default: admin + ipaserver_host: + description: Hostname or IP address of the FreeIPA server. + default: C(inventory_hostname) + ipaserver_domain: + description: The FreeIPA domain to use for creating the DNS zone and records. + required: true + freeipa_dns_records: + description: DNS records to create within the FreeIPA domain, i.e. DNS zone. + type: list + elements: dict + options: + zone_name: + description: Name of the DNS zone to create the record in. + required: true + record_name: + description: Name of the DNS record (use C(*) for a wildcard record). + required: true + record_type: + description: Type of the DNS record (e.g., A, AAAA). + required: true + default: + - zone_name: "apps.C(ipaserver_domain)" + record_name: "*" + record_type: "A" + - zone_name: "C(ipaserver_domain)" + record_name: "*" + record_type: "A" + freeipa_dns_records_address: + description: DNS records target IP address for the records defined in O(freeipa_dns_records). + type: str + required: true + dnszone_skip_overlap_check: + description: Skip overlap check when creating DNS zones. + type: bool + required: false + default: false + ipaapi_context: + description: The FreeIPA role of the host where the DNS Zone creation will execute. + type: str + required: false + choices: + - client + - server diff --git a/roles/freeipa_server_ecs/molecule/default/converge.yml b/roles/freeipa_server_ecs/molecule/default/converge.yml new file mode 100644 index 00000000..5e11f135 --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/converge.yml @@ -0,0 +1,24 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +- name: Converge + hosts: all + gather_facts: true + tasks: + - name: Provision ECS DNS entries + ansible.builtin.import_role: + name: cloudera.exe.freeipa_server_ecs + vars: + freeipa_dns_records_address: "{{ ansible_default_ipv4.address }}" diff --git a/roles/freeipa_server_ecs/molecule/default/create.yml b/roles/freeipa_server_ecs/molecule/default/create.yml new file mode 100644 index 00000000..41583e5e --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/create.yml @@ -0,0 +1,336 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + vars: + # Run config handling + default_run_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase length=5') }}" + default_run_config: + run_id: "{{ default_run_id }}" + + run_config_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/run-config.yml" + run_config_from_file: "{{ (lookup('file', run_config_path, errors='ignore') or '{}') | from_yaml }}" + run_config: "{{ default_run_config | combine(run_config_from_file) }}" + + # Platform settings handling + default_assign_public_ip: true + default_aws_profile: "{{ lookup('env', 'AWS_PROFILE') }}" + default_boot_wait_seconds: 120 + default_instance_type: t3a.medium + default_key_inject_method: cloud-init # valid values: [cloud-init, ec2] + default_key_name: "molecule-{{ run_config.run_id }}" + default_private_key_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/id_rsa" + default_public_key_path: "{{ default_private_key_path }}.pub" + default_ssh_user: ansible + default_ssh_port: 22 + default_user_data: "" + + default_security_group_name: "molecule-{{ run_config.run_id }}" + default_security_group_description: Ephemeral security group for Molecule instances + default_security_group_rules: + - proto: tcp + from_port: "{{ default_ssh_port }}" + to_port: "{{ default_ssh_port }}" + cidr_ip: "0.0.0.0/0" + - proto: icmp + from_port: 8 + to_port: -1 + cidr_ip: "0.0.0.0/0" + default_security_group_rules_egress: + - proto: -1 + from_port: 0 + to_port: 0 + cidr_ip: "0.0.0.0/0" + + platform_defaults: + assign_public_ip: "{{ default_assign_public_ip }}" + aws_profile: "{{ default_aws_profile }}" + boot_wait_seconds: "{{ default_boot_wait_seconds }}" + instance_type: "{{ default_instance_type }}" + key_inject_method: "{{ default_key_inject_method }}" + key_name: "{{ default_key_name }}" + private_key_path: "{{ default_private_key_path }}" + public_key_path: "{{ default_public_key_path }}" + security_group_name: "{{ default_security_group_name }}" + security_group_description: "{{ default_security_group_description }}" + security_group_rules: "{{ default_security_group_rules }}" + security_group_rules_egress: "{{ default_security_group_rules_egress }}" + ssh_user: "{{ default_ssh_user }}" + ssh_port: "{{ default_ssh_port }}" + cloud_config: {} + image: "" + image_name: "" + image_owner: [self] + name: "" + region: "" + security_groups: [] + tags: {} + volumes: [] + vpc_id: "" + vpc_subnet_id: "" + + # Merging defaults into a list of dicts is, it turns out, not straightforward + platforms: >- + {{ [platform_defaults | dict2items] + | product(molecule_yml.platforms | map('dict2items') | list) + | map('flatten', levels=1) + | list + | map('items2dict') + | list }} + pre_tasks: + - name: Validate platform configurations + ansible.builtin.assert: + that: + - platforms | length > 0 + - platform.name is string and platform.name | length > 0 + - platform.assign_public_ip is boolean + - platform.aws_profile is string + - platform.boot_wait_seconds is integer and platform.boot_wait_seconds >= 0 + - platform.cloud_config is mapping + - platform.image is string + - platform.image_name is string + - platform.image_owner is sequence or (platform.image_owner is string and platform.image_owner | length > 0) + - platform.instance_type is string and platform.instance_type | length > 0 + - platform.key_inject_method is in ["cloud-init", "ec2"] + - platform.key_name is string and platform.key_name | length > 0 + - platform.private_key_path is string and platform.private_key_path | length > 0 + - platform.public_key_path is string and platform.public_key_path | length > 0 + - platform.region is string + - platform.security_group_name is string and platform.security_group_name | length > 0 + - platform.security_group_description is string and platform.security_group_description | length > 0 + - platform.security_group_rules is sequence + - platform.security_group_rules_egress is sequence + - platform.security_groups is sequence + - platform.ssh_user is string and platform.ssh_user | length > 0 + - platform.ssh_port is integer and platform.ssh_port in range(1, 65536) + - platform.tags is mapping + - platform.volumes is sequence + - platform.vpc_id is string + - platform.vpc_subnet_id is string and platform.vpc_subnet_id | length > 0 + quiet: true + loop: "{{ platforms }}" + loop_control: + loop_var: platform + label: "{{ platform.name }}" + tasks: + - name: Write run config to file + ansible.builtin.copy: + dest: "{{ run_config_path }}" + content: "{{ run_config | to_yaml }}" + mode: "0600" + + - name: Generate local key pairs + community.crypto.openssh_keypair: + path: "{{ item.private_key_path }}" + type: rsa + size: 2048 + regenerate: never + loop: "{{ platforms }}" + loop_control: + label: "{{ item.name }}" + register: local_keypairs + + - name: Look up EC2 AMI(s) by owner and name (if image not set) + amazon.aws.ec2_ami_info: + owners: "{{ item.image_owner }}" + filters: "{{ item.image_filters | default({}) | combine(image_name_map) }}" + vars: + image_name_map: "{% if item.image_name is defined and item.image_name | length > 0 %}{{ {'name': item.image_name} }}{% else %}{}{% endif %}" + loop: "{{ platforms }}" + loop_control: + label: "{{ item.name }}" + when: not item.image + register: ami_info + + - name: Look up subnets to determine VPCs (if needed) + amazon.aws.ec2_vpc_subnet_info: + subnet_ids: "{{ item.vpc_subnet_id }}" + loop: "{{ platforms }}" + loop_control: + label: "{{ item.name }}" + when: not item.vpc_id + register: subnet_info + + - name: Validate discovered information + ansible.builtin.assert: + that: + - platform.image or (ami_info.results[index].images | length > 0) + - platform.vpc_id or (subnet_info.results[index].subnets | length > 0) + quiet: true + loop: "{{ platforms }}" + loop_control: + loop_var: platform + index_var: index + label: "{{ platform.name }}" + + - name: Create ephemeral EC2 keys (if needed) + amazon.aws.ec2_key: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + name: "{{ item.key_name }}" + key_material: "{{ local_keypair.public_key }}" + vars: + local_keypair: "{{ local_keypairs.results[index] }}" + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + when: item.key_inject_method == "ec2" + register: ec2_keys + + - name: Create ephemeral security groups (if needed) + amazon.aws.ec2_security_group: + profile: "{{ item.aws_profile | default(omit) }}" + iam_instance_profile: "{{ item.iam_instance_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + vpc_id: "{{ item.vpc_id or vpc_subnet.vpc_id }}" + name: "{{ item.security_group_name }}" + description: "{{ item.security_group_description }}" + rules: "{{ item.security_group_rules }}" + rules_egress: "{{ item.security_group_rules_egress }}" + vars: + vpc_subnet: "{{ subnet_info.results[index].subnets[0] }}" + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + when: item.security_groups | length == 0 + + - name: Create ephemeral EC2 instance(s) + amazon.aws.ec2_instance: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + filters: "{{ platform_filters }}" + instance_type: "{{ item.instance_type }}" + image_id: "{{ platform_image_id }}" + vpc_subnet_id: "{{ item.vpc_subnet_id }}" + security_groups: "{{ platform_security_groups }}" + network: + assign_public_ip: "{{ item.assign_public_ip }}" + volumes: "{{ item.volumes }}" + key_name: "{{ (item.key_inject_method == 'ec2') | ternary(item.key_name, omit) }}" + tags: "{{ platform_tags }}" + user_data: "{{ platform_user_data }}" + state: "running" + wait: true + vars: + platform_security_groups: "{{ item.security_groups or [item.security_group_name] }}" + platform_generated_image_id: "{{ (ami_info.results[index].images | sort(attribute='creation_date', reverse=True))[0].image_id }}" + platform_image_id: "{{ item.image or platform_generated_image_id }}" + + platform_generated_cloud_config: + users: + - name: "{{ item.ssh_user }}" + ssh_authorized_keys: + - "{{ local_keypairs.results[index].public_key }}" + sudo: "ALL=(ALL) NOPASSWD:ALL" + platform_cloud_config: >- + {{ (item.key_inject_method == 'cloud-init') + | ternary((item.cloud_config | combine(platform_generated_cloud_config)), item.cloud_config) }} + platform_user_data: |- + #cloud-config + {{ platform_cloud_config | to_yaml }} + + platform_generated_tags: + instance: "{{ item.name }}" + "molecule-run-id": "{{ run_config.run_id }}" + platform_tags: "{{ (item.tags or {}) | combine(platform_generated_tags) }}" + platform_filter_keys: "{{ platform_generated_tags.keys() | map('regex_replace', '^(.+)$', 'tag:\\1') }}" + platform_filters: "{{ dict(platform_filter_keys | zip(platform_generated_tags.values())) }}" + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + register: ec2_instances_async + async: 7200 + poll: 0 + + - name: Instance boot block + when: ec2_instances_async is changed + block: + - name: Wait for instance creation to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ ec2_instances_async.results }}" + loop_control: + index_var: index + label: "{{ platforms[index].name }}" + register: ec2_instances + until: ec2_instances is finished + retries: 300 + + - name: Collect instance configs + ansible.builtin.set_fact: + instance_config: + instance: "{{ item.name }}" + address: "{{ item.assign_public_ip | ternary(instance.public_ip_address, instance.private_ip_address) }}" + user: "{{ item.ssh_user }}" + port: "{{ item.ssh_port }}" + identity_file: "{{ item.private_key_path }}" + instance_ids: + - "{{ instance.instance_id }}" + vars: + instance: "{{ ec2_instances.results[index].instances[0] }}" + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + register: instance_configs + + - name: Write Molecule instance configs + ansible.builtin.copy: + dest: "{{ molecule_instance_config }}" + content: >- + {{ instance_configs.results + | map(attribute='ansible_facts.instance_config') + | list + | to_json + | from_json + | to_yaml }} + mode: "0600" + + - name: Start SSH pollers + ansible.builtin.wait_for: + host: "{{ item.address }}" + port: "{{ item.port }}" + search_regex: SSH + delay: 10 + timeout: 320 + loop: "{{ instance_configs.results | map(attribute='ansible_facts.instance_config') | list }}" + loop_control: + label: "{{ item.instance }}" + register: ssh_wait_async + async: 300 + poll: 0 + + - name: Wait for SSH + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ ssh_wait_async.results }}" + loop_control: + index_var: index + label: "{{ platforms[index].name }}" + register: ssh_wait + until: ssh_wait is finished + retries: 300 + delay: 1 + + - name: Wait for boot process to finish + ansible.builtin.pause: + seconds: "{{ platforms | map(attribute='boot_wait_seconds') | max }}" diff --git a/roles/freeipa_server_ecs/molecule/default/destroy.yml b/roles/freeipa_server_ecs/molecule/default/destroy.yml new file mode 100644 index 00000000..a090fe0b --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/destroy.yml @@ -0,0 +1,157 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + vars: + # Run config handling + default_run_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase length=5') }}" + default_run_config: + run_id: "{{ default_run_id }}" + + run_config_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/run-config.yml" + run_config_from_file: "{{ (lookup('file', run_config_path, errors='ignore') or '{}') | from_yaml }}" + run_config: "{{ default_run_config | combine(run_config_from_file) }}" + + # Platform settings handling + default_aws_profile: "{{ lookup('env', 'AWS_PROFILE') }}" + default_key_inject_method: cloud-init # valid values: [cloud-init, ec2] + default_key_name: "molecule-{{ run_config.run_id }}" + default_security_group_name: "molecule-{{ run_config.run_id }}" + + platform_defaults: + aws_profile: "{{ default_aws_profile }}" + key_inject_method: "{{ default_key_inject_method }}" + key_name: "{{ default_key_name }}" + region: "" + security_group_name: "{{ default_security_group_name }}" + security_groups: [] + vpc_id: "" + vpc_subnet_id: "" + + # Merging defaults into a list of dicts is, it turns out, not straightforward + platforms: >- + {{ [platform_defaults | dict2items] + | product(molecule_yml.platforms | map('dict2items') | list) + | map('flatten', levels=1) + | list + | map('items2dict') + | list }} + + # Stored instance config + instance_config: "{{ (lookup('file', molecule_instance_config, errors='ignore') or '{}') | from_yaml }}" + pre_tasks: + - name: Validate platform configurations + ansible.builtin.assert: + that: + - platforms | length > 0 + - platform.name is string and platform.name | length > 0 + - platform.aws_profile is string + - platform.key_inject_method is in ["cloud-init", "ec2"] + - platform.key_name is string and platform.key_name | length > 0 + - platform.region is string + - platform.security_group_name is string and platform.security_group_name | length > 0 + - platform.security_groups is sequence + - platform.vpc_id is string + - platform.vpc_subnet_id is string and platform.vpc_subnet_id | length > 0 + quiet: true + loop: "{{ platforms }}" + loop_control: + loop_var: platform + label: "{{ platform.name }}" + tasks: + - name: Look up subnets to determine VPCs (if needed) + amazon.aws.ec2_vpc_subnet_info: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + subnet_ids: "{{ item.vpc_subnet_id }}" + loop: "{{ platforms }}" + loop_control: + label: "{{ item.name }}" + when: not item.vpc_id + register: subnet_info + + - name: Validate discovered information + ansible.builtin.assert: + that: platform.vpc_id or (subnet_info.results[index].subnets | length > 0) + quiet: true + loop: "{{ platforms }}" + loop_control: + loop_var: platform + index_var: index + label: "{{ platform.name }}" + + - name: Destroy resources + when: instance_config | length != 0 + block: + - name: Destroy ephemeral EC2 instances + amazon.aws.ec2_instance: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + instance_ids: "{{ instance_config | map(attribute='instance_ids') | flatten }}" + vpc_subnet_id: "{{ item.vpc_subnet_id }}" + state: absent + loop: "{{ platforms }}" + loop_control: + label: "{{ item.name }}" + register: ec2_instances_async + async: 7200 + poll: 0 + + - name: Wait for instance destruction to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ ec2_instances_async.results }}" + loop_control: + index_var: index + label: "{{ platforms[index].name }}" + register: ec2_instances + until: ec2_instances is finished + retries: 300 + + - name: Destroy ephemeral security groups (if needed) + amazon.aws.ec2_security_group: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + vpc_id: "{{ item.vpc_id or vpc_subnet.vpc_id }}" + name: "{{ item.security_group_name }}" + state: absent + vars: + vpc_subnet: "{{ subnet_info.results[index].subnets[0] }}" + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + when: item.security_groups | length == 0 + + - name: Destroy ephemeral keys (if needed) + amazon.aws.ec2_key: + profile: "{{ item.aws_profile | default(omit) }}" + region: "{{ item.region | default(omit) }}" + name: "{{ item.key_name }}" + state: absent + loop: "{{ platforms }}" + loop_control: + index_var: index + label: "{{ item.name }}" + when: item.key_inject_method == "ec2" + + - name: Write Molecule instance configs + ansible.builtin.copy: + dest: "{{ molecule_instance_config }}" + content: "{{ {} | to_yaml }}" diff --git a/roles/freeipa_server_ecs/molecule/default/molecule.yml b/roles/freeipa_server_ecs/molecule/default/molecule.yml new file mode 100644 index 00000000..94351652 --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/molecule.yml @@ -0,0 +1,47 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +# Requires a preexisting AWS VPC and subnet, the latter referenced by the +# environment variable, ROLE_SUBNET_ID, typically set in the Molecule ENV file +# which is /.env.yml + +# Requires AWS credentials, including region + +# Requires the following Python libraries on localhost +# - netaddr +# - boto3 + +driver: + name: ec2 +platforms: + - name: rhel9-4.molecule.internal + image_owner: "309956199498" + image_name: RHEL-9.4.0_HVM-* + instance_type: t3.medium + boot_wait_seconds: 15 + vpc_subnet_id: ${TEST_VPC_SUBNET_ID} + tags: + Name: molecule-prereq_server_dns_ecs-rhel9-4 + Project: Molecule testing for prereq_server_dns_ecs +provisioner: + name: ansible + inventory: + group_vars: + all: + ipaadmin_password: admin_password + ipadm_password: admin_password + ipaserver_host: "{{ inventory_hostname }}" + ipaserver_domain: "molecule.internal" + ipaserver_realm: "MOLECULE.INTERNAL" diff --git a/roles/freeipa_server_ecs/molecule/default/prepare.yml b/roles/freeipa_server_ecs/molecule/default/prepare.yml new file mode 100644 index 00000000..d7706778 --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/prepare.yml @@ -0,0 +1,57 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +- name: Prepare + hosts: all + gather_facts: true + become: true + tasks: + - name: Extract the VPC subnet ID from the Molecule platform configuration + ansible.builtin.set_fact: + test_subnet_id: "{{ molecule_yml.platforms | selectattr('name', 'eq', inventory_hostname) | map(attribute='vpc_subnet_id') | first }}" + + - name: Retrieve the VPC subnet details + amazon.aws.ec2_vpc_subnet_info: + subnet_id: "{{ test_subnet_id }}" + register: __subnet + become: false + delegate_to: localhost + + - name: Retrieve the VPC details + amazon.aws.ec2_vpc_net_info: + vpc_ids: "{{ __subnet.subnets | map(attribute='vpc_id') | first }}" + register: __vpc + become: false + delegate_to: localhost + + - name: Update the core networking + ansible.builtin.import_role: + name: prereq_network_dns + vars: + network_ip_address: "{{ ansible_default_ipv4.address }}" + network_dns_domain: "{{ ipaserver_domain }}" + # See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html + vpc_cidr: "{{ __vpc.vpcs | map(attribute='cidr_block') | list }}" + network_dns_forwarders: "{{ vpc_cidr | map('ansible.utils.ipmath', '2') | list }}" + + - name: Install FreeIPA server + ansible.builtin.import_role: + name: freeipa_server + vars: + # See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html + vpc_cidr: "{{ __vpc.vpcs | map(attribute='cidr_block') | list }}" + ipaserver_cidr: "{{ vpc_cidr }}" + ipaserver_forwarders: "{{ vpc_cidr | map('ansible.utils.ipmath', '2') | list }}" + ipaserver_recursion_acl_cidr: "{{ vpc_cidr }}" diff --git a/roles/freeipa_server_ecs/molecule/default/requirements.yml b/roles/freeipa_server_ecs/molecule/default/requirements.yml new file mode 100644 index 00000000..1779da38 --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/requirements.yml @@ -0,0 +1,22 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +collections: + - community.crypto + - amazon.aws + - ansible.posix + - ansible.utils + - community.general + - freeipa.ansible_freeipa diff --git a/roles/freeipa_server_ecs/molecule/default/verify.yml b/roles/freeipa_server_ecs/molecule/default/verify.yml new file mode 100644 index 00000000..34fbb510 --- /dev/null +++ b/roles/freeipa_server_ecs/molecule/default/verify.yml @@ -0,0 +1,42 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +- name: Verify + hosts: all + gather_facts: false + vars: + freeipa_dns_records: + - zone_name: "apps.{{ ipaserver_domain }}" + record_name: "*" + record_type: "A" + - zone_name: "{{ ipaserver_domain }}" + record_name: "*" + record_type: "A" + tasks: + - name: Verify that the DNS zone exists + ansible.builtin.command: ipa dnszone-find apps.{{ ipaserver_realm }} + register: zone_check + changed_when: false + failed_when: "'Number of entries returned 0' in zone_check.stdout" + + - name: Verify that the DNS records exist + ansible.builtin.command: ipa dnsrecord-find {{ __rec.zone_name }} --all + register: record_check + failed_when: "'Number of entries returned 0' in record_check.stdout" + changed_when: false + loop: "{{ freeipa_dns_records }}" + loop_control: + loop_var: __rec + label: "{{ __rec.zone_name }}" diff --git a/roles/freeipa_server_ecs/tasks/main.yml b/roles/freeipa_server_ecs/tasks/main.yml new file mode 100644 index 00000000..a1b9df4c --- /dev/null +++ b/roles/freeipa_server_ecs/tasks/main.yml @@ -0,0 +1,41 @@ +--- +# Copyright 2024 Cloudera, 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 +# +# https://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. + +# NOTE: Below task replaces community.general.ipa_dnszone while waiting for a patch to include skip_overlap_check +- name: Ensure apps zone is present for PvC ECS + freeipa.ansible_freeipa.ipadnszone: + zone_name: "apps.{{ ipaserver_domain }}" + ipaadmin_password: "{{ ipaadmin_password }}" + ipaadmin_principal: "{{ ipaadmin_principal }}" + ipaapi_context: "{{ ipaapi_context | default(omit) }}" + skip_overlap_check: "{{ dnszone_skip_overlap_check }}" + state: present + register: ipa_dnszone_result + failed_when: ipa_dnszone_result.failed and "no modifications to be performed" not in ipa_dnszone_result.msg + +- name: Ensure wildcard records are prepared for PvC ECS + community.general.ipa_dnsrecord: + zone_name: "{{ __rec.zone_name }}" + record_name: "{{ __rec.record_name }}" + record_type: "{{ __rec.record_type }}" + record_value: "{{ freeipa_dns_records_address }}" + ipa_pass: "{{ ipaadmin_password }}" + ipa_user: "{{ ipaadmin_principal }}" + ipa_host: "{{ ipaserver_host }}" + state: present + loop: "{{ freeipa_dns_records }}" + loop_control: + loop_var: __rec + label: "{{ __rec.zone_name }}"