diff --git a/roles/freeipa_server/README.md b/roles/freeipa_server/README.md index 05e0b061..a1a41867 100644 --- a/roles/freeipa_server/README.md +++ b/roles/freeipa_server/README.md @@ -1,17 +1,61 @@ - +The role will: +- Enable local IPv6 networking, per FreeIPA installation requirements. +- Install FreeIPA server packages. +- Install and configure FreeIPA server for DNS, Kerberos, TLS, and LDAP. +- Set up an ACL for DNS recursion. +- Establish DNS zones for the defined domain. -# freeipa_server +## Requirements + +None. + +## Dependencies + +- `freeipa.ansible` + +## Examples + +```yaml +- name: Install FreeIPA server for DNS, Kerberos, TLS, and LDAP. + ansible.builtin.import_role: + name: freeipa_server + vars: + ipaserver_forwarders: [ "1.1.1.1" ] + ipaserver_cidr: [ "10.0.0.1/20" ] + ipaserver_recursion_acl_cidr: [ "10.0.0.1/20" ] + ipaserver_domain: "example.internal" + ipaserver_realm: "EXAMPLE.INTERNAL" + ipaadmin_password: "krb_example" + ipadm_password: "dir_example" +``` + +To bind to explicit IP addresses, provide optional (push-down) parameters: + +```yaml +ipaserver_ip_addresses: [ "10.0.0.14"] # Bind DNS to these IP addresses only +ipaclient_ip_address: "10.0.1.122" # Join with this IP address +``` + +## License + +``` +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. +``` diff --git a/roles/freeipa_server/defaults/main.yml b/roles/freeipa_server/defaults/main.yml index 59b18bb8..487b9a38 100644 --- a/roles/freeipa_server/defaults/main.yml +++ b/roles/freeipa_server/defaults/main.yml @@ -1,12 +1,10 @@ ---- - -# Copyright 2023 Cloudera, Inc. All Rights Reserved. +# 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 # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -14,15 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -#ipaserver_realm: "{{ krb5_realm | upper }}" -#ipaserver_domain: "{{ krb5_domain | default(krb5_realm | lower) }}" -#ipaserver_setup_firewalld: "no" -#ipaserver_setup_dns: "{{ freeipa_autodns | default(omit) }}" -#ipaserver_auto_forwarders: -#ipadm_password: +# ipaserver_packages: [] + +ipaserver_domain: "{{ undef(hint='Please define the DNS domain') }}" +ipaserver_realm: "{{ undef(hint='Please define the Kerberos realm') }}" + +ipaserver_cidr: "{{ undef(hint='Please define the CIDR list under management for the DNS service.') }}" +ipaserver_forwarders: "{{ undef(hint='Please define the upstream DNS servers') }}" +ipaserver_recursion_acl_cidr: "{{ undef(hint='Please define the CIDR list for the DNS recursion ACL') }}" + +ipaserver_ca_subject: "CN=CLDR-RootCA,O={{ ipaserver_domain }}" + +ipaadmin_principal: admin +ipaadmin_password: "{{ undef(hint='Please define the FreeIPA administrator principal password') }}" -# ipaserver_recursion_acl_cidr: -ipaserver_resolv_nameservers: ["8.8.8.8"] -ipaserver_server_recursion: true -enable_dns: false -needs_python2: true +ipadm_password: "{{ undef(hint='Please define the FreeIPA Directory Manager admin password') }}" diff --git a/roles/freeipa_server/handlers/main.yml b/roles/freeipa_server/handlers/main.yml index ea9ec813..e61120b8 100644 --- a/roles/freeipa_server/handlers/main.yml +++ b/roles/freeipa_server/handlers/main.yml @@ -1,11 +1,10 @@ ---- -# Copyright 2023 Cloudera, Inc. All Rights Reserved. +# 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 # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -13,12 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -- name: Restart network +# named-pkcs11 is masked, so restart named +- name: Restart DNS ansible.builtin.service: - name: NetworkManager - state: restarted - -- name: Restart dns - ansible.builtin.service: - name: named-pkcs11 + name: named state: restarted diff --git a/roles/freeipa_server/meta/argument_specs.yml b/roles/freeipa_server/meta/argument_specs.yml new file mode 100644 index 00000000..a8bd060a --- /dev/null +++ b/roles/freeipa_server/meta/argument_specs.yml @@ -0,0 +1,70 @@ +# 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. + +argument_specs: + main: + short_description: Set up FreeIPA server + description: | + Set up FreeIPA server with support for DNS, Kerberos, TLS, and LDAP. + Optionally, install the FreeIPA server packages. + author: Cloudera Labs + options: + ipaserver_packages: + description: + - List of FreeIPA packages to install. + - If not defined, the role will default to the packages defined in the P(freeipa.ansible_freeipa.ipaserver#role) role. + type: list + elements: str + ipaserver_domain: + description: + - Domain name to set as the root zone. + required: true + ipaserver_realm: + description: + - Realm to set for the Kerberos service. + required: true + ipaserver_cidr: + description: + - List of CIDR blocks to manage for the DNS service. + type: list + elements: str + required: true + ipaserver_forwarders: + description: + - List of upstream name servers for the DNS service. + type: list + elements: str + required: true + ipaserver_recursion_acl_cidr: + description: + - List of CIDR blocks to define the access ACL for DNS recursion. + type: list + elements: str + required: true + ipaserver_ca_subject: + description: + - Certificate Authority subject for the self-signed root CA. + default: "CN=CLDR-RootCA,O=ipaserver_domain" + ipaadmin_principal: + description: + - Kerberos principal for the FreeIPA administrator account. + default: admin + ipaadmin_password: + description: + - Password for the FreeIPA adminstrator Kerberos principal. + required: true + ipadm_password: + description: + - Password for the FreeIPA Directory Services administrator. + required: true diff --git a/roles/freeipa_server/meta/main.yml b/roles/freeipa_server/meta/main.yml deleted file mode 100644 index 5d077cf1..00000000 --- a/roles/freeipa_server/meta/main.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- - -# Copyright 2023 Cloudera, 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. - -galaxy_info: - role_name: freeipa_server - namespace: cloudera - author: Webster Mudge Jim Enright Chuck Levesque - description: > - Deployment of sidecar FreeIPA Server for Cloudera Data Platform (CDP) Base and ECS - company: Cloudera - license: Apache-2.0 - - min_ansible_version: 2.10 - - platforms: - - name: Debian - versions: all - - name: Fedora - versions: all - - name: GenericLinux - versions: all - - name: MacOSX - versions: all - - name: Ubuntu - versions: all - - galaxy_tags: - - cloudera - - cdp - - freeipa diff --git a/roles/freeipa_server/molecule/default/converge.yml b/roles/freeipa_server/molecule/default/converge.yml new file mode 100644 index 00000000..b97e7c6c --- /dev/null +++ b/roles/freeipa_server/molecule/default/converge.yml @@ -0,0 +1,46 @@ +# 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 + 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: 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/molecule/default/create.yml b/roles/freeipa_server/molecule/default/create.yml new file mode 100644 index 00000000..132438bf --- /dev/null +++ b/roles/freeipa_server/molecule/default/create.yml @@ -0,0 +1,335 @@ +# 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/molecule/default/destroy.yml b/roles/freeipa_server/molecule/default/destroy.yml new file mode 100644 index 00000000..fb95a201 --- /dev/null +++ b/roles/freeipa_server/molecule/default/destroy.yml @@ -0,0 +1,156 @@ +# 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/molecule/default/molecule.yml b/roles/freeipa_server/molecule/default/molecule.yml new file mode 100644 index 00000000..d01a7908 --- /dev/null +++ b/roles/freeipa_server/molecule/default/molecule.yml @@ -0,0 +1,46 @@ +# 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, TEST_VPC_SUBNET_ID + +# Requires AWS credentials, including region + +# Requires the following Python libraries on localhost +# - netattr +# - 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_freeipa_server-rhel9-4 + Project: Molecule testing for freeipa_server +provisioner: + name: ansible + inventory: + group_vars: + all: + # Note, ipaserver_cidr, ipaserver_forwarders, and ipaserver_recursion_acl_cidr are set in + # converge.yml due to runtime dependencies on VPC CIDR + ipaserver_domain: molecule.internal + ipaserver_realm: MOLECULE.INTERNAL + ipaadmin_password: "freebird" + ipadm_password: "dirmanbird" diff --git a/roles/freeipa_server/molecule/default/prepare.yml b/roles/freeipa_server/molecule/default/prepare.yml new file mode 100644 index 00000000..9f074ab2 --- /dev/null +++ b/roles/freeipa_server/molecule/default/prepare.yml @@ -0,0 +1,46 @@ +# 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: cloudera.exe.prereq_network_dns + vars: + 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 }}" + network_ip_address: "{{ ansible_default_ipv4.address }}" diff --git a/roles/freeipa_server/vars/default.yml b/roles/freeipa_server/molecule/default/requirements.yml similarity index 69% rename from roles/freeipa_server/vars/default.yml rename to roles/freeipa_server/molecule/default/requirements.yml index c32494e2..6464be94 100644 --- a/roles/freeipa_server/vars/default.yml +++ b/roles/freeipa_server/molecule/default/requirements.yml @@ -1,12 +1,10 @@ ---- - -# Copyright 2023 Cloudera, Inc. All Rights Reserved. +# 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 # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -14,5 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -ipaserver_packages: ["ipa-server", "python3-libselinux"] -needs_python2: true +collections: + - community.crypto + - amazon.aws + - ansible.posix + - ansible.utils + - community.general + - freeipa.ansible_freeipa diff --git a/roles/freeipa_server/molecule/default/verify.yml b/roles/freeipa_server/molecule/default/verify.yml new file mode 100644 index 00000000..caf63257 --- /dev/null +++ b/roles/freeipa_server/molecule/default/verify.yml @@ -0,0 +1,53 @@ +# 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: true + become: true + tasks: + - name: Install supporting FreeIPA packages + ansible.builtin.package: + name: "{{ item }}" + state: present + loop: + - ipa-healthcheck + + - name: Run ipa-healthcheck CLI + ansible.builtin.command: ipa-healthcheck + changed_when: false + + - name: Gather details for forward DNS + ansible.builtin.command: host {{ inventory_hostname }} + register: __forward + failed_when: __forward.stdout | string is not regex(ansible_default_ipv4.address) + changed_when: false + + - name: Gather details for reverse DNS + ansible.builtin.command: host {{ ansible_default_ipv4.address }} + register: __reverse + failed_when: __reverse.stdout | string is not regex(inventory_hostname) + changed_when: false + + - name: Execute kinit for admin + ansible.builtin.shell: "set -o pipefail; echo '{{ ipaadmin_password }}' | kinit {{ ipaadmin_principal | default('admin') }}" + register: __kinit + failed_when: __kinit.stdout | string is not regex("Password for " + ipaadmin_principal | default('admin') + "@" + ipaserver_realm) + changed_when: false + + - name: List Kerberos tickets + ansible.builtin.command: klist + register: __klist + failed_when: __klist.stdout | string is not regex("krbtgt/" + ipaserver_realm + "@" + ipaserver_realm) + changed_when: false diff --git a/roles/freeipa_server/tasks/main.yml b/roles/freeipa_server/tasks/main.yml index 1001844f..3ae94930 100644 --- a/roles/freeipa_server/tasks/main.yml +++ b/roles/freeipa_server/tasks/main.yml @@ -1,11 +1,10 @@ ---- -# Copyright 2023 Cloudera, Inc. All Rights Reserved. +# 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 # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -13,181 +12,97 @@ # See the License for the specific language governing permissions and # limitations under the License. -- name: Include OS-specific variables - ansible.builtin.include_vars: "{{ item }}" - with_first_found: - - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}.yml" - - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml" - - "{{ ansible_facts['distribution'] }}.yml" - - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_version'] }}.yml" - - "{{ ansible_facts['os_family'] }}-{{ ansible_facts['distribution_major_version'] }}.yml" - - "{{ ansible_facts['os_family'] }}.yml" - - "default.yml" - -- name: Update NSS to the latest version - ansible.builtin.package: - name: nss - state: latest - -- name: Install Python2 when needed - ansible.builtin.package: - lock_timeout: 180 - name: python2 - update_cache: true +# - name: Configure host and DNS networking +# ansible.builtin.import_role: +# name: prereq_network_dns +# vars: +# network_dns_domain: "{{ ipaserver_domain }}" +# network_dns_forwarders: "{{ ipaserver_forwarders }}" + +- name: Enable local IPv6 + ansible.posix.sysctl: + name: "{{ __sysctl.key }}" + value: "{{ __sysctl.value | string }}" state: present - when: needs_python2 - -- name: Install Python3 - ansible.builtin.package: - lock_timeout: 180 - name: python3 - update_cache: true - state: present - -- name: Permissive SELinux - ansible.posix.selinux: - policy: targeted - state: permissive + reload: true + loop: "{{ params | dict2items }}" + loop_control: + loop_var: __sysctl + label: "{{ __sysctl.key }}" + vars: + params: + net.ipv6.conf.all.disable_ipv6: 1 + net.ipv6.conf.default.disable_ipv6: 1 + net.ipv6.conf.lo.disable_ipv6: 0 -- name: Install base FreeIPA server packages +- name: Install defined FreeIPA server packages + when: ipaserver_packages is defined ansible.builtin.package: - name: "{{ ipaserver_packages }}" + name: "{{ __ipa_package }}" state: present - -- name: Set up DNS and networking - when: ipaserver_setup_dns - block: - - name: Configure RHEL systems - when: ansible_facts['os_family'] == 'RedHat' - block: - - name: Check for existence of /etc/cloud/cloud.cfg - ansible.builtin.stat: - path: /etc/cloud/cloud.cfg - register: cloud_cfg - - - name: Set cloud-init to preserve hostname (RHEL) - ansible.builtin.lineinfile: - path: /etc/cloud/cloud.cfg - regex: "^(#)?preserve_hostname" - line: "preserve_hostname: 1" - state: present - when: cloud_cfg.stat.exists - notify: restart network - - - name: Check for existence of /etc/NetworkManager/conf.d - ansible.builtin.stat: - path: /etc/NetworkManager/conf.d - register: nm_conf - - - name: Set /etc/NetworkManager/conf.d/disable-resolve.conf-managing.conf (RHEL) - ansible.builtin.copy: - dest: /etc/NetworkManager/conf.d/disable-resolve.conf-managing.conf - content: | - # Generated by Ansible - [main] - dns=none - when: nm_conf.stat.exists - notify: restart network - - # TODO Either local if dns_provider=freeipa or keep nameserver and update search only - # TODO Convert to ansible.builtin.template with role templates - - name: Set /etc/resolv.conf directly - ansible.builtin.copy: - dest: /etc/resolv.conf - content: | - # Generated by Ansible - search {{ [[name_prefix, domain] | join('.'), domain] | join(' ') }} - nameserver 127.0.0.1 - {{ ['nameserver'] | product(ipaserver_resolv_nameservers) | map('join', ' ') | join('\n') }} - notify: restart network - - # TODO Need to check-n-set vs. overwrite (forces reboot...) - - name: Set /etc/hostname to the FQDN - ansible.builtin.copy: - content: "{{ inventory_hostname }}" - dest: /etc/hostname - notify: restart network - - # TODO Need to check-n-set vs. overwrite (forces reboot...) - - name: Set /etc/hosts - ansible.builtin.copy: - dest: /etc/hosts - content: | - # Generated by Ansible - 127.0.0.1 localhost - {{ ansible_default_ipv4.address }} {{ inventory_hostname }} {{ inventory_hostname_short }} - backup: true - notify: restart network - - - name: Check for existence of /etc/dhcp/dhclient.conf - ansible.builtin.stat: - path: /etc/dhcp/dhclient.conf - register: dhclient_conf - - - name: Set /etc/dhcp/dhclient.conf for domain search and name servers - ansible.builtin.lineinfile: - path: /etc/dhcp/dhclient.conf - regex: "^(#)?{{ dhclient_entry.value }}" - line: "{{ dhclient_entry.value }}" - state: present - loop: "{{ entries | dict2items }}" - loop_control: - loop_var: dhclient_entry - label: "{{ dhclient_entry.key }}" - vars: - entries: - domain_search: supersede domain-search "{{ [[name_prefix, domain] | join('.'), domain] | join('", "') }}"; - domain_name_servers: supersede domain-name-servers 127.0.0.1, {{ ipaserver_resolv_nameservers | join(', ') }}; - when: dhclient_conf.stat.exists - notify: restart network - -- name: Flush handlers - ansible.builtin.meta: flush_handlers + loop: "{{ ipaserver_packages }}" + loop_control: + loop_var: __ipa_package - name: Set up the FreeIPA Server ansible.builtin.import_role: name: freeipa.ansible_freeipa.ipaserver vars: state: present - -- name: Configure DNS recursion for priv & pub IPs - when: - - ipaserver_setup_dns - - ipaserver_server_recursion - block: - - name: Set up DNS recursion - when: ipaserver_recursion_acl_cidr is defined - block: - - name: Configure ACL for DNS recursion option - ansible.builtin.blockinfile: - path: /etc/named/ipa-ext.conf - block: | - acl "internal_network" { - localhost; - {{ ipaserver_recursion_acl_cidr }}; - }; - notify: restart dns - - - name: Configure DNS recursion option - ansible.builtin.lineinfile: - path: /etc/named/ipa-options-ext.conf - regex: "^(#)?allow-recursion" - line: "allow-recursion { internal_network; };" - notify: restart dns - -- name: Configure global DNS for PTR synchronization and forwarding rules - when: ipaserver_setup_dns - block: - - name: Update global DNS - freeipa.ansible_freeipa.ipadnsconfig: - allow_sync_ptr: true - forward_policy: only - ipaadmin_password: "{{ ipaadmin_password }}" - rescue: - - name: Check if global DNS error is legitimate - ansible.builtin.assert: - that: - - ansible_failed_result.changed is false - - ansible_failed_result.failed is true - - ansible_failed_result.msg == 'dnsconfig_mod: no modifications to be performed' - fail_msg: ansible_failed_result.msg + ipaserver_hostname: "{{ inventory_hostname }}" + # ipaserver_no_host_dns: yes # redundant with ipaserver_setup_dns + ipaserver_install_packages: "{{ (ipaserver_packages is undefined) | ternary('true', 'false') }}" + ipaserver_setup_firewalld: false + ipaserver_setup_dns: true + ipaserver_auto_reverse: true + # ipaserver_no_forwarders: no # Only use root servers within FreeIPA + # TODO Believe this is redundant, as the variable is the same for both DNS and IPA -- + # ipaserver_auto_forwarders: yes # Use the forwarders from /etc/resolv.conf + ipaserver_forward_policy: only + ipaserver_copy_csr_to_controller: true + ipasssd_enable_dns_updates: true + # ipaclient_configure_dns_resolver: yes # Not sure if we want this (and the following two entries) + # ipaclient_dns_servers: ["{{ ansible_host }}"] + # ipaclient_cleanup_dns_resolver: yes + ipaclient_mkhomedir: true + +- name: Configure ACL for DNS recursion option + ansible.builtin.blockinfile: + path: /etc/named/ipa-ext.conf + block: "{{ lookup('template', 'recursion_acl.j2') }}" + notify: Restart DNS + +- name: Configure DNS recursion option + ansible.builtin.lineinfile: + path: /etc/named/ipa-options-ext.conf + regex: "^(#)?allow-recursion" + line: "allow-recursion { internal_network; };" + notify: Restart DNS + +- name: Create DNS zone in provisioned FreeIPA service + freeipa.ansible_freeipa.ipadnszone: + zone_name: "{{ ipaserver_domain }}" + dynamic_update: true + allow_sync_ptr: true + forward_policy: none + ipaadmin_password: "{{ ipaadmin_password }}" + +- name: Create reverse DNS zones in provisioned FreeIPA service + freeipa.ansible_freeipa.ipadnszone: + name_from_ip: "{{ cidr }}" + dynamic_update: true + allow_sync_ptr: true + ipaadmin_password: "{{ ipaadmin_password }}" + loop: "{{ ipaserver_cidr }}" + loop_control: + loop_var: cidr + +- name: Ensure PTR for the FreeIPA server entry + freeipa.ansible_freeipa.ipadnsrecord: + name: "{{ inventory_hostname_short }}" + zone_name: "{{ ipaserver_domain }}." + record_type: A + record_value: "{{ ipaclient_ip_address | default(ansible_default_ipv4.address) }}" + record_ttl: 1200 + create_reverse: true + ipaadmin_password: "{{ ipaadmin_password }}" diff --git a/roles/freeipa_server/templates/recursion_acl.j2 b/roles/freeipa_server/templates/recursion_acl.j2 new file mode 100644 index 00000000..eeda2abb --- /dev/null +++ b/roles/freeipa_server/templates/recursion_acl.j2 @@ -0,0 +1,6 @@ +acl "internal_network" { + localhost; +{% for acl_cidr in ipaserver_recursion_acl_cidr %} + {{ acl_cidr }}; +{% endfor %} +}; diff --git a/roles/freeipa_server/vars/RedHat-7.yml b/roles/freeipa_server/vars/RedHat-7.yml deleted file mode 100644 index fd5e8156..00000000 --- a/roles/freeipa_server/vars/RedHat-7.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- - -# Copyright 2023 Cloudera, 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. - -ipaserver_packages: ["ipa-server", "libselinux-python"] -needs_python2: true diff --git a/roles/freeipa_server/vars/RedHat-8.yml b/roles/freeipa_server/vars/RedHat-8.yml deleted file mode 100644 index 33bb7d08..00000000 --- a/roles/freeipa_server/vars/RedHat-8.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- - -# Copyright 2023 Cloudera, 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. - -ipaserver_packages: ["@idm:DL1/server"] -needs_python2: true diff --git a/roles/freeipa_server/vars/RedHat-9.yml b/roles/freeipa_server/vars/RedHat-9.yml deleted file mode 100644 index 7aa439fa..00000000 --- a/roles/freeipa_server/vars/RedHat-9.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- - -# Copyright 2023 Cloudera, 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. - -ipaserver_packages: ["ipa-server"] -ipaserver_packages_dns: ["ipa-server-dns"] -needs_python2: false