/home/zuul/src/opendev.org/openstack/openstack-ansible-nspawn_container_create/tasks/main.yml
---
# Copyright 2017, Rackspace US, 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
#
# 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: Gather physical host facts
  setup:
    gather_subset: "network,virtual"
  delegate_to: "{{ physical_host }}"
  delegate_facts: true
  run_once: true

- name: Pull systemd version
  command: "systemctl --version"
  changed_when: false
  register: systemd_version
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint
    - always

- name: Pull filesystem information
  command: "btrfs filesystem show --raw /var/lib/machines"
  changed_when: false
  register: filesystem
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint
    - always

- name: Set qgroup space fact
  set_fact:
    nspawn_host_qgroup_space_limit: "{{ filesystem.stdout.strip().splitlines()[-1].split()[3] }}"
  when:
    - nspawn_host_qgroup_space_limit == 'none'
  tags:
    - always

- name: Set qgroup compression fact
  set_fact:
    nspawn_host_qgroup_compression_limit: "{{ filesystem.stdout.strip().splitlines()[-1].split()[3] }}"
  when:
    - nspawn_host_qgroup_compression_limit == 'none'
  tags:
    - always

- name: Set facts
  set_fact:
    nspawn_systemd_version: "{{ systemd_version.stdout_lines[0].split()[-1] }}"
  tags:
    - always

- name: Gather variables for each operating system
  include_vars: "{{ item }}"
  with_first_found:
    - "{{ hostvars[physical_host]['ansible_distribution'] | lower }}-{{ hostvars[physical_host]['ansible_distribution_version'] | lower }}.yml"
    - "{{ hostvars[physical_host]['ansible_distribution'] | lower }}-{{ hostvars[physical_host]['ansible_distribution_major_version'] | lower }}.yml"
    - "{{ hostvars[physical_host]['ansible_os_family'] | lower }}-{{ hostvars[physical_host]['ansible_distribution_major_version'] | lower }}.yml"
    - "{{ hostvars[physical_host]['ansible_distribution'] | lower }}.yml"
    - "{{ hostvars[physical_host]['ansible_os_family'] | lower }}.yml"
  tags:
    - always

- name: Escape quote container name
  command: "systemd-escape {{ inventory_hostname }}"
  changed_when: false
  register: systemd_escape
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint
    - always

- name: Get container status
  command: machinectl status "{{ inventory_hostname }}"
  register: machinectl_container_status
  failed_when: false
  changed_when: false
  delegate_to: "{{ physical_host }}"

- name: Get container image status
  command: machinectl image-status "{{ inventory_hostname }}"
  register: machinectl_container_image_status
  failed_when: false
  changed_when: false
  delegate_to: "{{ physical_host }}"

- name: Get image status
  command: machinectl image-status "{{ container_image }}"
  register: machinectl_image_status
  failed_when: false
  changed_when: false
  delegate_to: "{{ physical_host }}"

- name: Fail if base image does not exist
  fail:
    msg: >
      The base container image "{{ container_image }}" does not exist. Check
      the name and try again.
  when:
    - machinectl_image_status.rc != 0

- name: Locate nspawn config
  stat:
    path: "/etc/systemd/nspawn/{{ inventory_hostname }}.nspawn"
  register: nspawn_config_exists
  delegate_to: "{{ physical_host }}"

- name: Slurp existing nspawn config
  slurp:
    src: "/etc/systemd/nspawn/{{ inventory_hostname }}.nspawn"
  register: nspawn_config
  changed_when: false
  delegate_to: "{{ physical_host }}"
  when:
    - nspawn_container_preserve_config | bool
    - nspawn_systemd_version | int > 219
    - nspawn_config_exists.stat.exists | bool

# Check for the existence of an nspawn configuration file. If found slurp it up
# and use it as the base nspawn config file with the option to config template
# override.
- name: Copy container config (existing)
  config_template:
    content: "{{ nspawn_config.content | b64decode }}"
    dest: "/etc/systemd/nspawn/{{ inventory_hostname }}.nspawn"
    owner: "root"
    group: "root"
    mode: "0644"
    config_overrides: "{{ container_config_overrides | default({}) }}"
    config_type: "ini"
  register: container_config_existing
  delegate_to: "{{ physical_host }}"
  when:
    - nspawn_container_preserve_config | bool
    - nspawn_config_exists.stat.exists | bool
    - nspawn_systemd_version | int > 219

# If no nspawn configuration file exists, create a new config file using the
# default template.
- name: Copy container config (new)
  config_template:
    src: templates/container_config.nspawn.j2
    dest: "/etc/systemd/nspawn/{{ inventory_hostname }}.nspawn"
    owner: "root"
    group: "root"
    mode: "0644"
    config_overrides: "{{ container_config_overrides | default({}) }}"
    config_type: "ini"
  register: container_config_new
  delegate_to: "{{ physical_host }}"
  when:
    - not nspawn_container_preserve_config | bool or
      not nspawn_config_exists.stat.exists | bool
    - nspawn_systemd_version | int > 219

