Subsections of Storage

Configuring Storage Advanced Exercise

Configuring Storage Advanced Exercise

To work on this exercise, you need managed machines with an additional disk device: add a 10 GB second disk to host ansible2 and a 5 GB second disk to host ansible3. The exercise assumes the name of the second disk is /dev/sdb; if a different disk name is used in your configuration, change this according to your specifications.

Exercise 15-3 Setting Up an Advanced Storage Solution

In this exercise you need to set up a storage solution that meets the following requirements:

• Tasks in this playbook should be executed only on hosts where the device /dev/sdb exists.

• If no device /dev/sdb exists, the playbook should print “device sdb not present” and stop executing tasks on that host.

• Configure the device with one partition that includes all available disk space.

• Create an LVM volume group with the name vgfiles.

• If the volume group is bigger than 5 GB, create an LVM logical volume with the name lvfiles and a size of 6 GB. Note that you must check the LVM volume group size and not the /dev/sdb1 size because in theory you could have multiple block devices in a volume group.

• If the volume group is equal to or smaller than 5 GB, create an LVM logical volume with the name lvfiles and a size of 3 GB.

• Format the volume with the XFS file system.

• Mount it on the /files directory.

1. Check the size of the volume group. You can, however, write a test that works on a default volume group, and that is what you’re going to do first, using the name of the default volume group on CentOS 8, which is “cl”. The purpose is to test the constructions, which is why it doesn’t really matter that the two tasks have overlapping when statements. So create a file with the name exercise153-dev1.yaml and give it the following contents:

---
- name: get vg sizes
  hosts: all
  tasks:
  - name: find small vgroup sizes
    debug:
      msg: volume group smaller than or equal to 20G
    when:
    - ansible_facts[’lvm’][’vgs’][’cl’] is defined
    - ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00
  - name: find large vgroup size
    debug:
      msg: volume group larger than or equal to 19G
    when:
    - ansible_facts[’lvm’][’vgs’][’cl’] is defined
    - ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] >= 19.00

2. Run the playbook by using ansible-playbook exercise153-dev1.yaml. You’ll notice that it fails with the error shown in Listing 15-12.

Listing 15-12 exercise153-dev1.yaml Failure Message

TASK [find small vgroups sizes] ***************************************************
fatal: [ansible1]: FAILED! => \{\"msg": "The conditional check ’ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] \\<= 20.00’ failed. The error was: Unexpected templating type error occurred on ({% if ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00 %} True {% else %} False {% endif %}): ’<=’ not supported between instances of ’AnsibleUnsafeText’ and ’float’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-dev1.yaml’: line 5, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - name: find small vgroups sizes\n    ^ here\n"}
fatal: [ansible2]: FAILED! => {"msg": "The conditional check ’ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00’ failed. The error was: Unexpected templating type error occurred on ({% if ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00 %} True {% else %} False {% endif %}): ’<=’ not supported between instances of ’AnsibleUnsafeText’ and ’float’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-dev1.yaml’: line 5, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - name: find small vgroups sizes\n    ^ here\n"}
fatal: [ansible3]: FAILED! => {"msg": "The conditional check ’ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00’ failed. The error was: Unexpected templating type error occurred on ({% if ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00 %} True {% else %} False {% endif %}): ’<=’ not supported between instances of ’AnsibleUnsafeText’ and ’float’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-dev1.yaml’: line 5, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - name: find small vgroups sizes\n    ^ here\n"}
fatal: [ansible4]: FAILED! => {"msg": "The conditional check ’ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00’ failed. The error was: Unexpected templating type error occurred on ({% if ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] <= 20.00 %} True {% else %} False {% endif %}): ’<=’ not supported between instances of ’AnsibleUnsafeText’ and ’float’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-dev1.yaml’: line 5, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n  - name: find small vgroups sizes\n    ^ here\n"}
skipping: [ansible5]
skipping: [ansible6]

TASK [find large vgroups sizes] ***************************************************
skipping: [ansible5]
skipping: [ansible6]

:::

3. As you can see in the errors in Listing 15-12, there are two problems in the playbook. The first problem is that there is no ignore_errors in the failing play, which means that only hosts that haven’t failed will reach the next task. The second error is the “Unexpected templating error”. The playbook in its current form is trying to perform a logical test to compare the value of two variables that have an incompatible variable type. The Ansible fact has the type “AnsibleUnsafeText”, and the value of 20.00 is a float, not an integer. To make this test work, you must force the type of both variables to be set to an integer. Now write exercise153-dev2.yaml where this is happening; notice the use of the filter int, which is essential for the success of this playbook:

