BryanDavis has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/343956 )
Change subject: wmflib: sync with upstream
......................................................................
wmflib: sync with upstream
Copy in the version of wmflib from operations/puppet.git @ 69aba47
Change-Id: I3ef41e75539befb10f13394b03d9d47cb90ef1dd
---
A puppet/modules/wmflib/.rspec
M puppet/modules/wmflib/README.md
A puppet/modules/wmflib/Rakefile
A puppet/modules/wmflib/lib/hiera/backend/httpyaml_backend.rb
M puppet/modules/wmflib/lib/hiera/backend/mwyaml_backend.rb
M puppet/modules/wmflib/lib/hiera/backend/nuyaml_backend.rb
A puppet/modules/wmflib/lib/hiera/backend/proxy_backend.rb
A puppet/modules/wmflib/lib/hiera/backend/role_backend.rb
A puppet/modules/wmflib/lib/hiera/httpcache.rb
M puppet/modules/wmflib/lib/hiera/mwcache.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/conflicts.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/conftool.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/cron_splay.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/ensure_mounted.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/get_clusters.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/hash_deselect_re.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/hash_select_re.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/htpasswd.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/ini.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/ipresolve.rb
M puppet/modules/wmflib/lib/puppet/parser/functions/os_version.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/puppet_ssldir.rb
M puppet/modules/wmflib/lib/puppet/parser/functions/require_package.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/requires_os.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/role.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/secret.rb
M puppet/modules/wmflib/lib/puppet/parser/functions/ssl_ciphersuite.rb
A puppet/modules/wmflib/lib/puppet/parser/functions/validate_array_re.rb
A puppet/modules/wmflib/spec/fixtures/hiera.proxy.yaml
A puppet/modules/wmflib/spec/fixtures/hiera.yaml
A puppet/modules/wmflib/spec/fixtures/hieradata/common.yaml
A puppet/modules/wmflib/spec/fixtures/hieradata/hosts/foo.yaml
A puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test.yaml
A puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test2.yaml
A puppet/modules/wmflib/spec/functions/conftool_spec.rb
A puppet/modules/wmflib/spec/functions/ensure_mounted_spec.rb
A puppet/modules/wmflib/spec/functions/hash_deselect_re_spec.rb
A puppet/modules/wmflib/spec/functions/hash_select_re_spec.rb
A puppet/modules/wmflib/spec/functions/ipresolve_spec.rb
A puppet/modules/wmflib/spec/functions/role_spec.rb
A puppet/modules/wmflib/spec/hiera/proxy_backend_spec.rb
A puppet/modules/wmflib/spec/hiera/role_backend_spec.rb
A puppet/modules/wmflib/spec/spec_helper.rb
43 files changed, 2,111 insertions(+), 176 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/vagrant
refs/changes/56/343956/1
diff --git a/puppet/modules/wmflib/.rspec b/puppet/modules/wmflib/.rspec
new file mode 100644
index 0000000..f449dae
--- /dev/null
+++ b/puppet/modules/wmflib/.rspec
@@ -0,0 +1,2 @@
+--format doc
+--color
diff --git a/puppet/modules/wmflib/README.md b/puppet/modules/wmflib/README.md
index 882a17a..f6ab3fe 100644
--- a/puppet/modules/wmflib/README.md
+++ b/puppet/modules/wmflib/README.md
@@ -15,6 +15,18 @@
$packages = apply_format('texlive-lang-%s', $languages)
+## conflicts
+
+`conflicts( string|resource $resource )`
+
+Throw an error if a resource is declared.
+
+### Examples
+
+ conflicts('::redis::legacy')
+ conflicts(Class['::redis-server'])
+
+
## ensure_directory
`ensure_directory( string|bool $ensure )`
@@ -66,6 +78,33 @@
}
+## ensure_mounted
+
+`ensure_mounted( string|bool $ensure )`
+
+Takes a generic `ensure` parameter value and convert it to an
+appropriate value for use with a mount declaration.
+
+If `$ensure` is `true` or `present`, the return value is `mounted`.
+Otherwise, the return value is the unmodified `$ensure` parameter.
+
+### Examples
+
+ # Sample class which mounts or unmounts '/var/lib/nginx'
+ # based on the class's generic $ensure parameter:
+ class nginx ( $ensure = present ) {
+ package { 'nginx-full':
+ ensure => $ensure,
+ }
+ mount { '/var/lib/nginx':
+ ensure => ensure_mounted($ensure),
+ device => 'tmpfs',
+ fstype => 'tmpfs',
+ options => 'defaults,noatime,uid=0,gid=0,mode=755,size=1g',
+ }
+ }
+
+
## ensure_service
`ensure_service( string|bool $ensure )`
@@ -89,6 +128,52 @@
require => Package['redis-server'],
}
}
+
+
+## hash_deselect_re
+
+`hash_deselect_re( string $regex, hash $input )`
+
+Does exactly the opposite of hash_select_re below: keys matching
+the regex are *excluded* from the new hash.
+
+
+## hash_select_re
+
+`hash_select_re( string $regex, hash $input )`
+
+This creates a new hash from the input hash, but only copies the
+keys which match the regex. In other words, it does the
+equivalent of this in Ruby pseudo-code:
+
+ return input.select { |k, _v| regex.match(k) }
+
+### Example
+
+ hash_select_re('^a', {"abc" => 1, "def" => 2, "asdf" => 3})
+
+will produce:
+
+ {"abc" => 1, "asdf" => 3}
+
+
+## ini
+
+`ini( hash $ini_settings [, hash $... ] )`
+
+Serialize a hash into the .ini-style format expected by Python's
+ConfigParser. Takes one or more hashes as arguments. If the argument
+list contains more than one hash, they are merged together. In case of
+duplicate keys, hashes to the right win.
+
+### Example
+
+ ini({'server' => {'port' => 80}})
+
+will produce:
+
+ [server]
+ port = 80
## ordered_json
@@ -127,6 +212,30 @@
file { '/etc/kibana/config.yaml':
content => ordered_yaml($options),
}
+
+
+## os_version
+
+`os_version( string $version_predicate )`
+
+Performs semantic OS version comparison.
+
+Takes one or more string arguments, each containing one or more predicate
+expressions. Each expression consts of a distribution name, followed by a
+comparison operator, followed by a release name or number. Multiple clauses
+are OR'd together. The arguments are case-insensitive.
+
+The host's OS version will be compared to to the comparison target
+using the specified operator, returning a boolean. If no operator is
+present, the equality operator is assumed.
+
+### Examples
+
+ # True if Ubuntu Trusty or newer or Debian Jessie or newer
+ os_version('ubuntu >= trusty || debian >= Jessie')
+
+ # True if exactly Debian Jessie
+ os_version('debian jessie')
## php_ini
@@ -177,23 +286,23 @@
requires_realm('labs')
-## requires_ubuntu
+## requires_os
-`requires_ubuntu( string $version_predicate )`
+`requires_os( string $version_predicate )`
-Validate that the host Ubuntu version satisfies a version
+Validate that the host OS version satisfies a version
check. Abort catalog compilation if not.
-See the documentation for ubuntu_version() for supported
+See the documentation for os_version() for supported
predicate syntax.
### Examples
- # Fail unless version is Trusty
- requires_ubuntu('trusty')
+ # Fail unless version is Trusty or Jessie
+ requires_os('ubuntu trusty || debian jessie')
# Fail unless Trusty or newer
- requires_ubuntu('> trusty')
+ requires_os('ubuntu >= trusty')
@@ -225,21 +334,18 @@
## ssl_ciphersuite
-`ssl_ciphersuite( string $servercode, string $encryption_type, int $hsts_days
)`
+`ssl_ciphersuite( string $servercode, string $encryption_type, boolean $hsts )`
Outputs the ssl configuration directives for use with either Nginx
or Apache using our selection of ciphers and SSL options.
Takes three arguments:
-- The servercode, or which browser-version combination to
- support. At the moment only 'apache-2.2', 'apache-2.4' and 'nginx'
- are supported.
+- The server to configure for: 'apache' or 'nginx'
- The compatibility mode,indicating the degree of compatibility we
want to retain with older browsers (basically, IE6, IE7 and
Android prior to 3.0)
-- An optional argument, that if non-nil will set HSTS to max-age of
- N days
+- hsts - optional boolean, true emits our standard public HSTS
Whenever called, this function will output a list of strings that
can be safely used in your configuration file as the ssl
@@ -247,7 +353,7 @@
### Examples
- ssl_ciphersuite('apache-2.4', 'compat')
+ ssl_ciphersuite('apache', 'compat', true)
ssl_ciphersuite('nginx', 'strong')
@@ -274,50 +380,22 @@
to_seconds('2 days') # 172800
-## ubuntu_version
+## validate_array_re
+`validate_array_re( array $items, string $re )`
-`ubuntu_version( string $version_predicate )`
-
-Performs semantic Ubuntu version comparison.
-
-Takes a single string argument containing a comparison operator
-followed by an optional space, followed by a comparison target,
-provided as Ubuntu version number or release name.
-
-The host's Ubuntu version will be compared to to the comparison target
-using the specified operator, returning a boolean. If no operator is
-present, the equality operator is assumed.
-
-Release names are case-insensitive. The comparison operator and
-comparison target can be provided as two separate arguments, if you
-prefer.
+Throw an error if any member of $items does not match the regular
+expression $re.
### Examples
- # True if Precise or newer
- ubuntu_version('>= precise')
- ubuntu_version('>= 12.04.4')
+ # OK -- each array item is a four-digit number.
+ validate_array_re([8123, 8124, 8125], '^\d{4}$')
- # True if older than Utopic
- ubuntu_version('< utopic')
-
- # True if newer than Precise
- ubuntu_version('> precise')
-
- # True if Trusty or older
- ubuntu_version('<= trusty')
-
- # True if exactly Trusty
- ubuntu_version('trusty')
- ubuntu_version('== trusty')
-
- # True if anything but Trusty
- ubuntu_version('!trusty')
- ubuntu_version('!= trusty')
+ # Fail -- last array item is not a four-digit number.
+ validate_array_re([8123, 8124, 812], '^\d{4}$')
## validate_ensure
-
`validate_ensure( string $ensure )`
Throw an error if the $ensure argument is not 'present' or 'absent'.
diff --git a/puppet/modules/wmflib/Rakefile b/puppet/modules/wmflib/Rakefile
new file mode 100644
index 0000000..a0c3b79
--- /dev/null
+++ b/puppet/modules/wmflib/Rakefile
@@ -0,0 +1,37 @@
+require 'rake'
+require 'fileutils'
+
+require 'rspec/core/rake_task'
+
+modulename = File.basename(File.expand_path(File.dirname(__FILE__)))
+
+symlinks = { 'spec/fixtures/modules/%s/files' % modulename =>
'../../../../files',
+ 'spec/fixtures/modules/%s/manifests' % modulename =>
'../../../../manifests',
+ 'spec/fixtures/modules/%s/templates' % modulename =>
'../../../../templates',
+ }
+
+
+task :setup do
+ FileUtils.mkdir_p('spec/fixtures/modules/%s' % modulename)
+ symlinks.each do |x|
+ if !File.exist?(x[0])
+ FileUtils.ln_s(x[1], x[0])
+ end
+ end
+end
+
+task :teardown do
+ symlinks.each { |x| FileUtils.rm(x[0], :force => true) }
+ FileUtils.rmdir('spec/fixtures/modules/%s' % modulename)
+ FileUtils.rmdir('spec/fixtures/modules')
+end
+
+RSpec::Core::RakeTask.new(:realspec) do |t|
+ t.fail_on_error = false
+ t.pattern = 'spec/*/*_spec.rb'
+end
+
+task :spec_standalone => [ :setup, :realspec, :teardown]
+
+task :default => :spec do
+end
diff --git a/puppet/modules/wmflib/lib/hiera/backend/httpyaml_backend.rb
b/puppet/modules/wmflib/lib/hiera/backend/httpyaml_backend.rb
new file mode 100644
index 0000000..fae9f51
--- /dev/null
+++ b/puppet/modules/wmflib/lib/hiera/backend/httpyaml_backend.rb
@@ -0,0 +1,44 @@
+require "hiera/httpcache"
+class Hiera
+ module Backend
+ class Httpyaml_backend
+ def initialize
+ @cache = Httpcache.new
+ end
+
+ def lookup(key, scope, order_override, resolution_type)
+ answer = nil
+ Hiera.debug("Looking up #{key}")
+
+ Backend.datasources(scope, order_override) do |source|
+ # Small hack: We don't want to search any datasource but the
+ # httpyaml/%{::labsproject} hierarchy here; so we plainly exit
+ # in any other case.
+ next unless source.start_with?('httpyaml/') && source.length >
'httpyaml/'.length
+
+ data = @cache.read(source)
+
+ next if data.nil? || data.empty?
+ next unless data.include?(key)
+
+ new_answer = Backend.parse_answer(data[key], scope)
+ case resolution_type
+ when :array
+ raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of?(Array) ||
new_answer.kind_of?(String)
+ answer ||= []
+ answer << new_answer
+ when :hash
+ raise Exception, "Hiera type mismatch: expected Hash and got
#{new_answer.class}" unless new_answer.kind_of? Hash
+ answer ||= {}
+ answer = Backend.merge_answer(new_answer, answer)
+ else
+ answer = new_answer
+ break
+ end
+ end
+
+ answer
+ end
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/hiera/backend/mwyaml_backend.rb
b/puppet/modules/wmflib/lib/hiera/backend/mwyaml_backend.rb
index 6e0c638..5f6253e 100644
--- a/puppet/modules/wmflib/lib/hiera/backend/mwyaml_backend.rb
+++ b/puppet/modules/wmflib/lib/hiera/backend/mwyaml_backend.rb
@@ -11,25 +11,30 @@
Hiera.debug("Looking up #{key}")
Backend.datasources(scope, order_override) do |source|
- # Small hack: - we don't want to search any datasource but the
- # labs/%{::instanceproject} hierarchy here; so we plainly exit
- # in any other case
- if m = /labs\/([^\/]+)$/.match(source)
- source = m[1].capitalize
- else
- next
- end
+ # Small hack: We don't want to search any datasource but the
+ # labs/%{::labsproject} hierarchy here; so we plainly exit
+ # in any other case.
+ next unless source.start_with?('labs/') && source.length >
'labs/'.length
+
+ # For hieradata/, the hierarchy is defined as
+ # "labs/%{::labsproject}/host/%{::hostname}" and
+ # "labs/%{::labsproject}/common". We map the former
+ # verbatim to "Hiera:$labsproject/host/$hostname", while the
+ # latter gets simplified to "Hiera:$labsproject". In both
+ # cases, we capitalize $labsproject.
+ source = source['labs/'.length..-1].chomp('/common').capitalize
+
data = @cache.read(source, Hash, {}) do |content|
YAML.load(content)
end
- next if data.empty?
+ next if data.nil? || data.empty?
next unless data.include?(key)
new_answer = Backend.parse_answer(data[key], scope)
case resolution_type
when :array
- raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of?
String
+ raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of?(Array) ||
new_answer.kind_of?(String)
answer ||= []
answer << new_answer
when :hash
diff --git a/puppet/modules/wmflib/lib/hiera/backend/nuyaml_backend.rb
b/puppet/modules/wmflib/lib/hiera/backend/nuyaml_backend.rb
index 2f4e376..ad01bf9 100644
--- a/puppet/modules/wmflib/lib/hiera/backend/nuyaml_backend.rb
+++ b/puppet/modules/wmflib/lib/hiera/backend/nuyaml_backend.rb
@@ -7,7 +7,7 @@
#
#
# This backend allows some more flexibility over the vanilla yaml
-# backend, as path expansion in the lookup and even dynamic lookups.
+# backend, as path expansion in the lookup.
#
# == Private path
#
@@ -21,7 +21,6 @@
# :expand_path be expanded when looking the file up on disk. This
# allows both to have a more granular set of files, but also to avoid
# unnecessary cache evictions for cached data.
-#
# === Example
#
# Say your hiera.yaml has defined
@@ -41,33 +40,30 @@
#
# Unless very small, all files should be split up like this.
#
-# == Dynamic lookup
+# == Regexp matching
#
-# Sometimes we want to search for data based on variables... that are
-# hosted within hiera! Dynamic lookup allows to define hierachies that
-# will determine the full path based on results from hiera
-# itself. Tricky? Let's see with an example
+# As multiple hosts may correspond to the same rules/configs in a
+# large cluster, we allow to define a self-contained "regex.yaml" file
+# in your datadir, where each different class of servers may be
+# represented by a label and a corresponding regexp.
#
# === Example
+# Say you have a lookup for "cluster", and you have
+#"regex/%{hostname}" in your hierarchy; also, let's say that your
+# scope contains hostname = "web1001.local". So if your regex.yaml
+# file contains:
#
-# Say you have in your hiera config
-# :nuyaml:
-# :dynamic_lookup:
-# - role
-# :hierarchy:
-# - "host/%{fqdn}"
-# - role
+# databases:
+# __regex: !ruby/regex '/db.*\.local/'
+# cluster: db
#
-# What will happen will be that any key we search (say $cluster) will
-# be first searched in the specific file for that host
-# (host/hostname.yaml), if not found, it will be searched as follows:
-# - if host/hostname.yaml contains a value for role, say
-# 'refrigerator', then lookup will continue in the
-# 'role/refrigerator.yaml' file
-# - else it will looked up in the 'role/default.yaml'
+# webservices:
+# __regex: !ruby/regex '/^web.*\.local$/'
+# cluster: www
#
-# Note that for added fun you may declare one part of the hierarchy to
-# be both dynamically looked up and expanded. It works!
+# This will make it so that "cluster" will assume the value "www"
+# given the regex matches the "webservices" stanza
+#
class Hiera
module Backend
class Nuyaml_backend
@@ -76,12 +72,17 @@
require 'yaml'
@cache = cache || Filecache.new
config = Config[:nuyaml]
- @dynlookup = config[:dynlookup] || []
@expand_path = config[:expand_path] || []
end
def get_path(key, scope, source)
config_section = :nuyaml
+ # Special case: regex
+ if m = /^regex\//.match(source)
+ Hiera.debug("Regex match going on - using regex.yaml")
+ return Backend.datafile(config_section, scope, 'regex', "yaml")
+ end
+
# Special case: 'private' repository.
# We use a different datadir in this case.
# Example: private/common will search in the common source
@@ -91,23 +92,52 @@
source = m[1]
end
+ # Special case: 'secret' repository. This is practically labs only
+ # We use a different datadir in this case.
+ # Example: private/common will search in the common source
+ # within the private datadir
+ if m = /secret\/(.*)/.match(source)
+ config_section = :secret
+ source = m[1]
+ end
+
Hiera.debug("The source is: #{source}")
# If the source is in the expand_path list, perform path
# expansion. This is thought to allow large codebases to live
# with fairly small yaml files as opposed to a very large one.
# Example:
- # $apache::mpm::worker => 'worker' in common/apache/mpm.yaml
- if @expand_path.include? source
+ # $apache::mpm::worker will be in common/apache/mpm.yaml
+ paths = @expand_path.map{ |x| Backend.parse_string(x, scope) }
+ if paths.include? source
namespaces = key.gsub(/^::/,'').split('::')
- newkey = namespaces.pop
+ namespaces.pop
unless namespaces.empty?
source += "/".concat(namespaces.join('/'))
- key = newkey
end
end
- return key, Backend.datafile(config_section, scope, source, "yaml")
+ return Backend.datafile(config_section, scope, source, "yaml")
+ end
+
+ def plain_lookup(key, data, scope)
+ return nil unless data.include?(key)
+ return Backend.parse_answer(data[key], scope)
+ end
+
+ def regex_lookup(key, matchon, data, scope)
+ data.each do |label, datahash|
+ r = datahash["__regex"]
+ Hiera.debug("Scanning label #{label} for matches to '#{r}' in
'#{matchon}' ")
+ next unless r.match(matchon)
+ Hiera.debug("Label #{label} matches; searching within it")
+ next unless datahash.include?(key)
+ return Backend.parse_answer(datahash[key], scope)
+ end
+ return nil
+ rescue => detail
+ Hiera.debug(detail)
+ return nil
end
def lookup(key, scope, order_override, resolution_type)
@@ -116,23 +146,11 @@
Hiera.debug("Looking up #{key}")
Backend.datasources(scope, order_override) do |source|
- # Yes this is kind of hacky. We look it up again on hiera,
- # and build a source based on the lookup.
- if @dynlookup.include? source
- Hiera.debug("Dynamic lookup for source #{source}")
- if key == source
- next
- end
- dynsource = lookup(source, scope, order_override, :priority)
- dynsource ||= 'default'
- source += "/#{dynsource}"
- end
-
Hiera.debug("Loading info from #{source} for #{key}")
- lookup_key, yamlfile = get_path(key, scope, source)
+ yamlfile = get_path(key, scope, source)
- Hiera.debug("Searching for #{lookup_key} in #{yamlfile}")
+ Hiera.debug("Searching for #{key} in #{yamlfile}")
next if yamlfile.nil?
@@ -144,14 +162,21 @@
YAML.load(content)
end
- next if data.empty?
- next unless data.include?(lookup_key)
+ next if data.nil?
+ if m = /regex\/(.*)$/.match(source)
+ matchto = m[1]
+ new_answer = regex_lookup(key, matchto, data, scope)
+ else
+ new_answer = plain_lookup(key, data, scope)
+ end
+
+ next if new_answer.nil?
# Extra logging that we found the key. This can be outputted
# multiple times if the resolution type is array or hash but that
# should be expected as the logging will then tell the user ALL the
# places where the key is found.
- Hiera.debug("Found #{lookup_key} in #{source}")
+ Hiera.debug("Found #{key} in #{source}")
# for array resolution we just append to the array whatever
# we find, we then goes onto the next file and keep adding to
@@ -160,10 +185,9 @@
# for priority searches we break after the first found data
# item
- new_answer = Backend.parse_answer(data[lookup_key], scope)
case resolution_type
when :array
- raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of?
String
+ raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of?(Array) ||
new_answer.kind_of?(String)
answer ||= []
answer << new_answer
when :hash
diff --git a/puppet/modules/wmflib/lib/hiera/backend/proxy_backend.rb
b/puppet/modules/wmflib/lib/hiera/backend/proxy_backend.rb
new file mode 100644
index 0000000..b8a77b2
--- /dev/null
+++ b/puppet/modules/wmflib/lib/hiera/backend/proxy_backend.rb
@@ -0,0 +1,91 @@
+# Proxy Hiera backend - for complex hiera queries
+#
+# Author: Giuseppe Lavagetto
+# Copyright (c) 2015 Wikimedia Foundation
+#
+# This backend allows you to plug multiple simpler backends that use
+# Backend.datasources in their lookup method to work toghether as if
+# they were part of the same hierarchy.
+#
+
+# Add Hiera::Config the ability to get overridden values
+# We use this to trick the other backends in doing one and only one
+# lookup each
+class Hiera::Config
+ class << self
+ def []=(key,value)
+ @config[key] = value
+ end
+ end
+end
+
+class Hiera
+ module Backend
+ class Proxy_backend
+ def initialize
+ Hiera.debug "Starting the proxy backend"
+ @config = Config[:proxy]
+ self.load_plugins
+ end
+
+ def load_plugins
+ @plugins ||={}
+ #Load plugins only once
+ @config[:plugins].each do |plugin|
+ Hiera.debug "Loading plugin #{plugin}"
+ begin
+ require "hiera/backend/#{plugin.downcase}_backend"
+ @plugins[plugin] ||=
+ Backend.const_get("#{plugin.capitalize}_backend").new
+ rescue
+ Hiera.warn "Failure: plugin #{plugin} failed to load"
+ end
+ end
+ end
+
+ def lookup(key, scope, order_override, resolution_type)
+ answer = nil
+ hierarchy = Config[:hierarchy].clone
+
+ Backend.datasources(scope, order_override) do |source|
+ if source.include? '@@'
+ plugin, source = source.split('@@')
+ else
+ plugin = @config[:default_plugin]
+ end
+ if not @plugins.include? plugin
+ Hiera.
+ warn "Hierarchy specifies to use plugin '#{plugin}' but can't
find it"
+ next
+ end
+ # We look up onto a foreign backend by limiting us to a
+ # single element of hierarchy.
+ Config[:hierarchy] = [source]
+ new_answer = @plugins[plugin].
+ lookup(key, scope, order_override,
+ resolution_type)
+ Config[:hierarchy] = hierarchy
+
+ next if new_answer.nil?
+
+ case resolution_type
+ when :array
+ raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of? Array
+ # The plugins already return an array of answers, so just
concatenate it.
+ answer ||= []
+ answer += new_answer
+ when :hash
+ raise Exception, "Hiera type mismatch: expected Hash and got
#{new_answer.class}" unless new_answer.kind_of? Hash
+ answer ||= {}
+ answer = Backend.merge_answer(new_answer,answer)
+ else
+ answer = new_answer
+ break
+ end
+ end
+
+ answer
+ end
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/hiera/backend/role_backend.rb
b/puppet/modules/wmflib/lib/hiera/backend/role_backend.rb
new file mode 100644
index 0000000..8de978c
--- /dev/null
+++ b/puppet/modules/wmflib/lib/hiera/backend/role_backend.rb
@@ -0,0 +1,177 @@
+# Role-based Hiera backend
+#
+# Author: Giuseppe Lavagetto <[email protected]>
+# Copyright (c) 2014 Wikimedia Foundation
+#
+#
+# This backend allows to group definitions based on the roles defined
+# at the node level using the 'role' keyword.
+# It allows searching in hierarchies that are based on the role
+# currently applied; this is very handy if you have group of hosts
+# whose configuration is dependent on their role rather than on other
+# facts like hostnames.
+#
+# == How this works
+#
+# Whenever you use the 'role' keyword in a node:
+#
+# role cache::text
+#
+# Two things happen:
+# - the class role::cache::text gets included in the current node
+# - this gets registered in a global registry
+#
+# Whenever a hiera lookup is performed and the role backend is used,
+# the key is searched in all the hierarchies defined in the
+# :role_hierarchy.
+#
+# A typical hierarchy would be:
+# - "%{::environment}"
+# - common
+#
+# The path in which the files must be is as follows:
+# role/${hierarchy}/<path>.yaml
+# where <path> corresponds to the argument passed to the 'role'
+# keyword.
+#
+# === Example
+#
+# In site.pp:
+#
+# node /pinkunicorn.wikimedia.org/ {
+# role foo::bar, fizzbuzz
+# notice(hiera('admin::groups'))
+# }
+#
+# Give the hierarchy from before, the final hiera lookup will result
+# in searching admin::groups in two distinct hierarchies:
+#
+# role/production/foo/bar.yaml
+# role/common/foo/bar.yaml
+#
+# role/production/fizzbuzz.yaml
+# role/common/fizzbuzz.yaml
+#
+# The research will be performed for *both* hierarchies as a normal
+# hiera lookup; if a plain lookup is performed, the results of the two
+# searches will be compared; if they differ, an exception will be
+# thrown, so that conflicting directives will need manual resolution.
+#
+# === Things to pay attention to
+#
+# One big caveat: if you do use multiple times the role keyword, any
+# class included by the first role keyword is declared would be
+# evaluated just after the first "role" has been called, thus
+# not having the full scope of both to search from; this could result
+# in unexpected behaviour so I advice _against_ using multiple role
+# keywords in a single node, if both roles include conflicting
+# classes.
+require 'yaml'
+class Hiera
+ module Backend
+ class Role_backend
+ def initialize(cache=nil)
+ @cache = cache || Filecache.new
+ end
+
+ def get_path(key, role, source, scope)
+ config_section = :role
+
+ # Special case: 'private' repository.
+ # We use a different datadir in this case.
+ # Example: private/common will search in the role/common source
+ # within the private datadir
+ if m = /private\/(.*)/.match(source)
+ config_section = :private
+ source = m[1]
+ end
+
+ # Variables for role::foo::bar will be searched in:
+ # role/foo/bar.yaml
+ # role/$::site/foo/bar.yaml
+ # etc, depending on your hierarchy
+ path = role.split('::').join('/')
+ src = "role/#{source}/#{path}"
+
+ # Use the datadir for the 'role' section of the config
+ return Backend.datafile(config_section, scope, src, "yaml")
+ end
+
+ def merge_answer(new_answer, answer, resolution_type)
+ case resolution_type
+ when :array
+ raise Exception, "Hiera type mismatch: expected Array and got
#{new_answer.class}" unless new_answer.kind_of?(Array) ||
new_answer.kind_of?(String)
+ answer ||= []
+ answer << new_answer
+ when :hash
+ raise Exception, "Hiera type mismatch: expected Hash and got
#{new_answer.class}" unless new_answer.kind_of? Hash
+ answer ||= {}
+ answer = Backend.merge_answer(new_answer,answer)
+ else
+ answer = new_answer
+ return true, answer
+ end
+ return false, answer
+ end
+
+ def lookup(key, scope, order_override, resolution_type)
+ topscope_var = '_roles'
+ resultset = nil
+ return nil unless scope.include?topscope_var
+ roles = scope[topscope_var]
+ return nil if roles.nil?
+ if Config.include?(:role_hierarchy)
+ hierarchy = Config[:role_hierarchy]
+ else
+ hierarchy = nil
+ end
+
+ roles.keys.each do |role|
+ Hiera.debug("Looking in hierarchy for role #{role}")
+ answer = nil
+ Backend.datasources(scope, order_override, hierarchy) do |source|
+ yamlfile = get_path(key,role,source, scope)
+ next if yamlfile.nil?
+ Hiera.debug("Searching in file #{yamlfile} for #{key}")
+ next unless File.exist?(yamlfile)
+
+ data = @cache.read(yamlfile, Hash) do |content|
+ YAML.load(content)
+ end
+
+ next if data.nil? || data.empty?
+
+ next unless data.include? key
+
+ new_answer = Backend.parse_answer(data[key], scope)
+ Hiera.debug("Found: #{key} => #{new_answer}")
+ is_done, answer = merge_answer(new_answer, answer,
+ resolution_type)
+ break if is_done
+ end
+ # skip parsing if no answer was found.
+ next if answer.nil?
+
+ # Now we got one answer for this role, we can merge it with
+ # what we got earlier.
+ case resolution_type
+ when :array
+ resultset ||= []
+ answer.each { |el| resultset.push(el) }
+ when :hash
+ resultset ||= {}
+ resultset = Backend.merge_answer(answer, resultset)
+ else
+ # We raise an exception if we have received conflicting results
+ if resultset && answer != resultset
+ raise Exception, "Conflicting value for #{key} found in role
#{role}"
+ else
+ resultset = answer
+ end
+ end
+ end
+ resultset
+ end
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/hiera/httpcache.rb
b/puppet/modules/wmflib/lib/hiera/httpcache.rb
new file mode 100644
index 0000000..270c449
--- /dev/null
+++ b/puppet/modules/wmflib/lib/hiera/httpcache.rb
@@ -0,0 +1,83 @@
+class Hiera
+ class Httpcache < Filecache
+ def initialize
+ super
+ require 'httpclient'
+ require 'yaml'
+ require 'json'
+ config = Config[:httpyaml]
+ @url_prefix = config[:url_prefix]
+ @http = HTTPClient.new(:agent_name => 'HieraHttpCache/0.1')
+
+ # Use the operating system's certificate store, not ruby-httpclient's
cacert.p7s which doesn't
+ # even have the root used to sign Let's Encrypt CAs (DST Root CA X3)
+ @http.ssl_config.clear_cert_store
+ @http.ssl_config.set_default_paths
+
+ @stat_ttl = config[:cache_ttl] || 60
+ if defined? @http.ssl_config.ssl_version
+ @http.ssl_config.ssl_version = 'TLSv1'
+ else
+ # Note: this seem to work in later versions of the library,
+ # but has no effect. How cute, I <3 ruby.
+ @http.ssl_config.options = OpenSSL::SSL::OP_NO_SSLv3
+ end
+ end
+
+ def read(path, expected_type=Hash, default=nil)
+ read_file(path)
+ rescue => detail
+ # When failing to read data, we raise an exception, see
https://phabricator.wikimedia.org/T78408
+ error = "Reading data from #{path} failed: #{detail.class}: #{detail}"
+ raise error
+ end
+
+ def read_file(path)
+ if stale?(path)
+ data = get_from_http(path)
+ @cache[path][:data] = data
+
+ if !@cache[path][:data].is_a?(Object)
+ raise TypeError, "Data retrieved from #{path} is #{data.class} not
Object"
+ end
+ end
+
+ @cache[path][:data]
+ end
+
+ private
+
+ def stale?(path)
+ # We don't actually have caching information lol
+ # So we just, uh, blindly cache for 60s
+ now = Time.now.to_i
+ if @cache[path].nil?
+ @cache[path] = {:data => nil, :meta => {:ts => now}}
+ return true
+ elsif (now - @cache[path][:meta][:ts]) <= @stat_ttl
+ # if we already fetched the result within the last stat_ttl seconds,
+ # we don't bother killing the mediawiki instance with a flood of
requests
+ return false
+ else
+ # This means there's a ts and it's old enough
+ @cache[path][:meta][:ts] = now
+ return true
+ end
+ end
+
+ def get_from_http(path)
+ url = path.sub('httpyaml/', @url_prefix)
+ Hiera.debug("Fetching #{url}")
+ res = @http.get(url)
+ if res.status_code != 200
+ raise IOError, "Could not correctly fetch revision for #{path}, HTTP
status code #{res.status_code}, content #{res.data}"
+ end
+ # We shamelessly throw exceptions here, and catch them upper in the chain
+ # specifically in Hiera::Mwcache.stale? and Hiera::Mwcache.read
+ # FIXME: use safe_load here somehow?
+ body = YAML.load(res.body)
+
+ body['hiera']
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/hiera/mwcache.rb
b/puppet/modules/wmflib/lib/hiera/mwcache.rb
index 485f167..191eedb 100644
--- a/puppet/modules/wmflib/lib/hiera/mwcache.rb
+++ b/puppet/modules/wmflib/lib/hiera/mwcache.rb
@@ -1,4 +1,7 @@
class Hiera
+ class MediawikiPageNotFoundError < Exception
+ end
+
class Mwcache < Filecache
def initialize
super
@@ -9,14 +12,32 @@
@httphost = config[:host] || 'https://wikitech.wikimedia.org'
@endpoint = config[:endpoint] || '/w/api.php'
@http = HTTPClient.new(:agent_name => 'HieraMwCache/0.1')
+
+ # Use the operating system's certificate store, not ruby-httpclient's
cacert.p7s which doesn't
+ # even have the root used to sign Let's Encrypt CAs (DST Root CA X3)
+ @http.ssl_config.clear_cert_store
+ @http.ssl_config.set_default_paths
+
@stat_ttl = config[:cache_ttl] || 60
if defined? @http.ssl_config.ssl_version
- @http.ssl_config.ssl_version = 'TLSv1_2'
+ @http.ssl_config.ssl_version = 'TLSv1'
else
# Note: this seem to work in later versions of the library,
# but has no effect. How cute, I <3 ruby.
@http.ssl_config.options = OpenSSL::SSL::OP_NO_SSLv3
end
+ end
+
+ def read(path, expected_type, default=nil, &block)
+ read_file(path, expected_type, &block)
+ rescue Hiera::MediawikiPageNotFoundError => detail
+ # Any errors other than this will cause hiera to raise an error and
puppet to fail.
+ Hiera.debug("Page #{detail} is non-existent, setting defaults
#{default}")
+ @cache[path][:data] = default
+ rescue => detail
+ # When failing to read data, we raise an exception, see
https://phabricator.wikimedia.org/T78408
+ error = "Reading data from #{path} failed: #{detail.class}: #{detail}"
+ raise error
end
def read_file(path, expected_type = Object, &block)
@@ -25,8 +46,8 @@
data = resp["*"]
@cache[path][:data] = block_given? ? yield(data) : data
- if !@cache[path][:data].is_a?(expected_type)
- raise TypeError, "Data retrieved from #{path} is #{data.class} not
#{expected_type}"
+ if !@cache[path][:data].nil? &&
!@cache[path][:data].is_a?(expected_type)
+ raise TypeError, "Data retrieved from #{path} is
#{@cache[path][:data].class}, not #{expected_type} or nil"
end
end
@@ -50,8 +71,9 @@
@cache[path][:meta] = meta
return true
end
- rescue => detail
- error = "Retrieving metadata from #{path} failed: #{detail}"
+ rescue Hiera::MediawikiPageNotFoundError => detail
+ # Any errors other than this will cause hiera to raise an error and
puppet to fail.
+ error = "Page #{detail} is non-existent"
Hiera.warn(error)
# Fill this up with very safe defaults - we cache non-existence
# for cache_ttl as well.
@@ -85,12 +107,12 @@
raise IOError, "Could not correctly fetch revision for #{path}, HTTP
status code #{res.status_code}"
end
# We shamelessly throw exceptions here, and catch them upper in the chain
- # specifically in stale? and Filecache.read
+ # specifically in Hiera::Mwcache.stale? and Hiera::Mwcache.read
body = JSON.parse(res.body)
pages = body["query"]["pages"]
# Quoting Yuvi: "MediaWiki API doesn't give a fuck about HTTP status
codes"
if pages.keys.include? "-1"
- raise IndexError, "The mediawiki page was non-existent"
+ raise Hiera::MediawikiPageNotFoundError, "Hiera:#{path}"
end
#yes, it's that convoluted.
key = pages.keys[0]
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/conflicts.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/conflicts.rb
new file mode 100644
index 0000000..e29af6e
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/conflicts.rb
@@ -0,0 +1,18 @@
+# == Function: conflicts( string|resource $resource )
+#
+# Throw an error if a resource is declared.
+#
+# === Examples
+#
+# # Resource name
+# conflicts('::redis::legacy')
+#
+# # Resource
+# conflicts(Class['::redis-server'])
+#
+module Puppet::Parser::Functions
+ newfunction(:conflicts, :arity => 1) do |args|
+ Puppet::Parser::Functions.function(:defined)
+ fail(Puppet::ParseError, "Resource conflicts with #{args.first}.") if
function_defined(args)
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/conftool.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/conftool.rb
new file mode 100644
index 0000000..867ad1e
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/conftool.rb
@@ -0,0 +1,60 @@
+# == Function: conftool( string $tags, string $selector, string
$object_type='node')
+#
+# Fetch values from conftool. This should be used only for things that depend
on the dynamic
+# state from conftool but do not require the tight coordination.
+#
+# It will get (and json parse) values from conftool and return them, based on
the specified
+# selector.
+#
+# === Examples
+#
+# # get the current status of a node in confctl
+# $status = conftool({ name => 'cp1052.eqiad.wmnet', service => 'varnish-fe'})
# returns a list of associated services
+#
+require 'json'
+
+module Puppet::Parser::Functions
+ newfunction(:conftool, :type => :rvalue, :arity => -2) do |args|
+ case args.length
+ when 2
+ tags, type = *args
+ when 1
+ tags = args[0]
+ type = 'node'
+ end
+
+ # Raise an error if tags is empty or not an hash
+ if !tags.is_a?(Hash) || tags.empty?
+ raise Puppet::ParseError, "tags should be in hash format"
+ end
+
+ # get the data and return them parsed as json
+ begin
+ selector = tags.map { |k, v| "#{k}=#{v}" }.join ","
+ result = []
+ data = function_generate(
+ [
+ '/usr/bin/confctl',
+ '--object-type', type,
+ 'select', selector,
+ 'get'
+ ]
+ ).chomp
+
+ # No result returns the empty list
+ if data.empty?
+ return []
+ end
+
+ data.split("\n").each do |line|
+ entry = JSON.load(line)
+ tags = entry.delete 'tags'
+ obj_name = entry.keys.pop
+ result.push({'name' => obj_name, 'tags' => tags, 'value' =>
entry[obj_name]})
+ end
+ result
+ rescue
+ raise Puppet::ParseError, "Unable to read data from conftool"
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/cron_splay.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/cron_splay.rb
new file mode 100644
index 0000000..af76bf6
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/cron_splay.rb
@@ -0,0 +1,132 @@
+#
+# cron_splay.rb
+#
+
+require 'digest/md5'
+
+module Puppet::Parser::Functions
+ newfunction(:cron_splay, :type => :rvalue, :doc => <<-EOS
+Given an array of fqdn which a cron is applicable to, and a period arg which is
+one of 'hourly', 'daily', or 'weekly', this sorts the fqdn set with
+per-datacenter interleaving for DC-numbered hosts, splays them to fixed even
+intervals within the total period, and then outputs a set of crontab time
+fields for the fqdn currently being compiled-for.
+
+The idea here is to ensure each host in the set executes the cron once per time
+period, and also ensure the time between hosts is consistent (no edge cases
+much closer than the average) by splaying them as evenly as possible with
+rounding errors. For the case of hosts with NNNN numbers indicating the
+datacenter in the first digit, we also maximize the period between any two
+hosts in a given datacenter by interleaving sorted per-DC lists of hosts before
+splaying.
+
+The third and final argument is a static seed which modulates the splayed
+values in two different ways to minimize the effects of multiple cron_splay()
+with the same hostlist and period. It is used to select a determinstically
+random "offset" for the splayed time values (so that the first host doesn't
+always start at 00:00), and is also used to permute the order of the hosts
+within each DC uniquely.
+
+*Examples:*
+
+ $times = fqdn_splay($hosts, 'weekly', 'foo-static-seed')
+ cron { 'foo':
+ minute => $times['minute'],
+ hour => $times['hour'],
+ weekday => $times['weekday'],
+ }
+
+ EOS
+ ) do |arguments|
+
+ raise(Puppet::ParseError, "cron_splay(): Wrong number of arguments " +
+ "given (#{arguments.size} for 3)") if arguments.size != 3
+
+ hosts = arguments[0]
+ period = arguments[1]
+ seed = arguments[2]
+
+ unless hosts.is_a?(Array)
+ raise(Puppet::ParseError, 'cron_splay(): Argument 1 must be an array')
+ end
+
+ unless period.is_a?(String)
+ raise(Puppet::ParseError, 'cron_splay(): Argument 2 must be an string')
+ end
+
+ unless seed.is_a?(String)
+ raise(Puppet::ParseError, 'cron_splay(): Argument 3 must be an string')
+ end
+
+ case period
+ when 'hourly'
+ mins = 60
+ when 'daily'
+ mins = 1440
+ when 'weekly'
+ mins = 10080
+ else
+ raise(Puppet::ParseError, 'cron_splay(): invalid period')
+ end
+
+ # Avoid this edge case for now. At sufficiently large host counts and
+ # small period, randomization is probably better anyways.
+ if hosts.length > mins
+ raise(Puppet::ParseError, 'cron_splay(): too many hosts for period')
+ end
+
+ # split hosts into N lists based the first digit of /NNNN/, defaulting to
zero
+ sublists = [ [], [], [], [], [], [], [], [], [], [] ]
+ for h in hosts
+ match = /([1-9])[0-9]{3}/.match(h)
+ if match
+ sublists[match[1].to_i].push(h)
+ else
+ sublists[0].push(h)
+ end
+ end
+
+ # sort each sublist into a determinstic order based on seed
+ for s in sublists
+ s.sort_by! { |x| Digest::MD5.hexdigest(seed + x) }
+ end
+
+ # interleave sublists into "ordered"
+ longest = sublists.max_by(&:length)
+ sublists -= [longest]
+ ordered = longest.zip(*sublists).flatten.compact
+
+ # find the index of this host in ordered
+ this_idx = ordered.index(lookupvar('::fqdn'))
+ if this_idx.nil?
+ raise(Puppet::ParseError, 'cron_splay(): this host not in set')
+ end
+
+ # find the truncated-integer splayed value of this host
+ tval = this_idx * mins / ordered.length
+
+ # use the seed (again) to add a time offset to the splayed values,
+ # the time offset never being larger than the splayed interval
+ tval += Digest::MD5.hexdigest(seed).to_i(16) % (mins / ordered.length)
+
+ # generate the output
+ output = {}
+ output['minute'] = tval % 60
+
+ if period == 'hourly'
+ output['hour'] = '*'
+ else
+ output['hour'] = (tval / 60) % 24
+ end
+
+ if period == 'weekly'
+ output['weekday'] = tval / 1440
+ else
+ output['weekday'] = '*'
+ end
+
+ return output
+ end
+end
+
+# vim: set ts=2 sw=2 et :
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/ensure_mounted.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/ensure_mounted.rb
new file mode 100644
index 0000000..7a5e321
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/ensure_mounted.rb
@@ -0,0 +1,35 @@
+# == Function: ensure_mounted( string|bool $ensure )
+#
+# Takes a generic 'ensure' parameter value and convert it to an
+# appropriate value for use with a mount declaration.
+#
+# If $ensure is 'true' or 'present', the return value is 'mounted'.
+# Otherwise, the return value is the unmodified $ensure parameter.
+#
+# === Examples
+#
+# # Sample class which mounts or unmounts '/var/lib/nginx'
+# # based on the class's generic $ensure parameter:
+# class nginx ( $ensure = present ) {
+# package { 'nginx-full':
+# ensure => $ensure,
+# }
+# mount { '/var/lib/nginx':
+# ensure => ensure_mounted($ensure),
+# device => 'tmpfs',
+# fstype => 'tmpfs',
+# options => 'defaults,noatime,uid=0,gid=0,mode=755,size=1g',
+# }
+# }
+#
+module Puppet::Parser::Functions
+ newfunction(:ensure_mounted, :type => :rvalue, :arity => 1) do |args|
+
+ ensure_param = args.first
+ case ensure_param
+ when 'present', 'true', true then 'mounted'
+ when 'absent', 'false', false then ensure_param
+ else fail(ArgumentError, "ensure_directory(): invalid argument:
'#{ensure_param}'.")
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/get_clusters.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/get_clusters.rb
new file mode 100644
index 0000000..979a7b6
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/get_clusters.rb
@@ -0,0 +1,56 @@
+# == Function: get_clusters
+#
+# Given a selector hash, which can contain a cluster selector and/or
+# a site selector, this function will return a data
+# structure that contains all nodes clustered by cluster/site.
+#
+# === Parameters
+# [*selector*] An hash used to select the cluster and/or sites, if present;
+# allowed keys are 'site' and 'cluster', and should both be lists
+# of clusters and sites to select.
+#
+# === Examples
+#
+# # Return all nodes known to puppetDB
+# $nodes = get_clusters()
+#
+# # All eqiad nodes grouped by cluster/site
+# $eqiad_nodes = get_clusters({'site' => ['eqiad']})
+#
+# # All MediaWiki appserver and api nodes
+# $mw_servers = get_clusters({'cluster' => ['appserver', 'appserver_api'})
+#
+module Puppet::Parser::Functions
+ newfunction(:get_clusters, :type => :rvalue) do |args|
+ all = {}
+ # Ganglia config is the source of truth about clusters/site
+ cluster_config = function_hiera(['ganglia_clusters', {}])
+
+ # Arguments are an hash of selectors
+ selector ||= {}
+ selector = args[0] unless args.empty?
+ if selector.include? 'cluster'
+ clusters = selector['cluster']
+ else
+ clusters = cluster_config.keys
+ end
+
+ if selector.include? 'site'
+ sites = selector['site']
+ else
+ sites = false
+ end
+
+ function_query_resources([false, '@@Ganglia::Cluster', false]).each do
|node|
+ cluster = node['parameters']['cluster']
+ site = node['parameters']['site']
+ fqdn = node['title']
+ next unless clusters.include?cluster
+ next if sites && !sites.include?(site)
+ all[cluster] ||= {}
+ all[cluster][site] ||= []
+ all[cluster][site] << fqdn
+ end
+ all
+ end
+end
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/hash_deselect_re.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/hash_deselect_re.rb
new file mode 100644
index 0000000..c779625
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/hash_deselect_re.rb
@@ -0,0 +1,35 @@
+#
+# hash_deselect_re.rb
+#
+
+module Puppet::Parser::Functions
+ newfunction(:hash_deselect_re, :type => :rvalue, :doc => <<-EOS
+This function creates a new hash from the input hash, filtering out keys which
+match the provided regex.
+
+*Examples:*
+
+ $in = { 'abc' => 1, 'def' => 2, 'asdf' => 3 }
+ $out = hash_deselect_re('^a', $in);
+ # $out == { 'def' => 2 }
+ $out2 = hash_deselect_re('^(?!a)', $in);
+ # $out2 == { 'abc' => 1, 'asdf' => 3 }
+
+ EOS
+ ) do |arguments|
+
+ raise(Puppet::ParseError, "hash_deselect_re(): Wrong number of arguments "
+
+ "given (#{arguments.size} for 2)") if arguments.size != 2
+
+ pattern = Regexp.new(arguments[0])
+ in_hash = arguments[1]
+ unless in_hash.is_a?(Hash)
+ raise(Puppet::ParseError, 'hash_deselect_re(): Argument 2 must be a
hash')
+ end
+
+ #
https://bibwild.wordpress.com/2012/04/12/ruby-hash-select-1-8-7-and-1-9-3-simultaneously-compatible/
+ Hash[ in_hash.select { |k, _v| !pattern.match(k) } ]
+ end
+end
+
+# vim: set ts=2 sw=2 et :
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/hash_select_re.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/hash_select_re.rb
new file mode 100644
index 0000000..b5aa204
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/hash_select_re.rb
@@ -0,0 +1,35 @@
+#
+# hash_select_re.rb
+#
+
+module Puppet::Parser::Functions
+ newfunction(:hash_select_re, :type => :rvalue, :doc => <<-EOS
+This function creates a new hash from the input hash, filtering out keys which
+do not match the provided regex.
+
+*Examples:*
+
+ $in = { 'abc' => 1, 'def' => 2, 'asdf' => 3 }
+ $out = hash_select_re('^a', $in);
+ # $out == { 'abc' => 1, 'asdf' => 3 }
+ $out2 = hash_select_re('^(?!a)', $in);
+ # $out2 == { 'def' => 2 }
+
+ EOS
+ ) do |arguments|
+
+ raise(Puppet::ParseError, "hash_select_re(): Wrong number of arguments " +
+ "given (#{arguments.size} for 2)") if arguments.size != 2
+
+ pattern = Regexp.new(arguments[0])
+ in_hash = arguments[1]
+ unless in_hash.is_a?(Hash)
+ raise(Puppet::ParseError, 'hash_select_re(): Argument 2 must be a hash')
+ end
+
+ #
https://bibwild.wordpress.com/2012/04/12/ruby-hash-select-1-8-7-and-1-9-3-simultaneously-compatible/
+ Hash[ in_hash.select { |k, _v| pattern.match(k) } ]
+ end
+end
+
+# vim: set ts=2 sw=2 et :
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/htpasswd.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/htpasswd.rb
new file mode 100644
index 0000000..e6d9335
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/htpasswd.rb
@@ -0,0 +1,110 @@
+# == Function htpasswd( string $password, string $salt)
+#
+# Generate a password entry for a htpasswd file using the modified md5 digest
+# method from apr.
+#
+
+require 'digest/md5'
+require 'stringio'
+
+# This class is a conversion to puppet of htauth's methods
+# See https://github.com/copiousfreetime/htauth/blob/master/LICENSE for
copying rights
+# Original code Copyright (c) 2008 Jeremy Hinegardner
+# Modifications Copyright (c) 2017 Giuseppe Lavagetto, Wikimedia Foundation,
Inc.
+class Apr1Md5
+
+ DIGEST_LENGTH = 16
+
+ def initialize(salt)
+ @salt = salt
+ end
+
+ def prefix
+ "$apr1$"
+ end
+
+ # from
https://github.com/copiousfreetime/htauth/blob/master/lib/htauth/algorithm.rb
+ # this is not the Base64 encoding, this is the to64() method from apr
+ SALT_CHARS = (%w( . / ) + ("0".."9").to_a + ('A'..'Z').to_a +
('a'..'z').to_a).freeze
+ def to_64(number, rounds)
+ r = StringIO.new
+ rounds.times do
+ r.print(SALT_CHARS[number % 64])
+ number >>= 6
+ end
+ r.string
+ end
+
+
+ # this algorithm pulled straight from apr_md5_encode() and converted to ruby
syntax
+ def encode(password)
+ primary = ::Digest::MD5.new
+ primary << password
+ primary << prefix
+ primary << @salt
+
+ md5_t = ::Digest::MD5.digest("#{password}#{@salt}#{password}")
+
+ l = password.length
+ while l > 0
+ slice_size = (l > DIGEST_LENGTH) ? DIGEST_LENGTH : l
+ primary << md5_t[0, slice_size]
+ l -= DIGEST_LENGTH
+ end
+
+ # weirdness
+ l = password.length
+ while l != 0
+ case (l & 1)
+ when 1
+ primary << 0.chr
+ when 0
+ primary << password[0, 1]
+ end
+ l >>= 1
+ end
+
+ pd = primary.digest
+
+ encoded_password = "#{prefix}#{@salt}$"
+
+ # apr_md5_encode has this comment about a 60Mhz Pentium above this loop.
+ 1000.times do |x|
+ ctx = ::Digest::MD5.new
+ ctx << (((x & 1) == 1) ? password : pd[0, DIGEST_LENGTH])
+ (ctx << @salt) unless (x % 3) == 0
+ (ctx << password) unless (x % 7) == 0
+ ctx << (((x & 1) == 0) ? password : pd[0, DIGEST_LENGTH])
+ pd = ctx.digest
+ end
+
+
+ pd = pd.bytes.to_a
+
+ l = (pd[0] << 16) | (pd[6] << 8) | pd[12]
+ encoded_password << to_64(l, 4)
+
+ l = (pd[1] << 16) | (pd[7] << 8) | pd[13]
+ encoded_password << to_64(l, 4)
+
+ l = (pd[2] << 16) | (pd[8] << 8) | pd[14]
+ encoded_password << to_64(l, 4)
+
+ l = (pd[3] << 16) | (pd[9] << 8) | pd[15]
+ encoded_password << to_64(l, 4)
+
+ l = (pd[4] << 16) | (pd[10] << 8) | pd[ 5]
+ encoded_password << to_64(l, 4)
+ encoded_password << to_64(pd[11], 2)
+
+ encoded_password
+ end
+end
+
+
+module Puppet::Parser::Functions
+ newfunction(:htpasswd, :type => :rvalue, :arity => 2) do |args|
+ generator = Apr1Md5.new args[1]
+ generator.encode args[0]
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/ini.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/ini.rb
new file mode 100644
index 0000000..19a0d28
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/ini.rb
@@ -0,0 +1,42 @@
+# == Function: ini( hash $ini_settings [, hash $... ] )
+#
+# Serialize a hash into the .ini-style format expected by Python's
+# ConfigParser. Takes one or more hashes as arguments. If the argument
+# list contains more than one hash, they are merged together. In case of
+# duplicate keys, hashes to the right win.
+#
+# === Example
+#
+# ini({'server' => {'port' => 80}})
+#
+# will produce:
+#
+# [server]
+# port = 80
+#
+def ini_flatten(map, prefix = nil)
+ map.reduce({}) do |flat, (k, v)|
+ k = [prefix, k].compact.join('.')
+ flat.merge! v.is_a?(Hash) ? ini_flatten(v, k) : Hash[k, v]
+ end
+end
+
+def ini_cast(v)
+ v.include?('.') ? Float(v) : Integer(v) rescue v
+end
+
+module Puppet::Parser::Functions
+ newfunction(:ini, :type => :rvalue, :arity => -2) do |args|
+ if args.map(&:class).uniq != [Hash]
+ fail(ArgumentError, 'ini(): hash arguments required')
+ end
+ args.reduce(&:merge).map do |section,items|
+ ini_flatten(items).map do |k, vs|
+ case vs
+ when Array then vs.map { |v| "#{k}[#{v}] = #{ini_cast(v)}" }
+ else "#{k} = #{ini_cast(vs)}"
+ end
+ end.flatten.sort.push('').unshift("[#{section}]").join("\n")
+ end.flatten.sort.push('').join("\n")
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/ipresolve.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/ipresolve.rb
new file mode 100644
index 0000000..02afde3
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/ipresolve.rb
@@ -0,0 +1,147 @@
+# == Function: ipresolve( string $name_to_resolve, bool $ipv6 = false)
+#
+# Copyright (c) 2015 Wikimedia Foundation Inc.
+#
+# Performs a name resolution (for A AND AAAA records only) and returns
+# an hash of arrays.
+#
+# Takes one or more names to resolve, and returns an array of all the
+# A or AAAA records found. The resolution is actually only done when
+# the ttl has expired. A particular nameserver can also be specified
+# so only that is used, rather than the system default.
+#
+require 'resolv'
+
+class DNSCacheEntry
+ # Data structure for storing a DNS cached result.
+ def initialize(entry, ttl)
+ @value = entry
+ @ttl = Time.now.to_i + ttl
+ end
+
+ def is_valid?(time)
+ return @ttl > time
+ end
+
+ def value
+ return @value.to_s
+ end
+end
+
+class BasicTTLCache
+ def initialize
+ @cache = {}
+ end
+
+ def write(key, value, ttl)
+ @cache[key] = DNSCacheEntry.new(value, ttl)
+ end
+
+ def delete(key)
+ @cache.delete(key) if @cache.key?(key)
+ end
+
+ def is_valid?(key)
+ # If the key exists, and its ttl has not expired, return true.
+ # Return false (and maybe clean up the stale entry) otherwise.
+ return false unless @cache.key?(key)
+ t = Time.now.to_i
+ return true if @cache[key].is_valid?t
+ return false
+ end
+
+ def read(key)
+ if is_valid?key
+ return @cache[key].value
+ end
+ return nil
+ end
+
+ def read_stale(key)
+ if @cache.key?(key)
+ return @cache[key].value
+ end
+ return nil
+ end
+end
+
+class DNSCached
+ attr_accessor :dns
+ def initialize(cache = nil, default_ttl = 300)
+ @cache = cache || BasicTTLCache.new
+ @default_ttl = default_ttl
+ end
+
+ def get_resource(name, type, nameserver)
+ if nameserver.nil?
+ dns = Resolv::DNS.open()
+ else
+ dns = Resolv::DNS.open(:nameserver => [nameserver])
+ end
+ cache_key = "#{name}_#{type}_#{nameserver}"
+ res = @cache.read(cache_key)
+ if (res.nil?)
+ begin
+ res = dns.getresource(name, type)
+ # Ruby < 1.9 returns nil as the ttl...
+ if res.ttl
+ ttl = res.ttl
+ else
+ ttl = @default_ttl
+ end
+ if type == Resolv::DNS::Resource::IN::PTR
+ retval = res.name
+ else
+ retval = res.address
+ end
+ @cache.write(cache_key, retval, ttl)
+ retval.to_s
+ rescue
+ # If resolution fails and we do have a cached stale value, use it
+ res = @cache.read_stale(cache_key)
+ if res.nil?
+ fail("DNS lookup failed for #{name} #{type}")
+ end
+ res.to_s
+ end
+ else
+ res.to_s
+ end
+ end
+end
+
+
+module Puppet::Parser::Functions
+ dns = DNSCached.new
+ newfunction(:ipresolve, :type => :rvalue, :arity => -1) do |args|
+ name = args[0]
+ if args[1].nil?
+ type = 4
+ elsif args[1].to_s.downcase == 'ptr'
+ type = 'ptr'
+ else
+ type = args[1].to_i
+ end
+ nameserver = args[2] # Ruby returns nil if there's nothing there
+ if type == 4
+ source = Resolv::DNS::Resource::IN::A
+ elsif type == 6
+ source = Resolv::DNS::Resource::IN::AAAA
+ elsif type == 'ptr'
+ source = Resolv::DNS::Resource::IN::PTR
+ # Transform the provided IP address in a PTR record
+ case name
+ when Resolv::IPv4::Regex
+ ptr = Resolv::IPv4.create(name).to_name
+ when Resolv::IPv6::Regex
+ ptr = Resolv::IPv6.create(name).to_name
+ else
+ fail("Cannot interpret #{name} as an address")
+ end
+ name = ptr
+ else
+ raise ArgumentError, 'Type must be 4, 6 or ptr'
+ end
+ return dns.get_resource(name, source, nameserver).to_s
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/os_version.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/os_version.rb
index c105f0a..badeb79 100644
--- a/puppet/modules/wmflib/lib/puppet/parser/functions/os_version.rb
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/os_version.rb
@@ -101,4 +101,4 @@
end
end
end
-end
\ No newline at end of file
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/puppet_ssldir.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/puppet_ssldir.rb
new file mode 100644
index 0000000..d0e8446
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/puppet_ssldir.rb
@@ -0,0 +1,60 @@
+# == Function puppet_ssldir( string $override = nil )
+#
+# Returns puppet's configured ssldir, using some heuristics.
+# This function is needed because we have a separate configurations
+# for the self-hosted puppetmasters ssl directory compared to the
+# standard setup. If we're ever able to simplify or remove such
+# differences, this function might become way simpler, or even
+# disappear.
+#
+# It's possible to override the heuristics and provide an override
+# parameter, which if set to 'master' will assume you are on a
+# self-hosted puppetmaster.
+#
+# == Examples
+#
+# # returns the default result based on the catalog
+# $ssldir = puppet_ssldir()
+# # Forces ssldir to be the one of a self-hosted puppetmaster
+# $ssldir = puppet_ssldir('master')
+#
+module Puppet::Parser::Functions
+ newfunction(:puppet_ssldir, :type => :rvalue) do |overrides|
+ # Check arguments
+ override = overrides[0]
+
+ fail("Only 'master', 'client' and undef " \
+ "are valid arguments of puppet_ssldir") unless
+ ['master', 'client', nil].include?override
+
+ default = '/var/lib/puppet/ssl'
+ self_master = '/var/lib/puppet/server/ssl'
+ self_client = '/var/lib/puppet/client/ssl'
+
+ # Production uses the standard layout
+ return default if lookupvar('::realm') != 'labs'
+
+ # Self-hosted puppetmasters explicit setup
+ case override
+ when 'master'
+ return self_master
+ when 'client'
+ return self_client
+ end
+
+ # Since all self-hosted puppetmasters are in .eqiad.wmflabs, while
+ # the labs masters don't
+ return default if lookupvar('::settings::certname') =~ /\.wikimedia\.org$/
+ # Non-self-hosted puppetmasters all use the default ssldir
+ puppetmaster = lookupvar('puppetmaster')
+ puppetmaster ||= function_hiera(['role::puppet::self::master', ''])
+ if puppetmaster == ''
+ # Means we aren't using any of role::puppet::self!1!
+ default
+ elsif [lookupvar('hostname'), 'localhost', '', nil].include?puppetmaster
+ self_master
+ else
+ self_client
+ end
+ end
+end
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/require_package.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/require_package.rb
index cf32a7c..996462f 100644
--- a/puppet/modules/wmflib/lib/puppet/parser/functions/require_package.rb
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/require_package.rb
@@ -19,14 +19,38 @@
#
module Puppet::Parser::Functions
newfunction(:require_package, :arity => -2) do |args|
- args.each do |package_name|
+ Puppet::Parser::Functions.function :create_resources
+ args.flatten.each do |package_name|
+ # Puppet class names are alphanumeric + underscore
+ # 'g++' package would yield: 'packages::g__'
class_name = 'packages::' + package_name.tr('-+', '_')
- unless compiler.topscope.find_hostclass(class_name)
+
+ # Create host class
+
+ host = compiler.topscope.find_hostclass(class_name)
+ unless host
host = Puppet::Resource::Type.new(:hostclass, class_name)
known_resource_types.add_hostclass(host)
- send Puppet::Parser::Functions.function(:create_resources),
- ['package', { package_name => { :ensure => :present } }]
end
+
+ # Create class scope
+
+ cls = Puppet::Parser::Resource.new(
+ 'class', class_name, :scope => compiler.topscope)
+ catalog.add_resource(cls) rescue nil
+ host.evaluate_code(cls) rescue nil
+
+ # Create package resource
+
+ begin
+ host_scope = compiler.topscope.class_scope(host)
+ host_scope.function_create_resources(
+ ['package', { package_name => { :ensure => :present } }])
+ rescue Puppet::Resource::Catalog::DuplicateResourceError
+ end
+
+ # Declare dependency
+
send Puppet::Parser::Functions.function(:require), [class_name]
end
end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/requires_os.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/requires_os.rb
new file mode 100644
index 0000000..026affe
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/requires_os.rb
@@ -0,0 +1,22 @@
+# == Function: requires_os( string $version_predicate )
+#
+# Validate that the host operating system version satisfies a version
+# check. Abort catalog compilation if not.
+#
+# See the documentation for os_version() for supported predicate syntax.
+#
+# === Examples
+#
+# # Fail unless version is exactly Debian Jessie
+# requires_os('debian jessie')
+#
+# # Fail unless Ubuntu Trusty or newer or Debian Jessie or newer
+# requires_os('ubuntu >= trusty || debian >= Jessie')
+#
+module Puppet::Parser::Functions
+ newfunction(:requires_os, :arity => 1) do |args|
+ Puppet::Parser::Functions.function(:os_version)
+ fail(ArgumentError, 'requires_os(): string argument required') unless
args.first.is_a?(String)
+ fail(Puppet::ParseError, "OS #{args.first} required.") unless
function_os_version(args)
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/role.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/role.rb
new file mode 100644
index 0000000..e03cd8a
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/role.rb
@@ -0,0 +1,85 @@
+# == Function: role ( string $role_name [, string $... ] )
+#
+# Declare the _roles variable (or add keys to it), and if the role class
+# role::$role_name is in the scope, include it. This is roughly a
+# shortcut for
+#
+# $::_roles[foo] = true
+# include role::foo
+#
+# and has the notable advantage over that the syntax is shorter. Also,
+# this function will refuse to run anywhere but at the node scope,
+# thus making any additional role added to a node explicit.
+#
+# If you have more than one role to declare, you MUST do that in one
+# single role stanza, or you would encounter unexpected behaviour. If
+# you do, an exception will be raised.
+#
+# This function is very useful with our "role" hiera backend if you
+# have global configs that are role-based
+#
+# === Example
+#
+# node /^www\d+/ {
+# role mediawiki::appserver # this will load the
role::mediawiki::appserver class
+# include ::standard #this class will use hiera lookups defined for the
role.
+# }
+#
+# node monitoring.local {
+# role icinga, ganglia::collector #GOOD
+# }
+#
+# node monitoring2.local {
+# role icinga
+# role ganglia::collector #BAD, issues a warning
+# }
+
+module Puppet::Parser::Functions
+ newfunction(:role, :arity => -1) do |args|
+ # This will add to the catalog, and to the node specifically:
+ # - A global 'role' hash with the 'role::#{arg}' key set to true;
+ # if the variable is present, append to it
+ # - Include class role::#{arg} if present
+
+ # Prevent use outside of node definitions
+ if not self.is_nodescope?
+ raise Puppet::ParseError,
+ "role can only be used in node scope, while you are in scope
#{self}"
+ end
+
+ # Now check if the variable is already set and issue a warning
+ container = '_roles'
+ rolevar = compiler.topscope.lookupvar(container)
+ if rolevar
+ raise Puppet::ParseError,
+ "Using 'role' multiple times might yield unexpected results, use
'role role1, role2' instead"
+ else
+ compiler.topscope.setvar(container, {})
+ rolevar = compiler.topscope.lookupvar(container)
+ end
+
+ # sanitize args
+ args = args.map do |x|
+ if x.start_with? '::'
+ x.gsub(/^::/, '')
+ else
+ x
+ end
+ end
+
+ # Set the variables
+ args.each do |arg|
+ rolevar[arg] = true
+ end
+
+ args.each do |arg|
+ rolename = 'role::' + arg
+ role_class = compiler.topscope.find_hostclass(rolename)
+ if role_class
+ send Puppet::Parser::Functions.function(:include), [rolename]
+ else
+ raise Puppet::ParseError, "Role class #{rolename} not found"
+ end
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/lib/puppet/parser/functions/secret.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/secret.rb
new file mode 100644
index 0000000..fc66a02
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/secret.rb
@@ -0,0 +1,29 @@
+require 'pathname'
+
+module Puppet::Parser::Functions
+ newfunction(:secret, :type => :rvalue) do |args|
+ mod_name = 'secret'
+ secs_subdir = '/secrets/'
+
+ if args.length != 1 || !args.first.is_a?(String)
+ fail(ArgumentError, 'secret(): exactly one string arg')
+ end
+ in_path = args.first
+
+ if mod = Puppet::Module.find(mod_name)
+ mod_path = mod.path()
+ else
+ fail("secret(): Module #{mod_name} not found")
+ end
+
+ sec_path = mod_path + secs_subdir + in_path
+ final_path = Pathname.new(sec_path).cleanpath()
+
+ # Bail early if it's not a regular, readable file
+ if !final_path.file? || !final_path.readable?
+ fail(ArgumentError, "secret(): invalid secret #{in_path}")
+ end
+
+ return final_path.read()
+ end
+end
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/ssl_ciphersuite.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/ssl_ciphersuite.rb
index 26d0bf7..6143766 100644
--- a/puppet/modules/wmflib/lib/puppet/parser/functions/ssl_ciphersuite.rb
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/ssl_ciphersuite.rb
@@ -1,4 +1,4 @@
-# == Function: ssl_ciphersuite( string $servercode, string $encryption_type,
int $hsts_days )
+# == Function: ssl_ciphersuite( string $server, string $encryption_type,
boolean $hsts )
#
# Outputs the ssl configuration directives for use with either Nginx
# or Apache using our selection of ciphers and SSL options.
@@ -7,14 +7,30 @@
#
# Takes three arguments:
#
-# - The servercode, or which browser-version combination to
-# support. At the moment only 'apache-2.2', 'apache-2.4' and 'nginx'
-# are supported.
-# - The compatibility mode,indicating the degree of compatibility we
-# want to retain with older browsers (basically, IE6, IE7 and
-# Android prior to 3.0)
-# - An optional argument, that if non-nil will set HSTS to max-age of
-# N days
+# - The server to configure for: 'apache' or 'nginx'
+# - The compatibility mode, trades security vs compatibility.
+# Note that due to POODLE, SSLv3 is universally disabled and none of these
+# options are compatible with SSLv3-only clients such as IE6/XP.
+# Current options are:
+# - strong: Only TLSv1.2 with FS+AEAD ciphers. In practice this is a
+# very short list, and requires a very modern client. No
+# tradeoff is made for compatibility. Known to work with:
+# FF/Chrome, IE11, Safari 9, Java8, Android 4.4+, OpenSSL 1.0.x
+# - mid: Supports TLSv1.0 and higher, and adds several forward-secret
+# options which are not AEAD. This is compatible with many
more
+# clients than "strong". Should only be incompatible with
+# unpatched IE8/XP, ancient/un-updated Java6, and some small
+# corner cases like Nokia feature phones.
+# - compat: Supports most legacy clients, FS optional but preferred.
+# - HSTS boolean - if true, will emit our standard HSTS header for canonical
+# public domains (which is currently 1 year with preload and includeSub).
+# Default false.
+#
+# In our WMF configurations, Apache only supports DHE ciphersuites securely on
+# Debian Jessie, which is necessary for "mid" to have the compatibility level
+# stated above. When this function is used with Apache an older host (e.g.
+# Ubuntu Trusty or Precise), the "mid" and "strong" options will be downgraded
+# to "compat" with a warning.
#
# Whenever called, this function will output a list of strings that
# can be safely used in your configuration file as the ssl
@@ -22,7 +38,7 @@
#
# == Examples
#
-# ssl_ciphersuite('apache-2.4', 'compat')
+# ssl_ciphersuite('apache', 'compat', true)
# ssl_ciphersuite('nginx', 'strong')
#
# == License
@@ -46,92 +62,160 @@
require 'puppet/util/package'
module Puppet::Parser::Functions
- ciphersuites = {
- 'compat' =>
'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!DH',
- 'strong' =>
'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!DH'
+ # Basic list chunks, used to construct bigger lists
+ # General preference ordering for fullest combined list:
+ # 0) Enc: 3DES < ALL (SWEET32)
+ # 1) Kx: (EC)DHE > RSA (Forward Secrecy)
+ # 2) Mac: AEAD > ALL (AES-GCM/CHAPOLY > Others)
+ # 3) Enc: CHAPOLY > AESGCM (Old client perf, sec)
+ # 4) Kx: ECDHE > DHE (Perf, mostly)
+ # 5) Enc: AES256 > AES128 (sec)
+ # 6) Auth: ECDSA > RSA (Perf, mostly)
+ #
+ # After all of that, the fullest list of reasonably-acceptable mid/compat
+ # ciphers has been filtered further to reduce pointless clutter:
+ # *) The 'mid' list has been filtered of AES256 options on the grounds that
+ # any such client can always use AES128 instead, and it's senseless to try to
+ # set a 'more bits' security policy if not using a strong cipher in general,
+ # and clients too old for strong ciphers are more likely to be impacted by
+ # AES256 performance differentials. SHA-2 HMAC variants were filtered
+ # similarly, as all clients that would negotiate x-SHA256 also negotiate
x-SHA
+ # and there's no effective security difference between the two.
+ # *) The 'compat' list has been reduced to just the two weakest and
+ # most-popular reasonable options there. The others were mostly
statistically
+ # insignificant, and things are so bad at this level it's not worth worrying
+ # about slight cipher strength gains.
+ basic = {
+ # Forward-Secret + AEAD
+ 'strong' => [
+ '-ALL',
+ 'ECDHE-ECDSA-CHACHA20-POLY1305', # openssl-1.1.0, 1.0.2+cloudflare
+ 'ECDHE-RSA-CHACHA20-POLY1305', # openssl-1.1.0, 1.0.2+cloudflare
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
+ 'ECDHE-RSA-AES256-GCM-SHA384',
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
+ 'ECDHE-RSA-AES128-GCM-SHA256',
+ 'DHE-RSA-AES128-GCM-SHA256',
+ ],
+ # Forward-Secret, but not AEAD
+ 'mid' => [
+ 'ECDHE-ECDSA-AES128-SHA', # Various outdated IE, Safari<9, Android<4.4
+ 'ECDHE-RSA-AES128-SHA',
+ 'DHE-RSA-AES128-SHA', # Android 2.x, openssl-0.9.8, etc
+ ],
+ # not-forward-secret compat for ancient stuff
+ 'compat' => [
+ 'AES128-SHA', # Mostly evil proxies, also ancient devices
+ 'DES-CBC3-SHA', # Mostly IE7-8 on XP, also ancient devices
+ ],
}
+
+ # Final lists exposed to callers
+ ciphersuites = {
+ 'strong' => basic['strong'],
+ 'mid' => basic['strong'] + basic['mid'],
+ 'compat' => basic['strong'] + basic['mid'] + basic['compat'],
+ }
+
+ # Our standard HSTS for all public canonical domains
+ hsts_val = "max-age=31536000; includeSubDomains; preload"
+
newfunction(
:ssl_ciphersuite,
:type => :rvalue,
:doc => <<-END
Outputs the ssl configuration part of the webserver config.
Function parameters are:
- servercode - either nginx, apache-2.2 or apache-2.4
- encryption_type - either strong for PFS only, or compat for maximum
compatibility
- hsts_days - how many days should the STS header live. If not expressed, HSTS
will
- be disabled
+ server - either nginx or apache
+ encryption_type - strong, mid, or compat
+ hsts - optional boolean, true emits our standard public HSTS
Examples:
- ssl_ciphersuite('apache-2.4', 'compat') # Compatible config for apache 2.4
- ssl_ciphersuite('nginx', 'strong', '365') # PFS-only, use HSTS for 365 days
+ ssl_ciphersuite('apache', 'compat', true) # Compatible config for apache
+ ssl_ciphersuite('apache', 'mid', true) # FS-only for apache
+ ssl_ciphersuite('nginx', 'strong', true) # FS-only, AEAD-only, TLSv1.2-only
END
) do |args|
+ Puppet::Parser::Functions.function(:os_version)
+ Puppet::Parser::Functions.function(:notice)
if args.length < 2 || args.length > 3
fail(ArgumentError, 'ssl_ciphersuite() requires at least 2 arguments')
end
- servercode = args.shift
- case servercode
- when 'apache-2.4' then
- server = 'apache'
- server_version = 24
- when 'apache-2.2' then
- server = 'apache'
- server_version = 22
- when 'nginx' then
- server = 'nginx'
- server_version = nil
- else
- fail(ArgumentError, "ssl_ciphersuite(): unknown server string
'#{servercode}'")
+ server = args.shift
+ if server != 'apache' && server != 'nginx'
+ fail(ArgumentError, "ssl_ciphersuite(): unknown server string
'#{server}'")
end
ciphersuite = args.shift
- unless ciphersuites.has_key?(ciphersuite)
+ unless ciphersuites.key?(ciphersuite)
fail(ArgumentError, "ssl_ciphersuite(): unknown ciphersuite
'#{ciphersuite}'")
end
- cipherlist = ciphersuites[ciphersuite]
-
- if ciphersuite == 'strong' && server == 'apache' && server_version < 24
- fail(ArgumentError, 'ssl_ciphersuite(): apache 2.2 cannot work in strong
PFS mode')
- end
+ do_hsts = false
if args.length == 1
- hsts_days = args.shift.to_i
+ do_hsts = args.shift
+ end
+
+ # OS / Server -dependant feature flags:
+ nginx_always_ok = true
+ dhe_ok = true
+ if !function_os_version(['debian >= jessie'])
+ nginx_always_ok = false
+ if server == 'apache'
+ dhe_ok = false
+ end
+ end
+
+ if !dhe_ok && ciphersuite != 'compat'
+ function_notice([
+ 'ssl_ciphersuite(): OS needs upgrade to Jessie! Downgrading SSL
ciphersuite to "compat"'
+ ])
+ ciphersuite = 'compat'
+ end
+
+ if dhe_ok
+ cipherlist = ciphersuites[ciphersuite].join(":")
else
- hsts_days = nil
+ cipherlist = ciphersuites[ciphersuite].reject{|x| x =~
/^(DHE|EDH)-/}.join(":")
end
output = []
if server == 'apache'
- case ciphersuite
- when 'strong' then
- output.push('SSLProtocol all -SSLv2 -SSLv3 -TLSv1')
- when 'compat' then
+ if ciphersuite == 'strong'
+ output.push('SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1')
+ else
output.push('SSLProtocol all -SSLv2 -SSLv3')
end
output.push("SSLCipherSuite #{cipherlist}")
output.push('SSLHonorCipherOrder On')
- unless hsts_days.nil?
- hsts_seconds = hsts_days * 86400
- output.push("Header set Strict-Transport-Security
\"max-age=#{hsts_seconds}\"")
+ if dhe_ok
+ output.push('SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparam.pem"')
end
- else
- # nginx
- case ciphersuite
- when 'strong' then
- output.push('ssl_protocols TLSv1.1 TLSv1.2;')
- when 'compat' then
+ if do_hsts
+ output.push("Header always set Strict-Transport-Security
\"#{hsts_val}\"")
+ end
+ else # nginx
+ if ciphersuite == 'strong'
+ output.push('ssl_protocols TLSv1.2;')
+ else
output.push('ssl_protocols TLSv1 TLSv1.1 TLSv1.2;')
end
output.push("ssl_ciphers #{cipherlist};")
output.push('ssl_prefer_server_ciphers on;')
- unless hsts_days.nil?
- hsts_seconds = hsts_days * 86400
- output.push("add_header Strict-Transport-Security
\"max-age=#{hsts_seconds}\";")
+ if dhe_ok
+ output.push('ssl_dhparam /etc/ssl/dhparam.pem;')
+ end
+ if do_hsts
+ if nginx_always_ok
+ output.push("add_header Strict-Transport-Security \"#{hsts_val}\"
always;")
+ else
+ output.push("add_header Strict-Transport-Security
\"#{hsts_val}\";")
+ end
end
end
return output
diff --git
a/puppet/modules/wmflib/lib/puppet/parser/functions/validate_array_re.rb
b/puppet/modules/wmflib/lib/puppet/parser/functions/validate_array_re.rb
new file mode 100644
index 0000000..22d1e19
--- /dev/null
+++ b/puppet/modules/wmflib/lib/puppet/parser/functions/validate_array_re.rb
@@ -0,0 +1,23 @@
+# == Function: validate_array_re( array $items, string $re )
+#
+# Throw an error if any member of $items does not match the regular
+# expression $re.
+#
+# === Examples
+#
+# # OK -- each array item is a four-digit number.
+# validate_array_re([8123, 8124, 8125], '^\d{4}$')
+#
+# # Fail -- last array item is not a four-digit number.
+# validate_array_re([8123, 8124, 812], '^\d{4}$')
+#
+module Puppet::Parser::Functions
+ newfunction(:validate_array_re, :arity => 2) do |args|
+ items, re = args
+ re = Regexp.new(re)
+ invalid = args.first.find { |item| item.to_s !~ re }
+ unless invalid.nil?
+ fail(Puppet::ParseError, "Array element \"#{invalid}\" does not match
regular expression \"#{re.source}\".")
+ end
+ end
+end
diff --git a/puppet/modules/wmflib/spec/fixtures/hiera.proxy.yaml
b/puppet/modules/wmflib/spec/fixtures/hiera.proxy.yaml
new file mode 100644
index 0000000..1f20afb
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hiera.proxy.yaml
@@ -0,0 +1,16 @@
+:backends:
+ - proxy
+:proxy:
+ :datadir: 'spec/fixtures/hieradata'
+ :plugins:
+ - role
+ - nuyaml
+ :default_plugin: nuyaml
+:role:
+ :datadir: 'spec/fixtures/hieradata'
+:nuyaml:
+ :datadir: 'spec/fixtures/hieradata'
+:hierarchy:
+ - "hosts/%{::hostname}"
+ - "role@@common"
+ - common
diff --git a/puppet/modules/wmflib/spec/fixtures/hiera.yaml
b/puppet/modules/wmflib/spec/fixtures/hiera.yaml
new file mode 100644
index 0000000..d2dab0b
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hiera.yaml
@@ -0,0 +1,6 @@
+:backends:
+ - role
+:role:
+ :datadir: 'spec/fixtures/hieradata'
+:role_hierarchy:
+ - common
diff --git a/puppet/modules/wmflib/spec/fixtures/hieradata/common.yaml
b/puppet/modules/wmflib/spec/fixtures/hieradata/common.yaml
new file mode 100644
index 0000000..feb50a1
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hieradata/common.yaml
@@ -0,0 +1,2 @@
+cluster: misc
+mysql::innodb_threads: 15
diff --git a/puppet/modules/wmflib/spec/fixtures/hieradata/hosts/foo.yaml
b/puppet/modules/wmflib/spec/fixtures/hieradata/hosts/foo.yaml
new file mode 100644
index 0000000..477aee1
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hieradata/hosts/foo.yaml
@@ -0,0 +1,2 @@
+admin::groups:
+ - go-spurs
diff --git
a/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test.yaml
b/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test.yaml
new file mode 100644
index 0000000..878c890
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test.yaml
@@ -0,0 +1,5 @@
+admin::groups:
+ - FooBar
+an_hash:
+ test: true
+mysql::innodb_threads: 50
diff --git
a/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test2.yaml
b/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test2.yaml
new file mode 100644
index 0000000..87bac76
--- /dev/null
+++ b/puppet/modules/wmflib/spec/fixtures/hieradata/role/common/test2.yaml
@@ -0,0 +1,6 @@
+admin::groups:
+ - FooBar1
+an_hash:
+ test2: true
+ test3:
+ another: "level"
diff --git a/puppet/modules/wmflib/spec/functions/conftool_spec.rb
b/puppet/modules/wmflib/spec/functions/conftool_spec.rb
new file mode 100644
index 0000000..888c51b
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/conftool_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+require 'mocha/test_unit'
+require 'json'
+
+describe 'conftool' do
+
+ def gen_conftool_call(selector)
+ ['/usr/bin/conftool', '--object-type', 'node', 'select', selector, 'get']
+ end
+
+ generate = {}
+
+ before(:each) {
+ Puppet::Parser::Functions.newfunction(:generate) {
+ |args| generate.call(args)
+ }
+ generate.stubs(:call).returns('')
+ }
+
+ it "should fail if 3 args are given" do
+ should run.with_params('a', 'b', 'c').and_raise_error(Puppet::ParseError)
+ end
+
+ it "should return an empty list if no result is returned" do
+ req = {'name' => 'foo'}
+ should run.with_params(req).and_return([])
+ end
+
+ it "should return correctly the results" do
+ resultset = [
+ {"cp1052.eqiad.wmnet" => {"pooled" => "yes", "weight" => 100}, "tags"
=> "dc=eqiad,cluster=cache_text,service=varnish-be"},
+ {"cp1052.eqiad.wmnet" => {"pooled" => "yes", "weight" => 1}, "tags" =>
"dc=eqiad,cluster=cache_text,service=varnish-fe"},
+ ]
+ retval = resultset.map{ |x| JSON.dump(x) }.join "\n"
+ conftool_out = [
+ {'name' => 'cp1052.eqiad.wmnet', 'tags' => resultset[0]['tags'], 'value'
=> resultset[0]['cp1052.eqiad.wmnet']},
+ {'name' => 'cp1052.eqiad.wmnet', 'tags' => resultset[1]['tags'], 'value'
=> resultset[1]['cp1052.eqiad.wmnet']},
+ ]
+ req = { 'name' => 'cp1052.*', 'service' => 'varnish-..'}
+ genargs = gen_conftool_call('name=cp1052.*,service=varnish-..')
+ generate.stubs(:call).with(genargs).returns(retval)
+ should run.with_params(req).and_return(conftool_out)
+ end
+
+ it "should fail if conftool read fails" do
+ generate.stubs(:call).raises(Puppet::ParseError, 'something')
+ req = {'name' => 'foo'}
+ should run.with_params(req).and_raise_error(Puppet::ParseError)
+ end
+
+ it "should respond with an error if an empty tag is specified" do
+ should run.with_params({}).and_raise_error(Puppet::ParseError)
+ end
+end
diff --git a/puppet/modules/wmflib/spec/functions/ensure_mounted_spec.rb
b/puppet/modules/wmflib/spec/functions/ensure_mounted_spec.rb
new file mode 100644
index 0000000..0923893
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/ensure_mounted_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe "the ensure_mounted function" do
+ it "should exist" do
+ expect(Puppet::Parser::Functions.function("ensure_mounted")).to
eq("function_ensure_mounted")
+ end
+
+ it "should raise a ParseError if there are less than 1 arguments" do
+ expect {
+ scope.function_ensure_mounted([])
+ }.to raise_error(ArgumentError)
+ end
+
+ it "should raise a ParseError if there are more than 1 arguments" do
+ expect {
+ scope.function_ensure_mounted(['a', 'b'])
+ }.to raise_error(ArgumentError)
+ end
+
+ it "should return 'mounted' for param 'present'" do
+ expect(scope.function_ensure_mounted(['present'])).to eq('mounted')
+ end
+
+ it "should return 'mounted' for param 'true'" do
+ expect(scope.function_ensure_mounted([true])).to eq('mounted')
+ end
+
+ it "should return 'absent' for param 'absent'" do
+ expect(scope.function_ensure_mounted(['absent'])).to eq('absent')
+ end
+
+ it "should return 'false' for param 'false'" do
+ expect(scope.function_ensure_mounted([false])).to eq(false)
+ end
+
+end
diff --git a/puppet/modules/wmflib/spec/functions/hash_deselect_re_spec.rb
b/puppet/modules/wmflib/spec/functions/hash_deselect_re_spec.rb
new file mode 100644
index 0000000..805310c
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/hash_deselect_re_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe "the hash_deselect_re function" do
+ it "should exist" do
+ expect(Puppet::Parser::Functions.function("hash_deselect_re")).to
eq("function_hash_deselect_re")
+ end
+
+ it "should raise a ParseError if there are less than 2 arguments" do
+ expect {
+ scope.function_hash_deselect_re(['a'])
+ }.to raise_error(Puppet::ParseError)
+ end
+
+ it "should raise a ParseError if there are more than 2 arguments" do
+ expect {
+ scope.function_hash_deselect_re(['a', 'b', 'c'])
+ }.to raise_error(Puppet::ParseError)
+ end
+
+ it "should select the right keys (simple)" do
+ expect(
+ scope.function_hash_deselect_re(['^a', {'abc' => 1, 'def' => 2, 'asdf'
=> 3}])
+ ).to eq({'def' => 2})
+ end
+
+ it "should select the right keys (neg lookahead)" do
+ expect(
+ scope.function_hash_deselect_re(['^(?!a)', {'abc' => 1, 'def' => 2,
'asdf' => 3}])
+ ).to eq({'abc' => 1, 'asdf' => 3})
+ end
+end
diff --git a/puppet/modules/wmflib/spec/functions/hash_select_re_spec.rb
b/puppet/modules/wmflib/spec/functions/hash_select_re_spec.rb
new file mode 100644
index 0000000..cc86663
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/hash_select_re_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe "the hash_select_re function" do
+ it "should exist" do
+ expect(Puppet::Parser::Functions.function("hash_select_re")).to
eq("function_hash_select_re")
+ end
+
+ it "should raise a ParseError if there are less than 2 arguments" do
+ expect {
+ scope.function_hash_select_re(['a'])
+ }.to raise_error(Puppet::ParseError)
+ end
+
+ it "should raise a ParseError if there are more than 2 arguments" do
+ expect {
+ scope.function_hash_select_re(['a', 'b', 'c'])
+ }.to raise_error(Puppet::ParseError)
+ end
+
+ it "should select the right keys (simple)" do
+ expect(
+ scope.function_hash_select_re(['^a', {'abc' => 1, 'def' => 2, 'asdf' =>
3}])
+ ).to eq({'abc' => 1, 'asdf' => 3})
+ end
+
+ it "should select the right keys (neg lookahead)" do
+ expect(
+ scope.function_hash_select_re(['^(?!a)', {'abc' => 1, 'def' => 2, 'asdf'
=> 3}])
+ ).to eq({'def' => 2})
+ end
+end
diff --git a/puppet/modules/wmflib/spec/functions/ipresolve_spec.rb
b/puppet/modules/wmflib/spec/functions/ipresolve_spec.rb
new file mode 100644
index 0000000..070dc01
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/ipresolve_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+describe 'ipresolve' do
+
+ it "should resolve ipv4 addresses by default" do
+ should
run.with_params('install1002.wikimedia.org').and_return('208.80.154.86')
+ end
+ it "should resolve ipv4 addresses when explicitly asked to" do
+ should run.with_params('install1002.wikimedia.org',
'4').and_return('208.80.154.86')
+ end
+
+ it "should resolve ipv6 addresses" do
+ should run.with_params('install1002.wikimedia.org',
'6').and_return('2620::861:1:208:80:154:86')
+ end
+
+ it "should be able to perform a reverse DNS lookup" do
+ should run.with_params('2620::861:1:208:80:154:86',
'ptr').and_return('install1002.wikimedia.org')
+ should run.with_params('208.80.154.86',
'ptr').and_return('install1002.wikimedia.org')
+ end
+
+ it "fails when resolving an inexistent name" do
+ # This will test if your ISP does DNS hijacking too
+ should
run.with_params('host.does.not.exists').and_raise_error(RuntimeError)
+ end
+end
diff --git a/puppet/modules/wmflib/spec/functions/role_spec.rb
b/puppet/modules/wmflib/spec/functions/role_spec.rb
new file mode 100644
index 0000000..fd9a3b5
--- /dev/null
+++ b/puppet/modules/wmflib/spec/functions/role_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+describe 'role' do
+
+ before :each do
+ @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
+ @scope = Puppet::Parser::Scope.new(@compiler)
+ @scope.source = Puppet::Resource::Type.new(:node, :foo)
+ @scope.stub(:is_nodescope?).and_return(true)
+ @topscope = @scope.compiler.topscope
+ @scope.parent = @topscope
+ roleclasses = ['role::test', 'role::test2']
+ roleclasses.each do |roleclass|
+ unless @compiler.topscope.find_hostclass(roleclass)
+ host_cls = Puppet::Resource::Type.new(:hostclass, roleclass)
+ @scope.known_resource_types.add_hostclass(host_cls)
+ end
+ end
+ end
+
+ it "should be called with one parameter" do
+ should run.and_raise_error(ArgumentError)
+ end
+ it "throws error if called outside of the node scope" do
+ should run.with_params('cache::text').and_raise_error(Puppet::ParseError)
+ end
+
+ it "throws error if called on a non-existing role" do
+ expect { @scope.function_role(['foo::bar']) }.to
raise_error(Puppet::ParseError)
+ end
+
+ it "includes the role class" do
+ expect { @scope.function_role(['test']) }.to_not raise_error()
+ end
+
+ it "raises an error when called more than once in a scope" do
+ @scope.function_role(['test2'])
+ expect { @scope.function_role(['test']) }.to
raise_error(Puppet::ParseError)
+ end
+
+ it "adds the keys to the top-scope variable" do
+ @scope.function_role(['test', 'test2'])
+ expect(@topscope.lookupvar('_roles')).to eq({'test' => true, 'test2' =>
true})
+ end
+
+ it "includes the role classes" do
+ @scope.function_role(['test'])
+ expect(@scope.find_hostclass('role::test')).to
be_an_instance_of(Puppet::Resource::Type)
+ end
+end
diff --git a/puppet/modules/wmflib/spec/hiera/proxy_backend_spec.rb
b/puppet/modules/wmflib/spec/hiera/proxy_backend_spec.rb
new file mode 100644
index 0000000..9b0574e
--- /dev/null
+++ b/puppet/modules/wmflib/spec/hiera/proxy_backend_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+require 'hiera'
+require 'hiera/backend/proxy_backend'
+
+describe 'proxy_backend' do
+ before :each do
+ # Build a node with two roles applied
+ @hiera = Hiera.new({:config => 'spec/fixtures/hiera.proxy.yaml'})
+ Hiera::Config.load('spec/fixtures/hiera.proxy.yaml')
+ @backend = Hiera::Backend::Proxy_backend.new()
+ @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
+ @scope = Puppet::Parser::Scope.new(@compiler)
+ @scope.source = Puppet::Resource::Type.new(:node, :foo)
+ @scope.stub(:is_nodescope?).and_return(true)
+ @topscope = @scope.compiler.topscope
+ @topscope.setvar('hostname', 'foo')
+ @scope.parent = @topscope
+ roleclasses = ['role::test', 'role::test2']
+ roleclasses.each do |roleclass|
+ unless @compiler.topscope.find_hostclass(roleclass)
+ host_cls = Puppet::Resource::Type.new(:hostclass, roleclass)
+ @scope.known_resource_types.add_hostclass(host_cls)
+ end
+ end
+ end
+
+ it "lookup returns the default when no role is defined" do
+ expect(
+ @backend.lookup('mysql::innodb_threads',@topscope, nil, nil)
+ ).to eq(15)
+ end
+
+ it "lookup returns the role-specific value if a role is defined" do
+ @scope.function_role(['test'])
+ expect(
+ @backend.lookup('mysql::innodb_threads',@topscope, nil, nil)
+ ).to eq(50)
+ end
+
+ it "return the host-overridden value for a role-defined variable" do
+ @scope.function_role(['test'])
+ expect(
+ @backend.lookup('admin::groups',@topscope, nil, nil)
+ ).to eq(['go-spurs'])
+ end
+
+ it "merges values when using an array lookup" do
+ @scope.function_role(['test'])
+ expect(@backend.lookup('admin::groups', @topscope, nil, :array)).to
eq([['go-spurs'],['FooBar']])
+ end
+
+end
diff --git a/puppet/modules/wmflib/spec/hiera/role_backend_spec.rb
b/puppet/modules/wmflib/spec/hiera/role_backend_spec.rb
new file mode 100644
index 0000000..19c436a
--- /dev/null
+++ b/puppet/modules/wmflib/spec/hiera/role_backend_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'hiera/backend/role_backend'
+require 'hiera'
+
+describe 'role_backend' do
+ before :each do
+
+ @hiera = Hiera.new({:config => 'spec/fixtures/hiera.yaml'})
+ Hiera::Config.load('spec/fixtures/hiera.yaml')
+ @backend = Hiera::Backend::Role_backend.new()
+ @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
+ @scope = Puppet::Parser::Scope.new(@compiler)
+ @scope.source = Puppet::Resource::Type.new(:node, :foo)
+ @scope.stub(:is_nodescope?).and_return(true)
+ @topscope = @scope.compiler.topscope
+ @scope.parent = @topscope
+ roleclasses = ['role::test', 'role::test2']
+ roleclasses.each do |roleclass|
+ unless @compiler.topscope.find_hostclass(roleclass)
+ host_cls = Puppet::Resource::Type.new(:hostclass, roleclass)
+ @scope.known_resource_types.add_hostclass(host_cls)
+ end
+ end
+ end
+
+ it "get_path returns the correct path" do
+ expect(@backend.get_path('pippo', 'test', 'common', @scope)).to
eq("spec/fixtures/hieradata/role/common/test.yaml")
+ end
+
+ it "lookup returns nil when no role is defined" do
+ expect(@backend.lookup('admin::groups', @topscope, nil, nil)).to eq(nil)
+ end
+
+ it "lookup returns a value when a role is defined" do
+ @scope.function_role(['test'])
+ expect(@backend.lookup('admin::groups', @topscope, nil, nil)).to
eq(['FooBar'])
+ end
+
+ it "lookup raises an error if conflicting values are given in different
roles" do
+ @scope.function_role(['test', 'test2'])
+ expect {@backend.lookup('admin::groups', @topscope, nil, nil)}.to
raise_error(Exception, "Conflicting value for admin::groups found in role
test2")
+ end
+
+ it "merges values when using an array lookup" do
+ @scope.function_role(['test', 'test2'])
+ expect(@backend.lookup('admin::groups', @topscope, nil, :array)).to
eq([['FooBar'],['FooBar1']])
+ end
+
+ it "merges values when using hash lookup" do
+ @scope.function_role(['test', 'test2'])
+ expect(@backend.lookup('an_hash', @topscope, nil, :hash)).to
eq({"test2"=>true, "test3"=>{"another"=>"level"}, "test"=>true})
+ end
+end
diff --git a/puppet/modules/wmflib/spec/spec_helper.rb
b/puppet/modules/wmflib/spec/spec_helper.rb
new file mode 100644
index 0000000..d3923f8
--- /dev/null
+++ b/puppet/modules/wmflib/spec/spec_helper.rb
@@ -0,0 +1,8 @@
+require 'rspec-puppet'
+
+fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures'))
+
+RSpec.configure do |c|
+ c.module_path = File.join(fixture_path, 'modules')
+ c.manifest_dir = File.join(fixture_path, 'manifests')
+end
--
To view, visit https://gerrit.wikimedia.org/r/343956
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I3ef41e75539befb10f13394b03d9d47cb90ef1dd
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/vagrant
Gerrit-Branch: master
Gerrit-Owner: BryanDavis <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits