diff options
Diffstat (limited to 'roles')
| -rw-r--r-- | roles/kube_nfs_volumes/README.md | 111 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/defaults/main.yml | 10 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/handlers/main.yml | 3 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/library/partitionpool.py | 240 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/meta/main.yml | 16 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/tasks/main.yml | 25 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/tasks/nfs.yml | 16 | ||||
| -rw-r--r-- | roles/kube_nfs_volumes/templates/nfs.json.j2 | 23 | 
8 files changed, 444 insertions, 0 deletions
diff --git a/roles/kube_nfs_volumes/README.md b/roles/kube_nfs_volumes/README.md new file mode 100644 index 000000000..965958bd6 --- /dev/null +++ b/roles/kube_nfs_volumes/README.md @@ -0,0 +1,111 @@ +# kube_nfs_volumes + +This role is useful to export disks as set of Kubernetes persistent volumes. +It does so by partitioning the disks, creating ext4 filesystem on each +partition, mounting the partitions, exporting the mounts via NFS and adding +these NFS shares as NFS persistent volumes to existing Kubernetes installation. + +All partitions on given disks are used as the persistent volumes, including +already existing partitions! There should be no other data (such as operating +system) on the disks! + +## Requirements + +* Running Kubernetes with NFS persistent volume support (on a remote machine). + +* Works only on RHEL/Fedora-like distros. + +## Role Variables + +``` +# Options of NFS exports. +nfs_export_options: "*(rw,no_root_squash,insecure,no_subtree_check)" + +# Directory, where the created partitions should be mounted. They will be +# mounted as <mount_dir>/sda1 etc. +mount_dir: /exports + +# Comma-separated list of disks to partition. +# This role always assumes that all partitions on these disks are used as +# physical volumes. +disks: /dev/sdb,/dev/sdc + +# Whether to re-partition already partitioned disks. +# Even though the disks won't get repartitioned on 'false', all existing +# partitions on the disk are exported via NFS as physical volumes! +foce: false + +# Specification of size of partitions to create. See library/partitionpool.py +# for details. +sizes: 100M + +# URL of Kubernetes API server, incl. port. +kubernetes_url: https://10.245.1.2:6443 + +# Token to use for authentication to the API server +kubernetes_token: tJdce6Fn3cL1112YoIJ5m2exzAbzcPZX +``` + +## Dependencies + +None + +## Example Playbook + +With this playbook, `/dev/sdb` is partitioned into 100MiB partitions, all of +them are mounted into `/exports/sdb<N>` directory and all these directories +are exported via NFS and added as physical volumes to Kubernetes running at +`https://10.245.1.2:6443`. + +    - hosts: servers +      roles: +        - role: kube_nfs_volumes +          disks: "/dev/sdb" +          sizes: 100M +          kubernetes_url: https://10.245.1.2:6443 +          kubernetes_token: tJdce6Fn3cL1112YoIJ5m2exzAbzcPZX + +See library/partitionpool.py for details how `sizes` parameter can be used +to create partitions of various sizes. + +## Full example +Let's say there are two machines, 10.0.0.1 and 10.0.0.2, that we want to use as +NFS servers for our Kubernetes cluster, running Kubernetes public API at +https://10.245.1.2:6443. + +Both servers have three 1 TB disks, /dev/sda for the system and /dev/sdb and +/dev/sdc to be partitioned. We want to split the data disks into 5, 10 and +20 GiB partitions so that 10% of the total capacity is in 5 GiB partitions, 40% +in 10 GiB and 50% in 20 GiB partitions. + +That means, each data disk will have 20x 5 GiB, 40x 10 GiB and 25x 20 GiB +partitions. + +* Create an `inventory` file: +    ``` +    [nfsservers] +    10.0.0.1 +    10.0.0.2 +    ``` + +* Create an ansible playbook, say `setupnfs.yaml`: +    ``` +    - hosts: nfsservers +      sudo: yes +      roles: +         - role: kube_nfs_volumes +           disks: "/dev/sdb,/dev/sdc" +           sizes: 5G:10,10G:40,20G:50 +           force: no +           kubernetes_url: https://10.245.1.2:6443 +           kubernetes_token: tJdce6Fn3cL1112YoIJ5m2exzAbzcPZX +    ``` + +* Run the playbook: +    ``` +    ansible-playbook -i inventory setupnfs.yml +    ``` + +## License + +Apache 2.0 diff --git a/roles/kube_nfs_volumes/defaults/main.yml b/roles/kube_nfs_volumes/defaults/main.yml new file mode 100644 index 000000000..e296492f9 --- /dev/null +++ b/roles/kube_nfs_volumes/defaults/main.yml @@ -0,0 +1,10 @@ +--- +# Options of NFS exports. +nfs_export_options: "*(rw,no_root_squash,insecure,no_subtree_check)" + +# Directory, where the created partitions should be mounted. They will be +# mounted as <mount_dir>/sda1 etc. +mount_dir: /exports + +# Force re-partitioning the disks +force: false diff --git a/roles/kube_nfs_volumes/handlers/main.yml b/roles/kube_nfs_volumes/handlers/main.yml new file mode 100644 index 000000000..52f3ceffe --- /dev/null +++ b/roles/kube_nfs_volumes/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart nfs +  service: name=nfs-server state=restarted diff --git a/roles/kube_nfs_volumes/library/partitionpool.py b/roles/kube_nfs_volumes/library/partitionpool.py new file mode 100644 index 000000000..1ac8eed4d --- /dev/null +++ b/roles/kube_nfs_volumes/library/partitionpool.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +""" +Ansible module for partitioning. +""" + +# There is no pyparted on our Jenkins worker +# pylint: disable=import-error +import parted + +DOCUMENTATION = """ +--- +module: partitionpool +short_description; Partition a disk into parititions. +description: +  - Creates partitions on given disk based on partition sizes and their weights. +    Unless 'force' option is set to True, it ignores already partitioned disks. + +    When the disk is empty or 'force' is set to True, it always creates a new +    GPT partition table on the disk. Then it creates number of partitions, based +    on their weights. + +    This module should be used when a system admin wants to split existing disk(s) +    into pools of partitions of specific sizes. It is not intended as generic disk +    partitioning module! + +    Independent on 'force' parameter value and actual disk state, the task +    always fills 'partition_pool' fact with all partitions on given disks, +    together with their sizes (in bytes). E.g.: +    partition_sizes = [ +        { name: sda1, Size: 1048576000 }, +        { name: sda2, Size: 1048576000 }, +        { name: sdb1, Size: 1048576000 }, +        ... +    ] + +options: +  disk: +    description: +      - Disk to partition. +  size: +    description: +      - Sizes of partitions to create and their weights. Has form of: +        <size1>[:<weigth1>][,<size2>[:<weight2>][,...]] +      - Any <size> can end with 'm' or 'M' for megabyte, 'g/G' for gigabyte +        and 't/T' for terabyte. Megabyte is used when no unit is specified. +      - If <weight> is missing, 1.0 is used. +      - From each specified partition <sizeX>, number of these partitions are +        created so they occupy spaces represented by <weightX>, proportionally to +        other weights. + +      - Example 1: size=100G says, that the whole disk is split in number of 100 GiB +        partitions. On 1 TiB disk, 10 partitions will be created. + +      - Example 2: size=100G:1,10G:1 says that ratio of space occupied by 100 GiB +        partitions and 10 GiB partitions is 1:1. Therefore, on 1 TiB disk, 500 GiB  +        will be split into five 100 GiB partition and 500 GiB will be split into fifty +        10GiB partitions. +      - size=100G:1,10G:1 = 5x 100 GiB and 50x 10 GiB partitions (on 1 TiB disk). + +      - Example 3: size=200G:1,100G:2 says that the ratio of space occupied by 200 GiB +        partitions and 100GiB partition is 1:2. Therefore, on 1 TiB disk, 1/3 +        (300 GiB) should be occupied by 200 GiB partitions. Only one fits there, +        so only one is created (we always round nr. of partitions *down*). Teh rest +        (800 GiB) is split into eight 100 GiB partitions, even though it's more +        than 2/3 of total space - free space is always allocated as much as possible. +      - size=200G:1,100G:2 = 1x 200 GiB and 8x 100 GiB partitions (on 1 TiB disk). + +      - Example: size=200G:1,100G:1,50G:1 says that the ratio of space occupied by +        200 GiB, 100 GiB and 50 GiB partitions is 1:1:1. Therefore 1/3 of 1 TiB disk +        is dedicated to 200 GiB partitions. Only one fits there and only one is +        created. The rest (800 GiB) is distributed according to remaining weights: +        100 GiB vs 50 GiB is 1:1, we create four 100 GiB partitions (400 GiB in total) +        and eight 50 GiB partitions (again, 400 GiB). +      - size=200G:1,100G:1,50G:1 = 1x 200 GiB, 4x 100 GiB and 8x 50 GiB partitions +        (on 1 TiB disk). +         +  force: +    description: +      - If True, it will always overwite partition table on the disk and create new one. +      - If False (default), it won't change existing partition tables. + +""" + +# It's not class, it's more a simple struct with almost no functionality. +# pylint: disable=too-few-public-methods +class PartitionSpec(object): +    """ Simple class to represent required partitions.""" +    def __init__(self, size, weight): +        """ Initialize the partition specifications.""" +        # Size of the partitions +        self.size = size +        # Relative weight of this request +        self.weight = weight +        # Number of partitions to create, will be calculated later +        self.count = -1 + +    def set_count(self, count): +        """ Set count of parititions of this specification. """ +        self.count = count + +def assign_space(total_size, specs): +    """ +    Satisfy all the PartitionSpecs according to their weight. +    In other words, calculate spec.count of all the specs. +    """ +    total_weight = 0.0 +    for spec in specs: +        total_weight += float(spec.weight) + +    for spec in specs: +        num_blocks = int((float(spec.weight) / total_weight) * (total_size / float(spec.size))) +        spec.set_count(num_blocks) +        total_size -= num_blocks * spec.size +        total_weight -= spec.weight + +def partition(diskname, specs, force=False, check_mode=False): +    """ +    Create requested partitions. +    Returns nr. of created partitions or 0 when the disk was already partitioned. +    """ +    count = 0 + +    dev = parted.getDevice(diskname) +    try: +        disk = parted.newDisk(dev) +    except parted.DiskException: +        # unrecognizable format, treat as empty disk +        disk = None + +    if disk and len(disk.partitions) > 0 and not force: +        print "skipping", diskname +        return 0 + +    # create new partition table, wiping all existing data +    disk = parted.freshDisk(dev, 'gpt') +    # calculate nr. of partitions of each size +    assign_space(dev.getSize(), specs) +    last_megabyte = 1 +    for spec in specs: +        for _ in range(spec.count): +            # create the partition +            start = parted.sizeToSectors(last_megabyte, "MiB", dev.sectorSize) +            length = parted.sizeToSectors(spec.size, "MiB", dev.sectorSize) +            geo = parted.Geometry(device=dev, start=start, length=length) +            filesystem = parted.FileSystem(type='ext4', geometry=geo) +            part = parted.Partition( +                disk=disk, +                type=parted.PARTITION_NORMAL, +                fs=filesystem, +                geometry=geo) +            disk.addPartition(partition=part, constraint=dev.optimalAlignedConstraint) +            last_megabyte += spec.size +            count += 1 +    try: +        if not check_mode: +            disk.commit() +    except parted.IOException: +        # partitions have been written, but we have been unable to inform the +        # kernel of the change, probably because they are in use. +        # Ignore it and hope for the best... +        pass +    return count + +def parse_spec(text): +    """ Parse string with partition specification. """ +    tokens = text.split(",") +    specs = [] +    for token in tokens: +        if not ":" in token: +            token += ":1" + +        (sizespec, weight) = token.split(':') +        weight = float(weight) # throws exception with reasonable error string + +        units = {"m": 1, "g": 1 << 10, "t": 1 << 20, "p": 1 << 30} +        unit = units.get(sizespec[-1].lower(), None) +        if not unit: +            # there is no unit specifier, it must be just the number +            size = float(sizespec) +            unit = 1 +        else: +            size = float(sizespec[:-1]) +        spec = PartitionSpec(int(size * unit), weight) +        specs.append(spec) +    return specs + +def get_partitions(diskpath): +    """ Return array of partition names for given disk """ +    dev = parted.getDevice(diskpath) +    disk = parted.newDisk(dev) +    partitions = [] +    for part in disk.partitions: +        (_, _, pname) = part.path.rsplit("/") +        partitions.append({"name": pname, "size": part.getLength() * dev.sectorSize}) + +    return partitions + + +def main(): +    """ Ansible module main method. """ +    module = AnsibleModule( +        argument_spec=dict( +            disks=dict(required=True, type='str'), +            force=dict(required=False, default="no", type='bool'), +            sizes=dict(required=True, type='str') +        ), +        supports_check_mode=True, +    ) + +    disks = module.params['disks'] +    force = module.params['force'] +    if force is None: +        force = False +    sizes = module.params['sizes'] + +    try: +        specs = parse_spec(sizes) +    except ValueError, ex: +        err = "Error parsing sizes=" + sizes + ": " + str(ex) +        module.fail_json(msg=err) + +    partitions = [] +    changed_count = 0 +    for disk in disks.split(","): +        try: +            changed_count += partition(disk, specs, force, module.check_mode) +        except Exception, ex: +            err = "Error creating partitions on " + disk + ": " + str(ex) +            raise +            #module.fail_json(msg=err) +        partitions += get_partitions(disk) + +    module.exit_json(changed=(changed_count > 0), ansible_facts={"partition_pool": partitions}) + +# ignore pylint errors related to the module_utils import +# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import +# import module snippets +from ansible.module_utils.basic import * +main() + diff --git a/roles/kube_nfs_volumes/meta/main.yml b/roles/kube_nfs_volumes/meta/main.yml new file mode 100644 index 000000000..eb71a7a1f --- /dev/null +++ b/roles/kube_nfs_volumes/meta/main.yml @@ -0,0 +1,16 @@ +--- +galaxy_info: +  author: Jan Safranek +  description: Partition disks and use them as Kubernetes NFS physical volumes. +  company: Red Hat, Inc. +  license: license (Apache) +  min_ansible_version: 1.4 +  platforms: +  - name: EL +    versions: +    - 7 +  - name: Fedora +    versions: +    - all +  categories: +    - cloud diff --git a/roles/kube_nfs_volumes/tasks/main.yml b/roles/kube_nfs_volumes/tasks/main.yml new file mode 100644 index 000000000..23b228d32 --- /dev/null +++ b/roles/kube_nfs_volumes/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: Install pyparted (RedHat/Fedora) +  yum: name=pyparted,python-httplib2 state=installed + +- name: partition the drives +  partitionpool: disks={{ disks }} force={{ force }} sizes={{ sizes }} + +- name: create filesystem +  filesystem: fstype=ext4 dev=/dev/{{ item.name }} +  with_items: partition_pool + +- name: mount +  mount: name={{mount_dir}}/{{ item.name }} src=/dev/{{ item.name }} state=mounted fstype=ext4 passno=2 +  with_items: partition_pool + +- include: nfs.yml + +- name: export physical volumes +  uri: url={{ kubernetes_url }}/api/v1beta3/persistentvolumes +       method=POST +       body='{{ lookup("template", "../templates/nfs.json.j2") }}' +       body_format=json +       status_code=201 +       HEADER_Authorization="Bearer {{ kubernetes_token }}" +  with_items: partition_pool diff --git a/roles/kube_nfs_volumes/tasks/nfs.yml b/roles/kube_nfs_volumes/tasks/nfs.yml new file mode 100644 index 000000000..87cf5f9a4 --- /dev/null +++ b/roles/kube_nfs_volumes/tasks/nfs.yml @@ -0,0 +1,16 @@ +--- +- name: Install NFS server on Fedora/Red Hat +  yum: name=nfs-utils state=installed + +- name: Start rpcbind on Fedora/Red Hat +  service: name=rpcbind state=started enabled=yes + +- name: Start nfs on Fedora/Red Hat +  service: name=nfs-server state=started enabled=yes + +- name: Export the directories +  lineinfile: dest=/etc/exports +              regexp="^{{ mount_dir }}/{{ item.name }} " +              line="{{ mount_dir }}/{{ item.name }} {{nfs_export_options}}" +  with_items: partition_pool +  notify: restart nfs diff --git a/roles/kube_nfs_volumes/templates/nfs.json.j2 b/roles/kube_nfs_volumes/templates/nfs.json.j2 new file mode 100644 index 000000000..b42886ef1 --- /dev/null +++ b/roles/kube_nfs_volumes/templates/nfs.json.j2 @@ -0,0 +1,23 @@ +{ +  "kind": "PersistentVolume", +  "apiVersion": "v1beta3", +  "metadata": { +    "name": "pv-{{ inventory_hostname | regex_replace("\.", "-")  }}-{{ item.name }}", +    "labels": { +      "type": "nfs" +    } +  }, +  "spec": { +    "capacity": { +      "storage": "{{ item.size }}" +    }, +    "accessModes": [ +      "ReadWriteOnce" +    ], +    "NFS": { +      "Server": "{{ inventory_hostname }}", +      "Path": "{{ mount_dir }}/{{ item.name }}", +      "ReadOnly": false +    } +  } +}  | 