---
- name: get vg sizes
  ignore_errors: yes
  hosts: all
  tasks:
  - name: set vgroup sizes in variables
    set_fact:
      vgsize: "{{ ansible_facts[’lvm’][’vgs’][’cl’][’size_g’] | int }}"
  - name: debug this
    debug:
      msg: the value of vgsize is {{ vgsize }}
  - name: testing big vgsize value
    debug:
      msg: the value of vgsize is bigger than 5
    when: vgsize | int > 5
  - name: testing small vgsize value
    debug:
      msg: the value of vgsize is smaller than 5
    when: vgsize | int <= 5

4. Run this playbook. You’ll notice it skips and ignores some tasks but doesn’t fail anywhere, which means that this playbook—although absolutely not perfect—is usable as an example to test the size of the vgfiles volume group later in this exercise.

5. Now that you’ve tested the most complex part of the assignment, you can start writing the rest of the playbook. Do this in a new file with the name exercise153.yaml. Because this playbook has quite a few tasks to accomplish, it might be smart to define the rough structure and ensure that all elements that are needed later are at least documented so that you can later work out the details. So let’s start with the first part, where the play header is defined, as well as the rough structure. This is the part where you still have the global overview of all the tasks in this requirement, so you need to make sure you won’t forget about them later, which is a real risk if you’ve been into the details too much for too long.

---
- name: set up hosts that have an sdb device
  hosts: all
  tasks:
  - name: getting out with a nice failure message if there is no second disk
    # fail:
    debug:
      msg: write a nice failure message and a when test here
    # when: something
  - name: create a partition
    #parted
    debug:
      msg: creating the partition
  - name: create a volume group
    #lvg:
    debug:
      msg: creating the volume group
  - name: get the vg size and store it in a variable
    #set_fact:
    debug:
      msg: storing variable as an integer
  - name: create an LVM on big volume groups
    #lvol:
    debug:
      msg: use when statement to create 6g lvol if vsize > 5
  - name: create an LVM on small volume groups
    #lvol:
    debug:
      msg: use when statement to create 3g lvol if vsize <= 5
  - name: formatting the XFS filesystem
    # filesystem
    debug:
      msg: creating the filesystem
  - name: mounting /dev/vgfiles/lvfiles
    # mount:
    debug:
      msg: mounting the volume

6. The advantage of a generic structure like the one you just defined is that you can run a test at any moment. Now it’s time to fill it in. Start with the play header and then check whether /dev/sdb is present on the managed system:

---
- name: setup up hosts that have an sdb device
  hosts: all
  tasks:
  - name: getting out with a nice failure message if there is no second disk
    fail:
      msg: there is no second disk
    when: ansible_facts[’devices’][’sdb’] is not defined

7. At this point I recommend you run a test to see that the playbook really does skip all hosts that don’t have a second disk device. Use ansible-playbook exercise153.yaml to do so and observe that you see a lot of skipping messages in the output.

8. If all is well so far, you can continue to create the partition and create the logical volume group as well. Here are the tasks you need to enter. Notice that no size is specified at any point, which means that the partition and the volume group will be allowed to grow up to the maximum size.

- name: create a partition
  parted:
    device: /dev/sdb
    number: 1
    state: present
- name: create a volume group
  lvg:
    pvs: /dev/sdb1
    vg: vgfiles

9. At this point you can insert the part where you save the volume group size into a variable, which can be used in the when statement that will occur in one of the next tasks. Also, because it’s good to check a lot while you are writing a complex playbook, use the debug module to verify the results.

- name: get vg size and convert to integer in new variable
  set_fact:
    vgsize: "{{ ansible_facts[’lvm’][’vgs’][’vgfiles’][’size_g’] | int }}"
- name: show vgsize value
  debug:
    var: "{{ vgsize }}"

10. After this important step, it’s time to run a test. If you need it, you can find a sample playbook of the state so far named exercise153-step9.yaml in the GitHub repository at https://github.com/sandervanvugt/rhce8-book, but it’s obviously much better and recommended to run your own code! So use ansible-playbook exercise153.yaml to verify what you’ve got so far. Notice that you must make sure to run it on hosts that don’t have any configuration yet. If a configuration already exists, that will most likely give you false positives! If you want to make sure all is clean, use ansible all -a “dd if=/dev/zero of=/dev/sdb bs=1M count=10” to wipe the /dev/sdb devices on your managed hosts, followed by ansible all -m reboot to reboot all of them before you test. The purpose of all this is that at this point you see the error message shown in Listing 15-13. Before moving on to the next step, try to understand what is going wrong.

