apt_key 5.99 KB
Newer Older
Jayson Vantuyl committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2012, Jayson Vantuyl <jayson@aggressive.ly>
#
# 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 <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: apt_key
25
author: Jayson Vantuyl & others
Jayson Vantuyl committed
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
version_added: 1.0
short_description: Add or remove an apt key
description:
    - Add or remove an I(apt) key, optionally downloading it
notes:
    - doesn't download the key unless it really needs it
    - as a sanity check, downloaded key id must match the one specified
    - best practice is to specify the key id and the url
options:
    id:
        required: false
        default: none
        description:
            - identifier of key
    url:
        required: false
        default: none
        description:
            - url to retrieve key from.
    state:
        required: false
        choices: [ absent, present ]
        default: present
        description:
            - used to specify if key is being added or revoked
examples:
    - code: "apt_key: url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=present"
      description: Add an Apt signing key, uses whichever key is at the URL
    - code: "apt_key: id=473041FA url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=present"
      description: Add an Apt signing key, will not download if present
    - code: "apt_key: url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=absent"
      description: Remove an Apt signing key, uses whichever key is at the URL
    - code: "apt_key: id=473041FA state=absent"
      description: Remove a Apt specific signing key
'''

62
# FIXME: standardize into module_common
Jayson Vantuyl committed
63 64 65
from urllib2 import urlopen, URLError
from traceback import format_exc
from re import compile as re_compile
66
# FIXME: standardize into module_common
Jayson Vantuyl committed
67 68 69
from distutils.spawn import find_executable
from os import environ
from sys import exc_info
70
import traceback
Jayson Vantuyl committed
71 72 73 74 75 76

match_key = re_compile("^gpg:.*key ([0-9a-fA-F]+):.*$")

REQUIRED_EXECUTABLES=['gpg', 'grep', 'apt-key']


77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
def check_missing_binaries(module):
    missing = [e for e in REQUIRED_EXECUTABLES if not find_executable(e)]
    if len(missing):
        module.fail_json(msg="binaries are missing", names=all)

def all_keys(module):
    (rc, out, err) = module.run_command("apt-key list")
    results = []
    lines = out.split('\n')
    for line in lines:
       if line.startswith("pub"):
          tokens = line.split()
          code = tokens[1]
          (len_type, real_code) = code.split("/")
          results.append(real_code)
    return results

def key_present(module, key_id):
    (rc, out, err) = module.run_command("apt-key list | 2>&1 grep -q %s" % key_id)
    return rc == 0

def download_key(module, url):
    # FIXME: move get_url code to common, allow for in-memory D/L, support proxies
    # and reuse here
Jayson Vantuyl committed
101
    if url is None:
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
        module.fail_json(msg="needed a URL but was not specified")
    try:
        connection = urlopen(url)
        if connection is None:
            module.fail_json("error connecting to download key from url")
        data = connection.read()
        return data
    except:
        module.fail_json(msg="error getting key id from url", traceback=format_exc())


def add_key(module, key):
    cmd = "apt-key add -"
    (rc, out, err) = module.run_command(cmd, data=key, check_rc=True)
    return True
Jayson Vantuyl committed
117 118

def remove_key(key_id):
119 120 121 122
    # FIXME: use module.run_command, fail at point of error and don't discard useful stdin/stdout
    cmd = 'apt-key del %s' 
    (rc, out, err) = module.run_command(cmd, check_rc=True)
    return True
Jayson Vantuyl committed
123 124 125 126 127 128

def main():
    module = AnsibleModule(
        argument_spec=dict(
            id=dict(required=False, default=None),
            url=dict(required=False),
129 130
            data=dict(required=False),
            key=dict(required=False),
Jayson Vantuyl committed
131
            state=dict(required=False, choices=['present', 'absent'], default='present')
132
        ),
Jayson Vantuyl committed
133 134
    )

135 136 137 138 139 140 141 142
    key_id          = module.params['id']
    url             = module.params['url']
    data            = module.params['data']
    state           = module.params['state']
    changed         = False
    
    # FIXME: I think we have a common facility for this, if not, want
    check_missing_binaries(module)
Jayson Vantuyl committed
143

144
    keys = all_keys(module)
Jayson Vantuyl committed
145 146

    if state == 'present':
147 148
        if key_id and key_id in keys:
            module.exit_json(changed=False)
Jayson Vantuyl committed
149
        else:
150 151 152 153
            if not data:
                data = download_key(module, url)
            if key_id and key_id in keys:
                module.exit_json(changed=False)
Jayson Vantuyl committed
154
            else:
155 156 157 158 159 160 161 162
                add_key(module, data)
                changed=False
                keys2 = all_keys(module)
                if len(keys) != len(keys2):
                    changed=True
                if key_id and not key_id in keys2:
                    module.fail_json(msg="key does not seem to have been added", id=key_id)
                module.exit_json(changed=changed)
Jayson Vantuyl committed
163
    elif state == 'absent':
164 165 166
        if not key_id:
            module.fail_json(msg="key is required")
        if key_id in keys:
Jayson Vantuyl committed
167 168 169
            if remove_key(key_id):
                changed=True
            else:
170
                # FIXME: module.fail_json  or exit-json immediately at point of failure
Jayson Vantuyl committed
171 172 173 174 175 176 177
                module.fail_json(msg="error removing key_id", **return_values(True))

    module.exit_json(changed=changed, **return_values())

# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()