Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apt_key type/provider #212

Merged
merged 2 commits into from
Feb 19, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about 0X? is that entirely invalid?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0X is entirely invalid, 0x is the hex denomination, 0X is just madness.

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+/)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\A seems obscure, why not ^ ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because ruby: http://stackoverflow.com/questions/577653/difference-between-a-z-and-in-ruby-regular-expressions

Someone pointed me at that, apparently there's a "feature" or two in Ruby's regex engine and it isn't a 100% PCRE compatible either 😒.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bah.

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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debian people might not be happy with such a default ;)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very true, however it's the current default on apt::key so I opted to keep that.

# 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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This space left blank intentionally.

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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This space left blank intentionally.

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
Loading