Listing 15-13 Error Message After Exercise 15-3 Step 10

::: pre_1

TASK [get vg size and convert to integer in new variable] ******************************
fatal: [ansible2]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: ’dict object’ has no attribute ’vgfiles’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-step9.yaml’: line 18, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n      vg: vgfiles\n  - name: get vg size and convert to integer in new variable\n    ^ here\n"}
fatal: [ansible3]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: ’dict object’ has no attribute ’vgfiles’\n\nThe error appears to be in ’/home/ansible/rhce8-book/exercise153-step9.yaml’: line 18, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n      vg: vgfiles\n  - name: get vg size and convert to integer in new variable\n    ^ here\n"}

:::

11. As you can see, the variable that you are trying to use has no value yet. And that is for the simple reason that fact gathering is required to set the variable, and fact gathering is happening at the beginning of the playbook. At this point, you need to add a task that runs the setup module right after creating the volume group, and then you can try again. In the output you have to look at the [show vgsize value] task, which should look all right now, and everything after that can be ignored. See exercise153-step11.yaml in the GitHub repository if you need the complete example.

# skipping first part of the playbook in this listing
- name: create a volume group
  lvg:
    pvs: /dev/sdb1
    vg: vgfiles
- name: run the setup module so that we can use updated facts
  setup:
- name: get vg size and convert to integer in new variable
  set_fact:
    vgsize: "{{ ansible_facts[’lvm’][’vgs’][’vgfiles’][’size_g’] | int }}"
- name: show vgsize value
  debug:
    var: "{{ vgsize }}"

12. Assuming that all went well, you can now add the two conditional tests, where according to the vgsize value, the lvol module is used to create the logical volumes:

- name: create an LVM on big volume groups
  lvol:
    vg: vgfiles
    lv: lvfiles
    size: 6g
  when: vgsize | int > 5
- name: create an LVM on small volume groups
  lvol:
    vg: vgfiles
    lv: lvfiles
    size: 3g
  when: vgsize | int <= 5

13. Add the tasks to format the volumes with the XFS file system and mount them:

- name: formatting the XFS filesystem
  filesystem:
    dev: /dev/vgfiles/lvfiles
    fstype: xfs
- name: mounting /dev/vgfile/lvfiles
  mount:
    path: /file
    state: mounted
    src: /dev/vgfiles/lvfiles
    fstype: xfs

14. That’s all! The playbook is now ready for use. Run it by using ansible-playbook exercise153.yaml and verify its output.

15. Use the ad hoc command ansible ansible2,ansible3 -a “lvs” to show LVM logical volume sizes on the machines with the additional hard drive. You should see that all has worked out well and you are done! :::

Discovering storage related facts

Table 15-2 Modules for Managing Storage

Image Image

To make sure that your playbook is applied to the right devices, you first need to find which devices are available on your managed system.

After you find them, you can use conditionals to make sure that tasks are executed on the right devices.

Ansible_facts related to storage

ansible_devices

  • Available storage and device info ansible_device_links
  • info on how to access storage and other device info ansible_mounts
  • Mount point info

ansible ansible1 -m setup -a 'filter=ansible_devices'

  • Find generic information about storage devices.

  • The filter argument to the setup module uses a shell-style wildcard to search for matching items and for that reason can search in the highest level facts, such as ansible_devices, but it is incapable of further specifying what is searched for. For that reason, in the filter argument to the setup module, you cannot use a construction like ansible ansible1 -m setup -a "filter=ansible_devices.sda" which is common when looking up the variable in conditional statements.

Assert module

  • show an error message if a device does not exist and to perform a task if the device exists.
  • For an easier solution, you can also use a when statement to look for the existence of a device.
  • The advantage of using the assert module is that an error message can be printed if the condition is not met.

Listing 15-2 Using assert to Run a Task Only If a Device Exists

    ---
    - name: search for /dev/sdb continue only if it is found
      hosts: all
      vars:
        disk_name: sdb
      tasks:
      - name: abort if second disk does not exist
        assert:
          that:
            - "ansible_facts['devices']['{{ disk_name }}'] is defined"
          fail_msg: second hard disk not found
      - debug:
          msg: "{{ disk_name }} was found, lets continue"