- name: Copy container config (old)
  template:
    src: templates/container_config_old.nspawn.j2
    dest: "/etc/systemd/system/systemd-nspawn@{{ systemd_escape.stdout }}.service"
    owner: "root"
    group: "root"
    mode: "0644"
  register: container_config_old
  delegate_to: "{{ physical_host }}"
  when:
    - nspawn_systemd_version | int < 220

- name: Notice existing container config changed
  debug:
    msg: >-
      The existing container config has changed. While this change has been
      noticed, no restart has been performed. By not restarting the container
      the system is preserving the state of the environment by not causing
      unexpected downtime. For these changes to go into effect the container
      must be restarted. This can be done using the following command,
      `machinectl [poweroff|start] {{ inventory_hostname }}`. To disable the
      configation saving set `nspawn_container_preserve_config` to false.
  when:
    - container_config_existing is changed

- name: Clone the base container image
  command: machinectl clone "{{ container_image }}" "{{ inventory_hostname }}"
  when:
    - machinectl_container_image_status.rc != 0
  register: machinectl_container_clone
  retries: 3
  delay: 2
  until: machinectl_container_clone is success
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint

- name: Set the qgroup limits
  block:
    - name: Set the qgroup size|compression limits on machines
      command: "btrfs qgroup limit {{ item }} /var/lib/machines/{{ inventory_hostname }}"
      changed_when: false
      delegate_to: "{{ physical_host }}"
      with_items:
        - "-e {{ nspawn_host_qgroup_space_limit }}"
        - "-c {{ nspawn_host_qgroup_compression_limit }}"
      when:
        - not nspawn_host_machine_quota_disabled
  rescue:
    - name: Notice regarding quota system
      debug:
        msg: >-
          There was an error processing the setup of qgroups. Check the system
          to ensure they're available otherwise disable the quota system by
          setting `nspawn_host_machine_quota_disabled` to true.

- name: Container directories
  file:
    path: "{{ item }}"
    state: "directory"
  with_items:
    - "/openstack/{{ inventory_hostname }}"
    - "/openstack/backup/{{ inventory_hostname }}"
    - "/openstack/log/{{ inventory_hostname }}"
    - "/var/lib/machines/{{ inventory_hostname }}/var/log/journal"
    - "/var/lib/machines/{{ inventory_hostname }}/etc/systemd/network"
    - "/var/lib/machines/{{ inventory_hostname }}/etc/systemd/nspawn"
    - "/var/lib/machines/{{ inventory_hostname }}/var/lib/dbus"
  delegate_to: "{{ physical_host }}"

- name: Container RO bind path cleanup
  file:
    path: "/var/lib/machines/{{ inventory_hostname }}{{ item.dest }}"
    state: "absent"
  with_items: "{{ nspawn_read_only_host_bindmount }}"
  delegate_to: "{{ physical_host }}"
  when:
    - machinectl_container_status.rc != 0

- name: Container inner service directories
  file:
    path: "/var/lib/machines/{{ inventory_hostname }}/{{ item.bind_dir_path }}"
    state: "directory"
  with_items: "{{ container_default_bind_mounts | union(container_bind_mounts | default([])) }}"
  delegate_to: "{{ physical_host }}"

- name: Container outer service directories
  file:
    path: "{{ item.mount_path }}"
    state: "directory"
  with_items: "{{ container_default_bind_mounts | union(container_bind_mounts | default([])) }}"
  delegate_to: "{{ physical_host }}"

- name: Generate machine-id
  command: "systemd-machine-id-setup --root=/var/lib/machines/{{ inventory_hostname }}"
  args:
    creates: "/var/lib/machines/{{ inventory_hostname }}/etc/machine-id"
  register: machineid_set
  retries: 3
  delay: 2
  until: machineid_set is success
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint

- name: Create dbus machine-id
  copy:
    src: "/var/lib/machines/{{ inventory_hostname }}/etc/machine-id"
    dest: "/var/lib/machines/{{ inventory_hostname }}/var/lib/dbus/machine-id"
    mode: "0444"
    remote_src: "yes"
  remote_user: root
  delegate_to: "{{ physical_host }}"

- name: Create resolved link
  file:
    src: "/var/run/systemd/resolve/resolv.conf"
    dest: "/var/lib/machines/{{ inventory_hostname }}/etc/resolv.conf"
    force: true
    follow: false
    state: link
  delegate_to: "{{ physical_host }}"
  when:
    - nspawn_container_enable_resolved | bool

