From 657ab5813902f15516ce79352323ab30cbdfd325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 19 Jul 2017 20:43:54 +0300 Subject: [PATCH] Added the docker_volume module (#24957) * Added the docker_volume module * Code style fixes * Added yours truly to the copyright statement * Added documentation link * Fixed YAML syntax in documentation string * Documentation style fixes based on the code review * Implemented requested code corrections * Added documentation for the "labels" option * Handled APIErrors from docker-py * Fixed the type of the "labels" option (dict -> list) * Fixed typo * Import APIError from docker_common, not from docker-py --- .../modules/cloud/docker/docker_volume.py | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 lib/ansible/modules/cloud/docker/docker_volume.py diff --git a/lib/ansible/modules/cloud/docker/docker_volume.py b/lib/ansible/modules/cloud/docker/docker_volume.py new file mode 100644 index 0000000000..5e0ac64a1a --- /dev/null +++ b/lib/ansible/modules/cloud/docker/docker_volume.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# coding: utf-8 +# +# Copyright 2017 Red Hat | Ansible, Alex Grönholm +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = u''' +module: docker_volume +version_added: "2.4" +short_description: Manage Docker volumes +description: + - Create/remove Docker volumes. + - Performs largely the same function as the "docker volume" CLI subcommand. +options: + name: + description: + - Name of the volume to operate on. + required: true + aliases: + - volume_name + + driver: + description: + - Specify the type of volume. Docker provides the C(local) driver, but 3rd party drivers can also be used. + default: local + + driver_options: + description: + - > + Dictionary of volume settings. Consult docker docs for valid options and values: + U(https://docs.docker.com/engine/reference/commandline/volume_create/#driver-specific-options) + + labels: + description: + - List of labels to set for the volume + + force: + description: + - With state C(present) causes the volume to be deleted and recreated if the volume already + exist and the driver, driver options or labels differ. + This will cause any data in the existing volume to be lost. + type: bool + default: 'no' + + state: + description: + - C(absent) deletes the volume. + - C(present) creates the volume, if it does not already exist. + default: present + choices: + - absent + - present + +extends_documentation_fragment: + - docker + +author: + - Alex Grönholm (@agronholm) + +requirements: + - "python >= 2.6" + - "docker-py >= 1.10.0" + - "The docker server >= 1.9.0" +''' + +EXAMPLES = ''' +- name: Create a volume + docker_volume: + name: volume_one + +- name: Remove a volume + docker_volume: + name: volume_one + state: absent + +- name: Create a volume with options + docker_volume: + name: volume_two + driver_options: + type: btrfs + device: /dev/sda2 +''' + +RETURN = ''' +facts: + description: Volume inspection results for the affected volume. + returned: success + type: dict + sample: {} +''' + +from ansible.module_utils.six import iteritems, text_type +from ansible.module_utils.docker_common import DockerBaseClass, AnsibleDockerClient, APIError + + +class TaskParameters(DockerBaseClass): + def __init__(self, client): + super(TaskParameters, self).__init__() + self.client = client + + self.volume_name = None + self.driver = None + self.driver_options = None + self.labels = None + self.force = None + self.debug = None + + for key, value in iteritems(client.module.params): + setattr(self, key, value) + + +class DockerVolumeManager(object): + + def __init__(self, client): + self.client = client + self.parameters = TaskParameters(client) + self.check_mode = self.client.check_mode + self.results = { + u'changed': False, + u'actions': [] + } + self.diff = self.client.module._diff + + self.existing_volume = self.get_existing_volume() + + state = self.parameters.state + if state == 'present': + self.present() + elif state == 'absent': + self.absent() + + def get_existing_volume(self): + try: + volumes = self.client.volumes() + except APIError as e: + self.client.fail(text_type(e)) + + for volume in volumes[u'Volumes']: + if volume['Name'] == self.parameters.volume_name: + return volume + + return None + + def has_different_config(self): + """ + Return the list of differences between the current parameters and the existing volume. + + :return: list of options that differ + """ + differences = [] + if self.parameters.driver and self.parameters.driver != self.existing_volume['Driver']: + differences.append('driver') + if self.parameters.driver_options: + if not self.existing_volume.get('Options'): + differences.append('driver_options') + else: + for key, value in iteritems(self.parameters.driver_options): + if (not self.existing_volume['Options'].get(key) or + value != self.existing_volume['Options'][key]): + differences.append('driver_options.%s' % key) + if self.parameters.labels: + existing_labels = self.existing_volume.get('Labels', {}) + all_labels = set(self.parameters.labels) | set(existing_labels) + for label in all_labels: + if existing_labels.get(label) != self.parameters.labels.get(label): + differences.append('labels.%s' % label) + + return differences + + def create_volume(self): + if not self.existing_volume: + if not self.check_mode: + try: + resp = self.client.create_volume(self.parameters.volume_name, + driver=self.parameters.driver, + driver_opts=self.parameters.driver_options, + labels=self.parameters.labels) + self.existing_volume = self.client.inspect_volume(resp['Name']) + except APIError as e: + self.client.fail(text_type(e)) + + self.results['actions'].append("Created volume %s with driver %s" % (self.parameters.volume_name, self.parameters.driver)) + self.results['changed'] = True + + def remove_volume(self): + if self.existing_volume: + if not self.check_mode: + try: + self.client.remove_volume(self.parameters.volume_name) + except APIError as e: + self.client.fail(text_type(e)) + + self.results['actions'].append("Removed volume %s" % self.parameters.volume_name) + self.results['changed'] = True + + def present(self): + differences = [] + if self.existing_volume: + differences = self.has_different_config() + + if differences and self.parameters.force: + self.remove_volume() + self.existing_volume = None + + self.create_volume() + + if self.diff or self.check_mode or self.parameters.debug: + self.results['diff'] = differences + + if not self.check_mode and not self.parameters.debug: + self.results.pop('actions') + + self.results['ansible_facts'] = {u'docker_volume': self.get_existing_volume()} + + def absent(self): + self.remove_volume() + + +def main(): + argument_spec = dict( + volume_name=dict(type='str', required=True, aliases=['name']), + state=dict(type='str', default='present', choices=['present', 'absent']), + driver=dict(type='str', default='local'), + driver_options=dict(type='dict', default={}), + labels=dict(type='list'), + force=dict(type='bool', default=False), + debug=dict(type='bool', default=False) + ) + + client = AnsibleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True + ) + + cm = DockerVolumeManager(client) + client.module.exit_json(**cm.results) + +if __name__ == '__main__': + main()