Write a playbook that finds out the name of the disk device and puts that in a variable that you can work with further on in the playbook.

The set_fact argument comes in handy to do so.

You can use it in combination with a when conditional statement to store a detected device name in a variable.

Storing the Detected Disk Device Name in a Variable

    ---
    - name: define variable according to diskname detected
      hosts: all
      tasks:
      - ignore_errors: yes
        set_fact:
          disk2name: sdb
        when: ansible_facts[’devices’][’sdb’]
  - name: Detect secondary disk name
    ignore_errors: yes
    set_fact:
      disk2name: vda
    when: ansible_facts['devices']['vda'] is defined

  - name: Search for second disk, continue only if it is found
    assert:
      that:
        - "ansible_facts['devices'][disk2name] is defined"
      fail_msg: second hard disk not found

  - name: Debug detected disk
    debug:
      msg: "{{ disk2name }} was found. Moving forward."
~                                                      

Next, see Managing Partitions and LVM

Managing Partitions and LVM

Managing Partitions and LVM

After detecting the disk device that needs to be used, you can move on and start creating partitions and logical volumes.

  • partition a disk using the parted module,
  • work with the lvg and lvol modules to manage LVM logical volumes,
  • create file systems using the filesystem module and mount them using the mount module
  • manage swap storage.

Creating Partitions

Parted Module name:

  • Assign unique name, required for GPT partitions label:
  • type of partition table, msdos is default, gpt for gpt device:
  • Device where you are creating partition number:
  • partition number state:
  • present or absent to add/remove

part_start:

  • Starting position expressed as an offset from the beginning of the disk part_end:
  • Where to end the partition If these arguments are not used, the partition starts at 0% and ends at 100% of the available disk space.

flags:

  • Set specific partition properties such as LVM partition type.
  • Required for LVM partition type
      - name: create new partition
        parted:
          name: files
          label: gpt
          device: /dev/sdb
          number: 1
          state: present
          part_start: 1MiB
          part_end: 2GiB
      - name: create another new partition
        parted:
          name: swap
          label: gpt
          device: /dev/sdb
          number: 2
          state: present
          part_start: 2GiB
          part_end: 4GiB
          flags: [ lvm ]

Managing Volume Groups and LVM Logical Volumes

lvg module

  • manage LVM logical volumes
  • managing LVM volume groups

lvol module

  • managing LVM logical volumes.

Creating an LVM volume group

  • vg argument to set the name of the volume group
  • pvs argument to identify the physical volume (which is often a partition or a disk device) on which the volume group needs to be created.
  • May need to specify the pesize to refer to the size of the physical extents.
- name: create a volume group
  lvg:
    vg: vgdata
    pesize: "8"
    pvs: /dev/sdb1

After you create an LVM volume group, you can create LVM logical volumes.

lvol Common Options: lv

  • Name of the LV pvs
  • comma separated list of pvs, if it is a partition then it should have the lvm option set resizefs
  • Indicates whether to resize filesystem when the lv is expanded size
  • size of the lv snapshot
  • specify name if this lv is a snapshot vg
  • VG is which the lv should be created

Creating an LVM Logical Volume

- name: create a logical volume
    lvol:
      lv: lvdata
      size: 100%FREE
      vg: vgdata

Creating and Mounting File Systems

filesystem module

  • Supports creating as well as resizing file systems.

Options: dev

  • block device name fstype
  • filesystem type opts
  • options passed to mkfs command resizefs
  • Extends the filesystem if set to yes. Extended to the current block size

Creating an XFS File System

- name: create an XFS filesystem
  filesystem:
    dev: /dev/vgdata/lvdata
    fstype: xfs

Mounting a filesystem

mount module.

  • Used to mount a filesystem

Options: fstype

  • Filesystem type is not automatically dedected.
  • Used to specify filesystem type path
  • directory to mount the filesystem to src
  • device to be mounted state
  • Current mount state
  • mounted to mount device now
  • present to set in /etc/fstab but not mount it now
      - name: mount the filesystem
        mount:
          src: /dev/vgdata/lvdata
          fstype: xfs
          state: mounted
          path: /mydir

Configuring Swap Space

  • To set up swap space, you first must format a device as swap space and next mount the swap space.

  • To format a device as swap space, you use the filesystem module.

  • There is no specific Ansible module to activate theswap space, so you use the command module to run the Linux swapon command.

  • Because adding swap space is not always required, it can be done in a conditional statement.

  • In the statement, use the ansible_swaptotal_mb fact to discover how much swap is actually available.

  • If that amount falls below a specific threshold, the swap space can be created and activated.

