ansible parted automate partition creation and get number and start dynamically

1.1k Views Asked by At

I need to automate the partition creation in several hundred server where a file system needs to be added.

the idea is have ansible get the number of the next partition and the start based on the output of parted info.

---
- hosts: all
  become: true

  tasks:
    - name: get partition info
      parted:
         device: /dev/sda
         unit: GiB
      register: sda_info

    - name: create new partition
      parted:
        device: /dev/sda
        number: ???
        part_start: ???
        part_end: 3GiB
        state: present
      loop: '{{ sda_info.partitions }}

this is the output of sda_info

"sda_info": {
    "changed": false,
    "disk": {
        "dev": "/dev/sda",
        "logical_block": 512,
        "model": "VMware Virtual disk",
        "physical_block": 512,
        "size": 49.0,
        "table": "msdos",
        "unit": "gib"
    },
    "failed": false,
    "partitions": [
        {
            "begin": 0.0,
            "end": 0.49,
            "flags": [
                "boot"
            ],
            "fstype": "ext4",
            "name": "",
            "num": 1,
            "size": 0.49,
            "unit": "gib"
        },
        {
            "begin": 0.49,
            "end": 40.0,
            "flags": [
                "lvm"
            ],
            "fstype": "",
            "name": "",
            "num": 2,
            "size": 39.5,
            "unit": "gib"
        },
        {
            "begin": 40.0,
            "end": 46.0,
            "flags": [],
            "fstype": "",
            "name": "",
            "num": 3,
            "size": 6.0,
            "unit": "gib"
        }
    ],
    "script": "unit 'GiB' print"

how can I calculate the number and the start based on the output of the sda_info.

In the example in hand the parted should be: item.num +1 = 4 and part_start = 46 where the last partition on the disk ended.

every server has different disk structure so I need to get this dynamically.

i am thinking of using last for the number eg:

'{{ (sda_info.partitions|last).num +1 }}'
'{{ (sda_info.partitions|last).end }}'

but how to make sure that the sda_info.partitions is output in order and last is actually the highest number and not just the last item in dictionary and end up destroy a bad partition?

sda_info.partition a dictionary and dictionary are not ordered in python.

3

There are 3 best solutions below

1
On

Here is simple playbook I've used with unit: s (sector as int) to avoid some errors because of floats:

- name: get partition info
  community.general.parted:
    device: /dev/sda
    unit: s
  register: sda_info
- name: create new partition
  community.general.parted:
    device: /dev/sda
    label: gpt
    number: "{{ lastsorted.num | int + 1 }}"
    part_start: "{{ lastsorted.end | int + 1}}s"
    part_end: "100%"
    state: present
  vars:
    lastsorted: "{{ sda_info.partitions|sort(attribute='num')|last }}"

It creates a new partition at disk end using all free space.

0
On

you just add sort like this:

- debug:
    msg: "maxnum: {{ maxnum | int + 1 }}, maxend: {{ maxend |float}}"
  vars:
    maxnum: "{{ ((sda_info.partitions|sort(attribute='num'))|last).num }}"
    maxend: "{{ ((sda_info.partitions|sort(attribute='end'))|last).end }}"

if you are sure maxnum and maxend are in same record: you could simplify

- debug:
    msg: "maxnum: {{ lastsorted.num | int + 1 }}, maxend: {{ lastsorted.end |float}}"
  vars:
    lastsorted: "{{ sda_info.partitions|sort(attribute='num')|last }}"
2
On

See the example of a complete playbook below. The idea is to split the play into two parts. In the first block get the disk info and write it to a file at the controller. In the second block create the partition. The reason is to make the code safer, more robust, and more flexible.

  • The first part is safe. There are no changes on the remote hosts. You can run it once on all hosts and display the debug to check the details. For example, let's test it on a 4GB flash disk
shell> parted -l
Model: Generic Flash Disk (scsi)
Disk /dev/sdb: 4089MB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags: 

Number  Start  End     Size    Type     File system  Flags
 1      512B   34.1MB  34.1MB  primary  fat32        esp

Set the variables

disk: sdb
part_size: 10
unit: MB

and run the playbook

shell> ansible-playbook pb.yml -e debug=true -e get_info=true

PLAY [localhost] *****************************************************************************

TASK [debug] *********************************************************************************
ok: [localhost] => 
  msg: |-
    disk: /dev/sdb
    part_unit: MB
    cache_update: False
    cache_path: /tmp/localhost-sdb-part_info.json

TASK [get partition info] ********************************************************************
ok: [localhost]

TASK [debug] *********************************************************************************
ok: [localhost] => 
  part_info:
    changed: false
    disk:
      dev: /dev/sdb
      logical_block: 512
      model: Generic Flash Disk
      physical_block: 512
      size: 4089.0
      table: msdos
      unit: mb
    failed: false
    partitions:
    - begin: 0.0
      end: 34.1
      flags:
      - esp
      fstype: fat32
      name: ''
      num: 1
      size: 34.1
      unit: mb
    script: unit 'MB' print

TASK [debug] *********************************************************************************
ok: [localhost] => 
  msg: |-
    part_size: 10
    last_number: 1
    last_end: 34.1
    next_number: 2
    next_part_start: 34.1MB
    next_part_end: 44.1MB

TASK [copy] **********************************************************************************
changed: [localhost]

TASK [set_fact] ******************************************************************************
skipping: [localhost]

TASK [debug] *********************************************************************************
skipping: [localhost]

TASK [debug] *********************************************************************************
skipping: [localhost]

TASK [create new partition] ******************************************************************
skipping: [localhost]

TASK [debug] *********************************************************************************
skipping: [localhost]

PLAY RECAP ***********************************************************************************
localhost: ok=5    changed=1    unreachable=0    failed=0    skipped=5    rescued=0    ignored=0

You can see that the task copy is changed. This means the file was written on a controller. Take a look

shell> cat /tmp/localhost-sdb-part_info.json 
{"changed": false, "disk": {"dev": "/dev/sdb", "size": 4089.0, "unit": "mb", "table": "msdos", "model": "Generic Flash Disk", "logical_block": 512, "physical_block": 512}, "partitions": [{"num": 1, "begin": 0.0, "end": 34.1, "size": 34.1, "fstype": "fat32", "name": "", "flags": ["esp"], "unit": "mb"}], "script": "unit 'MB' print", "failed": false}

The copy parameter force is set to false. This means the file won't be replaced. The file will only be transferred if the destination does not exist. This is what you want to make the play idempotent. You don't want to replace the cache with the parted info after you created new partition. But, if you for whatever reason want to update the cache set cache_update=true and run the playbook

shell> ansible-playbook pb.yml -e debug=true -e get_info=true -e cache_update=true
  • New partition will be created in the second part. Run it in --check mode first and see the debug output
shell> ansible-playbook pb6.yml -e debug=true -e create_part=true -C

gives (abridged)

TASK [debug] *********************************************************************************
ok: [localhost] => 
  msg: |-
    part_size: 10
    last_number: 1
    last_end: 34.1
    unit: MB
    next_number: 2
    next_part_start: 34.1MB
    next_part_end: 44.1MB

This is correct. We want to create partition number 2 of the size 10MB. Let's create it

shell> ansible-playbook pb.yml -e debug=true -e create_part=true

PLAY [localhost] ********************************************************************************

TASK [debug] ************************************************************************************
skipping: [localhost]

TASK [get partition info] ***********************************************************************
skipping: [localhost]

TASK [debug] ************************************************************************************
skipping: [localhost]

TASK [debug] ************************************************************************************
skipping: [localhost]

TASK [copy] *************************************************************************************
skipping: [localhost]

TASK [set_fact] *********************************************************************************
ok: [localhost]

TASK [debug] ************************************************************************************
ok: [localhost] => 
  part_info:
    changed: false
    disk:
      dev: /dev/sdb
      logical_block: 512
      model: Generic Flash Disk
      physical_block: 512
      size: 4089.0
      table: msdos
      unit: mb
    failed: false
    partitions:
    - begin: 0.0
      end: 34.1
      flags:
      - esp
      fstype: fat32
      name: ''
      num: 1
      size: 34.1
      unit: mb
    script: unit 'MB' print

TASK [debug] ************************************************************************************
ok: [localhost] => 
  msg: |-
    part_size: 10
    last_number: 1
    last_end: 34.1
    unit: MB
    next_number: 2
    next_part_start: 34.1MB
    next_part_end: 44.1MB

TASK [create new partition] *********************************************************************
changed: [localhost]

TASK [debug] ************************************************************************************
ok: [localhost] => 
  result:
    changed: true
    disk:
      dev: /dev/sdb
      logical_block: 512
      model: Generic Flash Disk
      physical_block: 512
      size: 4089.0
      table: msdos
      unit: mb
    failed: false
    partitions:
    - begin: 0.0
      end: 34.1
      flags:
      - esp
      fstype: fat32
      name: ''
      num: 1
      size: 34.1
      unit: mb
    - begin: 34.1
      end: 44.1
      flags: []
      fstype: ''
      name: ''
      num: 2
      size: 10.0
      unit: mb
    script: unit MB mkpart primary 34.1MB 44.1MB

PLAY RECAP **************************************************************************************
localhost: ok=5    changed=1    unreachable=0    failed=0    skipped=5    rescued=0    ignored=0

You can see the new partition was created. The playbook is idempotent. You can run it repeatedly to make sure the partition is present. There will be no changes reported when all is right.

shell> parted -l
Model: Generic Flash Disk (scsi)
Disk /dev/sdb: 4089MB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags: 

Number  Start   End     Size    Type     File system  Flags
 1      512B    34.1MB  34.1MB  primary  fat32        esp
 2      34.1MB  44.1MB  10.0MB  primary
  • It might be a good idea to split the remote hosts into smaller groups when you run the second part on a large number of remote hosts for the first time. When the partition is created you can run it all in one for the purpose of audit.

Example of a complete playbook

---
# All rights reserved (c) 2022, Vladimir Botka <[email protected]>
# Simplified BSD License, https://opensource.org/licenses/BSD-2-Clause

- hosts: localhost
  gather_facts: false
  become: true

  vars:

    disk: sda          # change this
    part_size: 3       # change this
    unit: GiB          # change this
    cache_update: false
    cache_path: "/tmp/{{ inventory_hostname }}-{{ disk }}-part_info.json"
    # next variables are calculated from part_info
    last_number: "{{ part_info.partitions|map(attribute='num')|max }}"
    last_end: "{{ part_info.partitions|map(attribute='end')|max }}"
    next_number: "{{ last_number|int + 1 }}"
    next_part_start: "{{ last_end }}{{ unit }}"
    next_part_end: "{{ last_end|float + part_size|float }}{{ unit }}"

  tasks:

    - name: Get info and write part_info to cache_path
      block:
        - debug:
            msg: |-
              disk: /dev/{{ disk }}
              unit: {{ unit }}
              cache_update: {{ cache_update }}
              cache_path: {{ cache_path }}
          when: debug|d(false)|bool
        - name: get partition info
          parted:
             device: "/dev/{{ disk }}"
             unit: "{{ unit }}"
          register: part_info
        - debug:
            var: part_info
          when: debug|d(false)|bool
        - debug:
            msg: |-
              part_size: {{ part_size }}
              last_number: {{ last_number }}
              last_end: {{ last_end }}
              next_number: {{ next_number }}
              next_part_start: {{ next_part_start }}
              next_part_end: {{ next_part_end }}
          when: debug|d(false)|bool
        - copy:
            dest: "{{ cache_path }}"
            force: "{{ cache_update|bool }}"
            content: |
              {{ part_info|to_json }}
          delegate_to: localhost
      when: get_info|d(false)|bool

    - name: Read part_info from cache_path and create partition
      block:
        - set_fact:
            part_info: "{{ lookup('file', cache_path)|from_yaml }}"
        - debug:
            var: part_info
          when: debug|d(false)|bool
        - debug:
            msg: |-
              part_size: {{ part_size }}
              last_number: {{ last_number }}
              last_end: {{ last_end }}
              unit: {{ unit }}
              next_number: {{ next_number }}
              next_part_start: {{ next_part_start }}
              next_part_end: {{ next_part_end }}
          when: debug|d(false)|bool
        - name: create new partition
          parted:
            device: "/dev/{{ disk }}"
            number: "{{ next_number }}"
            part_start: "{{ next_part_start }}"
            part_end: "{{ next_part_end }}"
            align: optimal
            unit: "{{ unit }}"
            state: present
          register: result
        - debug:
            var: result
          when: debug|d(false)|bool
      when: create_part|d(false)|bool