Skip to content

Commit

Permalink
Merge pull request #212 from daenney/apt-key
Browse files Browse the repository at this point in the history
apt_key type/provider
  • Loading branch information
Ashley Penney committed Feb 19, 2014
2 parents a797bd7 + 9ade658 commit c9443f9
Show file tree
Hide file tree
Showing 5 changed files with 831 additions and 5 deletions.
7 changes: 2 additions & 5 deletions .travis.yml
Expand Up @@ -15,9 +15,10 @@ rvm:
env:
matrix:
- PUPPET_GEM_VERSION="~> 2.7.0"
- PUPPET_GEM_VERSION="~> 3.0.0"
- PUPPET_GEM_VERSION="~> 3.1.0"
- PUPPET_GEM_VERSION="~> 3.2.0"
- PUPPET_GEM_VERSION="~> 3.3.0"
- PUPPET_GEM_VERSION="~> 3.4.0"
global:
- PUBLISHER_LOGIN=puppetlabs
- secure: |-
Expand All @@ -31,11 +32,7 @@ matrix:
env: PUPPET_GEM_VERSION="~> 2.7.0"
- rvm: 2.0.0
env: PUPPET_GEM_VERSION="~> 2.7.0"
- rvm: 2.0.0
env: PUPPET_GEM_VERSION="~> 3.0.0"
- rvm: 2.0.0
env: PUPPET_GEM_VERSION="~> 3.1.0"
- rvm: 1.8.7
env: PUPPET_GEM_VERSION="~> 3.2.0"
notifications:
email: false
170 changes: 170 additions & 0 deletions lib/puppet/provider/apt_key/apt_key.rb
@@ -0,0 +1,170 @@
require 'date'
require 'open-uri'
require 'tempfile'

Puppet::Type.type(:apt_key).provide(:apt_key) do

KEY_LINE = {
:date => '[0-9]{4}-[0-9]{2}-[0-9]{2}',
:key_type => '(R|D)',
:key_size => '\d{4}',
:key_id => '[0-9a-fA-F]+',
:expires => 'expire(d|s)',
}

confine :osfamily => :debian
defaultfor :osfamily => :debian
commands :apt_key => 'apt-key'

def self.instances
key_array = apt_key('list').split("\n").collect do |line|
line_hash = key_line_hash(line)
next unless line_hash
expired = false

if line_hash[:key_expiry]
expired = Date.today > Date.parse(line_hash[:key_expiry])
end

new(
:name => line_hash[:key_id],
:id => line_hash[:key_id],
:ensure => :present,
:expired => expired,
:expiry => line_hash[:key_expiry],
:size => line_hash[:key_size],
:type => line_hash[:key_type] == 'R' ? :rsa : :dsa,
:created => line_hash[:key_created]
)
end
key_array.compact!
end

def self.prefetch(resources)
apt_keys = instances
resources.keys.each do |name|
if provider = apt_keys.find{ |key| key.name == name }
resources[name].provider = provider
end
end
end

def self.key_line_hash(line)
line_array = line.match(key_line_regexp).to_a
return nil if line_array.length < 5

return_hash = {
:key_id => line_array[3],
:key_size => line_array[1],
:key_type => line_array[2],
:key_created => line_array[4],
:key_expiry => nil,
}

return_hash[:key_expiry] = line_array[7] if line_array.length == 8
return return_hash
end