A conditional check is performed, and additional swap space is configured if the current amount of swap space is lower than 256 MiB.

    ---
    - name: configure swap storage
      hosts: ansible2
      tasks:
      - name: setup swap
        block:
        - name: make the swap filesystem
          filesystem:
            fstype: swap
            dev: /dev/sdb1
        - name: activate swap space
          command: swapon /dev/sdb1
        when: ansible_swaptotal_mb < 256

Run an ad hoc command to ensure that /dev/sdb on the target host is empty:

ansible ansible2 -a "dd if=/dev/zero of=/dev/sdb bs=1M count=10"

To make sure that you don’t get any errors about partitions that are in use, also reboot the target host:

ansible ansible2 -m reboot
  • Lack of idempotency if the size is specified as 100%FREE, which is a relative value, not an absolute value.
  • This value works the first time you run the playbook, but it does not the second time you run the playbook.
  • Because no free space is available, the LVM layer interprets the task as if you wanted to create a logical volume with a size of 0 MiB and will complain about that. To ensure that plays are written in an idempotent way, make sure that you use absolute values, not relative values.

NFS Setup

Server hosting the storage:

--- 
  - name: Install Packages
    package:
      name:
        - nfs-utils
      state: present

  - name: Ensure directories to export exist
    file:  # noqa 208
      path: "{{ item }}"
      state: directory
    with_items: "{{ nfs_exports | map('split') | map('first') | unique }}"
  
  - name: Copy exports file
    template:
      src: exports.j2
      dest: /etc/exports
      owner: root
      group: root
      mode: 0644
    notify: reload nfs

  - name: Add firewall rule to enable NFS service
    ansible.posix.firewalld:
      immediate: true
      state: enabled
      permanent: true
      service: nfs
    notify: reload firewalld

  - name: Start and enable NFS service
    service:
      name: nfs-server
      state: started
      enabled: yes
     when: nfs_exports|length > 0

  - name: Set SELinux boolean for NFS
    ansible.posix.seboolean:
      name: nfs_export_all_rw
      state: yes
      persistent: yes

  - name: install required package for sefcontext module
    yum:
      name: policycoreutils-python-utils
      state: present

  - name: Set proper SELinux context on export dir
    sefcontext:
      target: /{{ item }}(/.*)?
      setype: nfs_t
      state: present
    notify: run restorecon
    with_items: "{{ nfs_exports | map('split') | map('first') | unique }}"
{% for host in nfs_hosts %}
/data {{ host }} (rw,wdelay,root_squash,no_subtree_check,sec=sys,rw,root_squash,no_all_squash)
{% endfor %}

Variables: nfs_exports:

  • /data server(rw,wdelay,root_squash,no_subtree_check,sec=sys,rw,root_squash,no_all_squash)

Handlers

---
- name: reload nfs
  command: 'exportfs -ra'
  
- name: reload firewalld
  command: firewall-cmd --reload

- name: run restorecon
  command: restorecon -Rv /codata

storage:

 name: Detect secondary disk name
    ignore_errors: yes
    set_fact:
      disk2name: vda
    when: ansible_facts['devices']['vda'] is defined

  - name: Search for second disk, continue only if it is found
    assert:
      that:
        - disk2name is defined
      fail_msg: second hard disk not found

  - name: Debug detected disk
    debug:
      msg: "{{ disk2name }} was found. Moving forward."  

  - name: Create LVM and partitions
    block:
    - name: Create LVM Partition on second disk
      parted: 
        name: data
        label: gpt
        device: /dev/{{ disk2name }}
        number: 1
        state: present
        flags: [ lvm ]

    - name: Create an LVM volume group
      lvg:
        vg: vgcodata
        pvs: /dev/{{ disk2name }}1

    - name: Create lv
      lvol:
        lv: lvdata
        size: 100%FREE
        vg: vgdata

    - name: create filesystem
      filesystem:
        dev: /dev/vgdata/lvdata
        fstype: xfs

    when: ansible_facts['devices']['vda']['partitions'] is not defined
    
  - name: Create data directory
    file:
      dest: /data
      mode: 777
      state: directory
    
  - name: Mount the filesystem
    mount:
      src: /dev/vgdata/lvdata
      fstype: xfs
      state: present
      path: /data
  
  - name: Set permissions on mounted filesystem
    file:
      path: /data
      state: directory
      mode: '0777'
    ```