rpm_key 7.17 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
'''
import syslog
import os.path
import re
import tempfile

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
90
        elif self.is_keyid(key):
Hector Acosta committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
            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)


117
    def fetch_key(self, url):
Hector Acosta committed
118 119
        """Downloads a key from url, returns a valid path to a gpg key"""
        try:
120
            rsp, info = fetch_url(self.module, url)
121
            key = rsp.read()
Hector Acosta committed
122 123 124 125 126 127 128 129 130 131 132 133 134
            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()
135 136 137
        if ret.startswith('0x'):
            return ret[2:]
        elif ret.startswith('0X'):
Hector Acosta committed
138 139 140 141 142 143 144 145 146
            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()
147
            if line.startswith(':signature packet:'):
Hector Acosta committed
148
                # We want just the last 8 characters of the keyid
149
                keyid = line.split()[-1].strip()[8:]
Hector Acosta committed
150 151 152
                return keyid
        self.json_fail(msg="Unexpected gpg output")

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

    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
161
        rc, stdout, stderr = self.module.run_command(cmd)
Hector Acosta committed
162 163 164 165 166
        if rc != 0:
            self.module.fail_json(msg=stderr)
        return stdout, stderr

    def is_key_imported(self, keyid):
167
        stdout, stderr = self.execute_command([self.rpm, '-qa', 'gpg-pubkey'])
Hector Acosta committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
        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'),
193 194
                key=dict(required=True, type='str'),
                validate_certs=dict(default='yes', type='bool'),
Hector Acosta committed
195 196 197 198 199 200 201 202
                ),
            supports_check_mode=True
            )

    RpmKey(module)



203 204
# import module snippets
from ansible.module_utils.basic import *
205
from ansible.module_utils.urls import *
Hector Acosta committed
206
main()