def self.key_line_regexp
# This regexp is trying to match the following output
# pub 4096R/4BD6EC30 2010-07-10 [expires: 2016-07-08]
# pub 1024D/CD2EFD2A 2009-12-15
regexp = /\A
pub # match only the public key, not signatures
\s+ # bunch of spaces after that
(#{KEY_LINE[:key_size]}) # size of the key, usually a multiple of 1024
#{KEY_LINE[:key_type]} # type of the key, usually R or D
\/ # separator between key_type and key_id
(#{KEY_LINE[:key_id]}) # hex id of the key
\s+ # bunch of spaces after that
(#{KEY_LINE[:date]}) # date the key was added to the keyring
# following an optional block which indicates if the key has an expiration
# date and if it has expired yet
(
\s+ # again with thes paces
\[ # we open with a square bracket
#{KEY_LINE[:expires]} # expires or expired
\: # a colon
\s+ # more spaces
(#{KEY_LINE[:date]}) # date indicating key expiry
\] # we close with a square bracket
)? # end of the optional block
\Z/x
regexp
end

def source_to_file(value)
if URI::parse(value).scheme.nil?
fail("The file #{value} does not exist") unless File.exists?(value)
value
else
begin
key = open(value).read
rescue OpenURI::HTTPError => e
fail("#{e.message} for #{resource[:source]}")
rescue SocketError
fail("could not resolve #{resource[:source]}")
else
tempfile(key)
end
end
end

def tempfile(content)
file = Tempfile.new('apt_key')
file.write content
file.close
file.path
end

def exists?
@property_hash[:ensure] == :present
end

def create
command = []
if resource[:source].nil? and resource[:content].nil?
# Breaking up the command like this is needed because it blows up
# if --recv-keys isn't the last argument.
command.push('adv', '--keyserver', resource[:server])
unless resource[:keyserver_options].nil?
command.push('--keyserver-options', resource[:keyserver_options])
end
command.push('--recv-keys', resource[:id])
elsif resource[:content]
command.push('add', tempfile(resource[:content]))
elsif resource[:source]
command.push('add', source_to_file(resource[:source]))
# In case we really screwed up, better safe than sorry.
else
fail("an unexpected condition occurred while trying to add the key: #{resource[:id]}")
end
apt_key(command)
@property_hash[:ensure] = :present
end

def destroy
apt_key('del', resource[:id])
@property_hash.clear
end

def read_only(value)
fail('This is a read-only property.')
end

mk_resource_methods

# Needed until PUP-1470 is fixed and we can drop support for Puppet versions
# before that.
def expired
@property_hash[:expired]
end

# Alias the setters of read-only properties
# to the read_only function.
alias :created= :read_only
alias :expired= :read_only
alias :expiry= :read_only
alias :size= :read_only
alias :type= :read_only
end
112 changes: 112 additions & 0 deletions lib/puppet/type/apt_key.rb
@@ -0,0 +1,112 @@
require 'pathname'

Puppet::Type.newtype(:apt_key) do

@doc = <<-EOS
This type provides Puppet with the capabilities to manage GPG keys needed
by apt to perform package validation. Apt has it's own GPG keyring that can
be manipulated through the `apt-key` command.
apt_key { '4BD6EC30':
source => 'http://apt.puppetlabs.com/pubkey.gpg'
}
**Autorequires**:
If Puppet is given the location of a key file which looks like an absolute
path this type will autorequire that file.
EOS

ensurable

validate do
if self[:content] and self[:source]
fail('The properties content and source are mutually exclusive.')
end
end

newparam(:id, :namevar => true) do
desc 'The ID of the key you want to manage.'
# GPG key ID's should be either 32-bit (short) or 64-bit (long) key ID's
# and may start with the optional 0x
newvalues(/\A(0x)?[0-9a-fA-F]{8}\Z/, /\A(0x)?[0-9a-fA-F]{16}\Z/)
munge do |value|
if value.start_with?('0x')
id = value.partition('0x').last.upcase
else
id = value.upcase
end
if id.length == 16
id[8..-1]
else
id
end
end
end

newparam(:content) do
desc 'The content of, or string representing, a GPG key.'
end

newparam(:source) do
desc 'Location of a GPG key file, /path/to/file, http:// or https://'
newvalues(/\Ahttps?:\/\//, /\A\/\w+/)
end

autorequire(:file) do
if self[:source] and Pathname.new(self[:source]).absolute?
self[:source]
end
end

newparam(:server) do
desc 'The key server to fetch the key from based on the ID.'
defaultto :'keyserver.ubuntu.com'
# Need to validate this, preferably through stdlib is_fqdn
# but still working on getting to that.
end

newparam(:keyserver_options) do
desc 'Additional options to pass to apt-key\'s --keyserver-options.'
end

newproperty(:expired) do
desc <<-EOS
Indicates if the key has expired.
This property is read-only.
EOS
end

newproperty(:expiry) do
desc <<-EOS
The date the key will expire, or nil if it has no expiry date.
This property is read-only.
EOS
end

newproperty(:size) do
desc <<-EOS
The key size, usually a multiple of 1024.
This property is read-only.
EOS
end

newproperty(:type) do
desc <<-EOS
The key type, either RSA or DSA.
This property is read-only.
EOS
end

newproperty(:created) do
desc <<-EOS
Date the key was created.
This property is read-only.
EOS
end
end

0 comments on commit c9443f9

Please sign in to comment.