# Some distros do not have access to systemd-resolved. If the option
# `nspawn_container_enable_resolved` is disabled this will ensure functionality
# in the absence of modern systemd.
- name: Legacy resolvers
  block:
    - name: Check resolv.conf
      stat:
        path: "/var/lib/machines/{{ inventory_hostname }}/etc/resolv.conf"
      delegate_to: "{{ physical_host }}"
      register: nspawn_resolv_conf

    - name: Remove resolv.conf link
      file:
        path: "/var/lib/machines/{{ inventory_hostname }}/etc/resolv.conf"
        state: absent
      delegate_to: "{{ physical_host }}"
      when:
        - nspawn_resolv_conf.stat.islnk is defined and
          nspawn_resolv_conf.stat.islnk

    - name: Place resolv.conf
      copy:
        content: |
          nameserver {{ hostvars[physical_host]['ansible_mv_' + nspawn_networks['nspawn_address']['bridge']]['ipv4']['address'] }}
          search {{ container_domain }}
        dest: "/var/lib/machines/{{ inventory_hostname }}/etc/resolv.conf"
      delegate_to: "{{ physical_host }}"
  when:
    - not nspawn_container_enable_resolved | bool

- name: Start new container (enable)
  systemd:
    daemon_reload: yes
    name: "systemd-nspawn@{{ systemd_escape.stdout }}"
    state: "{{ ((machinectl_container_clone is changed or container_config_new is changed or container_config_old is changed) | default(false)) | ternary('restarted', 'started') }}"
    enabled: true
  register: machinectl_start
  retries: 5
  delay: 2
  until: machinectl_start is success
  delegate_to: "{{ physical_host }}"

- name: Generate hostname
  command: >-
    hostnamectl --machine="{{ inventory_hostname }}" {{ item }} --pretty --static --transient
  with_items:
    - "set-hostname {{ inventory_hostname | replace('_', '-') }}.{{ container_domain }}"
    - "set-location {{ physical_host }}"
    - "set-chassis container"
    - "set-deployment {{ container_domain }}"
    - "set-icon-name container"
  register: hostnamectl_set
  retries: 3
  delay: 2
  until: hostnamectl_set is success
  delegate_to: "{{ physical_host }}"
  tags:
    - skip_ansible_lint

# This point the container is running. Delegation should no longer be required.
# ==============================================================================
- name: Run the systemd-networkd role
  include_role:
    name: systemd_networkd
  vars:
    systemd_networkd_prefix: "nspawn_container"
    systemd_run_networkd: true
    systemd_interface_cleanup: true
    systemd_resolved_available: "{{ nspawn_container_enable_resolved }}"
    systemd_resolved:
      DNS: "{{ hostvars[physical_host]['ansible_mv_' + nspawn_networks['nspawn_address']['bridge']]['ipv4']['address'] }}"
      Domains: "{{ container_domain }}"
    systemd_networks: |-
      {% set seen_networks = [] %}
      {% set _networks = [] %}
      {# All nspawn_networks and container_extra_networks will be iterated over. #}
      {# If a device is found, a networkd config will be generated for it. #}
      {% for _, value in nspawn_combined_networks.items() %}
      {%   set netname = value.interface | default('mv-' + value.bridge.split('br-')[-1]) %}
      {%   set _network = {'interface': 'mv-' + netname} %}
      {%   if netname not in seen_networks %}
      {%     set _ = seen_networks.append(netname) %}
      {%     if value.address is defined %}
      {%         set _ = _network.__setitem__('address', value.address) %}
      {%         if (value.netmask is defined) and (_network.address != 'dhcp') %}
      {%           set _ = _network.__setitem__('netmask', value.netmask) %}
      {%           set prefix = (value.address + '/' + value.netmask) | ipaddr('prefix') %}
      {%           set _ = _network.__setitem__('address', [value.address + '/' + prefix | string]) %}
      {%         endif %}
      {%     endif %}
      {%     set _ = _network.__setitem__('usedns', (value.usedns | default(true) | bool) | ternary('yes', 'no')) %}
      {%     set _ = _network.__setitem__('static_routes', value.static_routes | default([])) %}
      {%     if value.gateway is defined %}
      {%       set _ = _network.__setitem__('gateway', value.gateway) %}
      {%     endif %}
      {%     set _ = _network.__setitem__('mtu', value.mtu | default(1500 | string)) %}
      {%     set _ = _networks.append(_network) %}
      {%   endif %}
      {% endfor %}
      {{ _networks | sort(attribute='interface') }}
  tags:
    - network-config

- name: Force all notified handlers now
  meta: flush_handlers

- name: (RE)Gather facts post setup
  setup:
    gather_subset: "network,hardware,virtual"