#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2014, Steve # # 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 . DOCUMENTATION = ''' --- module: crypttab short_description: Encrypted Linux block devices description: - Control Linux encrypted block devices that are set up during system boot in C(/etc/crypttab). version_added: "1.9" options: name: description: - Name of the encrypted block device as it appears in the C(/etc/crypttab) file, or optionaly prefixed with C(/dev/mapper/), as it appears in the filesystem. I(/dev/mapper/) will be stripped from I(name). required: true default: null aliases: [] state: description: - Use I(present) to add a line to C(/etc/crypttab) or update it's definition if already present. Use I(absent) to remove a line with matching I(name). Use I(opts_present) to add options to those already present; options with different values will be updated. Use I(opts_absent) to remove options from the existing set. required: true choices: [ "present", "absent", "opts_present", "opts_absent"] default: null backing_device: description: - Path to the underlying block device or file, or the UUID of a block-device prefixed with I(UUID=) required: false default: null password: description: - Encryption password, the path to a file containing the pasword, or 'none' or '-' if the password should be entered at boot. required: false default: "none" opts: description: - A comma-delimited list of options. See C(crypttab(5) ) for details. required: false path: description: - Path to file to use instead of C(/etc/crypttab). This might be useful in a chroot environment. required: false default: /etc/crypttab notes: [] requirements: [] author: "Steve (@groks)" ''' EXAMPLES = ''' - name: Set the options explicitly a deivce which must already exist crypttab: name=luks-home state=present opts=discard,cipher=aes-cbc-essiv:sha256 - name: Add the 'discard' option to any existing options for all devices crypttab: name={{ item.device }} state=opts_present opts=discard with_items: ansible_mounts when: '/dev/mapper/luks-' in {{ item.device }} ''' from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), state = dict(required=True, choices=['present', 'absent', 'opts_present', 'opts_absent']), backing_device = dict(default=None), password = dict(default=None), opts = dict(default=None), path = dict(default='/etc/crypttab') ), supports_check_mode = True ) backing_device = module.params['backing_device'] password = module.params['password'] opts = module.params['opts'] state = module.params['state'] path = module.params['path'] name = module.params['name'] if name.startswith('/dev/mapper/'): name = name[len('/dev/mapper/'):] if state != 'absent' and backing_device is None and password is None and opts is None: module.fail_json(msg="expected one or more of 'backing_device', 'password' or 'opts'", **module.params) if 'opts' in state and (backing_device is not None or password is not None): module.fail_json(msg="cannot update 'backing_device' or 'password' when state=%s" % state, **module.params) for arg_name, arg in (('name', name), ('backing_device', backing_device), ('password', password), ('opts', opts)): if (arg is not None and (' ' in arg or '\t' in arg or arg == '')): module.fail_json(msg="invalid '%s': contains white space or is empty" % arg_name, **module.params) try: crypttab = Crypttab(path) existing_line = crypttab.match(name) except Exception: e = get_exception module.fail_json(msg="failed to open and parse crypttab file: %s" % e, **module.params) if 'present' in state and existing_line is None and backing_device is None: module.fail_json(msg="'backing_device' required to add a new entry", **module.params) changed, reason = False, '?' if state == 'absent': if existing_line is not None: changed, reason = existing_line.remove() elif state == 'present': if existing_line is not None: changed, reason = existing_line.set(backing_device, password, opts) else: changed, reason = crypttab.add(Line(None, name, backing_device, password, opts)) elif state == 'opts_present': if existing_line is not None: changed, reason = existing_line.opts.add(opts) else: changed, reason = crypttab.add(Line(None, name, backing_device, password, opts)) elif state == 'opts_absent': if existing_line is not None: changed, reason = existing_line.opts.remove(opts) if changed and not module.check_mode: try: f = open(path, 'wb') f.write(str(crypttab)) finally: f.close() module.exit_json(changed=changed, msg=reason, **module.params) class Crypttab(object): _lines = [] def __init__(self, path): self.path = path if not os.path.exists(path): if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) open(path,'a').close() try: f = open(path, 'r') for line in f.readlines(): self._lines.append(Line(line)) finally: f.close() def add(self, line): self._lines.append(line) return True, 'added line' def lines(self): for line in self._lines: if line.valid(): yield line def match(self, name): for line in self.lines(): if line.name == name: return line return None def __str__(self): lines = [] for line in self._lines: lines.append(str(line)) crypttab = '\n'.join(lines) if len(crypttab) == 0: crypttab += '\n' if crypttab[-1] != '\n': crypttab += '\n' return crypttab class Line(object): def __init__(self, line=None, name=None, backing_device=None, password=None, opts=None): self.line = line self.name = name self.backing_device = backing_device self.password = password self.opts = Options(opts) if line is not None: if self._line_valid(line): self.name, backing_device, password, opts = self._split_line(line) self.set(backing_device, password, opts) def set(self, backing_device, password, opts): changed = False if backing_device is not None and self.backing_device != backing_device: self.backing_device = backing_device changed = True if password is not None and self.password != password: self.password = password changed = True if opts is not None: opts = Options(opts) if opts != self.opts: self.opts = opts changed = True return changed, 'updated line' def _line_valid(self, line): if not line.strip() or line.startswith('#') or len(line.split()) not in (2, 3, 4): return False return True def _split_line(self, line): fields = line.split() try: field2 = fields[2] except IndexError: field2 = None try: field3 = fields[3] except IndexError: field3 = None return (fields[0], fields[1], field2, field3) def remove(self): self.line, self.name, self.backing_device = '', None, None return True, 'removed line' def valid(self): if self.name is not None and self.backing_device is not None: return True return False def __str__(self): if self.valid(): fields = [self.name, self.backing_device] if self.password is not None or self.opts: if self.password is not None: fields.append(self.password) else: self.password('none') if self.opts: fields.append(str(self.opts)) return ' '.join(fields) return self.line class Options(dict): """opts_string looks like: 'discard,foo=bar,baz=greeble' """ def __init__(self, opts_string): super(Options, self).__init__() self.itemlist = [] if opts_string is not None: for opt in opts_string.split(','): kv = opt.split('=') if len(kv) > 1: k, v = (kv[0], kv[1]) else: k, v = (kv[0], None) self[k] = v def add(self, opts_string): changed = False for k, v in Options(opts_string).items(): if self.has_key(k): if self[k] != v: changed = True else: changed = True self[k] = v return changed, 'updated options' def remove(self, opts_string): changed = False for k in Options(opts_string): if self.has_key(k): del self[k] changed = True return changed, 'removed options' def keys(self): return self.itemlist def values(self): return [self[key] for key in self] def items(self): return [(key, self[key]) for key in self] def __iter__(self): return iter(self.itemlist) def __setitem__(self, key, value): if not self.has_key(key): self.itemlist.append(key) super(Options, self).__setitem__(key, value) def __delitem__(self, key): self.itemlist.remove(key) super(Options, self).__delitem__(key) def __ne__(self, obj): return not (isinstance(obj, Options) and sorted(self.items()) == sorted(obj.items())) def __str__(self): ret = [] for k, v in self.items(): if v is None: ret.append(k) else: ret.append('%s=%s' % (k, v)) return ','.join(ret) main()