rpm_key 7.29 KB
Newer Older
1
#!/usr/bin/python
Hector Acosta committed
2 3
# -*- coding: utf-8 -*-

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Ansible module to import third party repo keys to your rpm db
# (c) 2013, Héctor Acosta <hector.acosta@gazzang.com>
#
# 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/>.
Hector Acosta committed
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

DOCUMENTATION = '''
---
module: rpm_key
author: Hector Acosta <hector.acosta@gazzang.com>
short_description: Adds or removes a gpg key from the rpm db
description:
    - Adds or removes (rpm --import) a gpg key to your rpm database.
version_added: "1.3"
options:
    key:
      required: true
      default: null
      aliases: []
      description:
          - Key that will be modified. Can be a url, a file, or a keyid if the key already exists in the database.
    state:
      required: false
      default: "present"
      choices: [present, absent]
      description:
          - Wheather the key will be imported or removed from the rpm db.
43 44 45 46 47 48 49 50
    validate_certs:
      description:
          - If C(no) and the C(key) is a url starting with https, SSL certificates will not be validated. This should only be used
            on personally controlled sites using self-signed certificates.
      required: false
      default: 'yes'
      choices: ['yes', 'no']

Hector Acosta committed
51 52 53 54
'''

EXAMPLES = '''
# Example action to import a key from a url
Akihiro YAMAZAKI committed
55
- rpm_key: state=present key=http://apt.sw.be/RPM-GPG-KEY.dag.txt
Hector Acosta committed
56 57

# Example action to import a key from a file
Akihiro YAMAZAKI committed
58
- rpm_key: state=present key=/path/to/key.gpg
Hector Acosta committed
59 60

# Example action to ensure a key is not present in the db
Akihiro YAMAZAKI committed
61
- rpm_key: state=absent key=DEADB33F
Hector Acosta committed
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
'''
import syslog
import os.path
import re
import tempfile

# Attempt to download at most 8192 bytes.
# Should be more than enough for all keys
MAXBYTES = 8192

def is_pubkey(string):
    """Verifies if string is a pubkey"""
    pgp_regex = ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*"
    return re.match(pgp_regex, string, re.DOTALL)

class RpmKey:

    def __init__(self, module):
        self.syslogging = False
        # If the key is a url, we need to check if it's present to be idempotent,
        # to do that, we need to check the keyid, which we can get from the armor.
        keyfile = None
        should_cleanup_keyfile = False
        self.module = module
        self.rpm = self.module.get_bin_path('rpm', True)
        state = module.params['state']
        key = module.params['key']

        if '://' in key:
            keyfile = self.fetch_key(key)
            keyid = self.getkeyid(keyfile)
            should_cleanup_keyfile = True
94
        elif self.is_keyid(key):
Hector Acosta committed
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
            keyid = key
        elif os.path.isfile(key):
            keyfile = key
            keyid = self.getkeyid(keyfile)
        else:
            self.module.fail_json(msg="Not a valid key %s" % key)
        keyid = self.normalize_keyid(keyid)

        if state == 'present':
            if self.is_key_imported(keyid):
                module.exit_json(changed=False)
            else:
                if not keyfile:
                    self.module.fail_json(msg="When importing a key, a valid file must be given")
                self.import_key(keyfile, dryrun=module.check_mode)
                if should_cleanup_keyfile:
                    self.module.cleanup(keyfile)
                module.exit_json(changed=True)
        else:
            if self.is_key_imported(keyid):
                self.drop_key(keyid, dryrun=module.check_mode)
                module.exit_json(changed=True)
            else:
                module.exit_json(changed=False)


    def fetch_key(self, url, maxbytes=MAXBYTES):
        """Downloads a key from url, returns a valid path to a gpg key"""
        try:
124
            rsp, info = fetch_url(self.module, url)
125
            key = rsp.read(maxbytes)
Hector Acosta committed
126 127 128 129 130 131 132 133 134 135 136 137 138
            if not is_pubkey(key):
                self.module.fail_json(msg="Not a public key: %s" % url)
            tmpfd, tmpname = tempfile.mkstemp()
            tmpfile = os.fdopen(tmpfd, "w+b")
            tmpfile.write(key)
            tmpfile.close()
            return tmpname
        except urllib2.URLError, e:
            self.module.fail_json(msg=str(e))

    def normalize_keyid(self, keyid):
        """Ensure a keyid doesn't have a leading 0x, has leading or trailing whitespace, and make sure is lowercase"""
        ret = keyid.strip().lower()
139 140 141
        if ret.startswith('0x'):
            return ret[2:]
        elif ret.startswith('0X'):
Hector Acosta committed
142 143 144 145 146 147 148 149 150
            return ret[2:]
        else:
            return ret

    def getkeyid(self, keyfile):
        gpg = self.module.get_bin_path('gpg', True)
        stdout, stderr = self.execute_command([gpg, '--no-tty', '--batch', '--with-colons', '--fixed-list-mode', '--list-packets', keyfile])
        for line in stdout.splitlines():
            line = line.strip()
151
            if line.startswith(':signature packet:'):
Hector Acosta committed
152
                # We want just the last 8 characters of the keyid
153
                keyid = line.split()[-1].strip()[8:]
Hector Acosta committed
154 155 156
                return keyid
        self.json_fail(msg="Unexpected gpg output")

157
    def is_keyid(self, keystr):
Hector Acosta committed
158 159 160 161 162 163 164
        """Verifies if a key, as provided by the user is a keyid"""
        return re.match('(0x)?(0-9a-f){8}', keystr, flags=re.IGNORECASE)

    def execute_command(self, cmd):
        if self.syslogging:
            syslog.openlog('ansible-%s' % os.path.basename(__file__))
            syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd))
Michael DeHaan committed
165
        rc, stdout, stderr = self.module.run_command(cmd)
Hector Acosta committed
166 167 168 169 170
        if rc != 0:
            self.module.fail_json(msg=stderr)
        return stdout, stderr

    def is_key_imported(self, keyid):
171
        stdout, stderr = self.execute_command([self.rpm, '-qa', 'gpg-pubkey'])
Hector Acosta committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
        for line in stdout.splitlines():
            line = line.strip()
            if not line:
                continue
            match = re.match('gpg-pubkey-([0-9a-f]+)-([0-9a-f]+)', line)
            if not match:
                self.module.fail_json(msg="rpm returned unexpected output [%s]" % line)
            else:
                if keyid == match.group(1):
                    return True
        return False

    def import_key(self, keyfile, dryrun=False):
        if not dryrun:
            self.execute_command([self.rpm, '--import', keyfile])

    def drop_key(self, key, dryrun=False):
        if not dryrun:
            self.execute_command([self.rpm, '--erase', '--allmatches', "gpg-pubkey-%s" % key])


def main():
    module = AnsibleModule(
            argument_spec = dict(
                state=dict(default='present', choices=['present', 'absent'], type='str'),
197 198
                key=dict(required=True, type='str'),
                validate_certs=dict(default='yes', type='bool'),
Hector Acosta committed
199 200 201 202 203 204 205 206
                ),
            supports_check_mode=True
            )

    RpmKey(module)



207 208
# import module snippets
from ansible.module_utils.basic import *
209
from ansible.module_utils.urls import *
Hector Acosta committed
210
main()