diff --git a/.gitignore b/.gitignore index 5e1422c..c0e41d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +**/*.swp + *.gem *.rbc /.config @@ -42,9 +44,11 @@ build-iPhoneSimulator/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# Gemfile.lock +Gemfile.lock # .ruby-version # .ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc + +/spec/fixtures/modules/** diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..4f662dc --- /dev/null +++ b/Gemfile @@ -0,0 +1,18 @@ +source ENV['GEM_SOURCE'] || 'https://rubygems.org' + +puppetversion = ENV.key?('PUPPET_VERSION') ? ENV['PUPPET_VERSION'] : ['>= 3.3'] +gem 'metadata-json-lint' +gem 'puppet', puppetversion +gem 'puppetlabs_spec_helper', '>= 1.0.0' +gem 'puppet-lint', '>= 1.0.0' +gem 'facter', '>= 1.7.0' +gem 'rspec-puppet' + +# rspec must be v2 for ruby 1.8.7 +if RUBY_VERSION >= '1.8.7' && RUBY_VERSION < '1.9' + gem 'rspec', '~> 2.0' + gem 'rake', '~> 10.0' +else + # rubocop requires ruby >= 1.9 + gem 'rubocop' +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..abf1725 --- /dev/null +++ b/README.md @@ -0,0 +1,549 @@ +# Puppet module for libelektra + +#### Table of Contents + +1. [Description](#description) + * [Elektra](#elektra) +1. [Setup - The basics of getting started with libelektra](#setup) + * [Elektra installation](#elektra-installation) + * [Setup requirements](#setup-requirements) + * [Beginning with libelektra](#beginning-with-libelektra) +1. [Usage - Basica and Examples](#usage) +1. [Reference - An under-the-hood peek at what the module is doing](#reference) + * [Kdbkey - manage Elektra keys](#kdbkey) + * [Kdbmount - manage Elektra mountpoints](#kdbmount) +1. [Limitations](#limitations) +1. [Development - Guide for contributing to the module](#development) +1. [Release Notes](#release-notes) + +## Description + +Puppet module for *libelektra* (https://www.libelektra.org). This allows +key-value based configuration manipulation. + +### Elektra + +Elektra is a general purpose key value based configuration framework. It +manages its keys in a global, modular and hierarchically organized key space: + + * **global**: all processes on the same machine access the same key space + * **hierarchical**: the key space is structured as a tree, similar to the UNIX + file system. Each key has a unique name, similar to the absolute path of a + file. + * **modular**: the key space can be split up in several parts, whereas each + part corresponds to a different configuration file. This split-up is called + **mounting**. Similar to a UNIX file system (the whole file system can be + split in different disks, locations...), the Elektra key space can be built + up by a set of different configuration files. A set of Elektra plugins define + how a configuration file is integrated into the Elektra key space (defining + which storage format is used, conversion parameter...). + +This *mounting* process makes Elektra very suitable for Puppet. Mounting allows +us to integrate different configuration files, of different formats, into the +Elektra key space. Once in the key space, configuration settings can be +manipulated on a key value basis. For example, to change the 'workgroup' in +Samba's configuration file everything needed is: +```puppet +kdbmount { 'system/sw/samba': + file => '/etc/samba/smb.conf', + plugins => ['ini'] +} + +kdbkey { 'system/sw/samba/global/workgroup': + value => 'MY_WORKGROUP' +} +``` + +Elektra also provides CLI tools to operate on the Elektra key space: +```sh +$> kdb get system/sw/samba/global/workgroup +MY_WORKGROUP +$> kdb set system/sw/samba/global/workgroup OTHER +Set string to OTHER +``` +Another important feature of Elektra is called *configuration specification*. +This allows us to define value (and even structure) restrictions. This means, we +can instruct Elektra to perform validation checks before writing a configuration +file. For example, if an application only accepts numeric values in the range of +1-10 for a certain setting, we can add this check with: +```puppet +kdbmount { 'system/sw/myapp': + file => '/etc/myapp/config.ini', + plugins => ['ini', 'type', 'range'] # add checking plugins +} + +kdbkey { 'system/sw/myapp/instances': + value => $instances, + check => { + 'type' => 'short', + 'range' => '1-10' + } +} +``` +This *configuration specification* stored within Elektra, thus it is now active +for other Elektra aware tools: +``` +$> kdb set system/sw/myapp/instances 11 +The command kdb set failed while accessing the key database with the info: +... +Description: value not within specified range. +Reason: value 11 not within range 1-10 +... +``` + +For further details on Elektra see https://www.libelektra.org/ + +## Setup + +The 'libelektra' Puppet module currently requires a recent Elektra version +(0.8.19) to work correctly. + +Elektra has to be installed on each managed node. In theory, Elektra is not +required on the master node, if the master node is unmanaged. + +### Elektra installation + +For Elektra installation instructions see +[Elektra installation](https://www.libelektra.org/docgettingstarted/installation). + + +The 'libelektra' Puppet module integrates with Elektra by the Elektra Ruby bindings +or the Elektra CLI tool `kdb`. Although, `kdb` is the minimum requirement for +this module to work, it is **highly** recommended to install Elektra's Ruby +bindings. + +### Setup Requirements + +We currently only support Linux operating systems. (Tested on Ubuntu Xenial and +Debian Jessie) + +### Beginning with libelektra + +After a successful Elektra installation, install the 'libelektra' Puppet module. +Currently this can only be done by cloning the Github repo. + +Once the module is released, it will be available in Puppet forge. + +## Usage + +The 'libelektra' Puppet module provides two resource types for managing +configuration files with Elektra: + + * **kdbmount**: mount configuration file into the Elektra key space + * **kdbkey**: manipulate configuration settings through Elektra + +To start configuring your systems with 'libelektra', you first have to integrate +(**mount**) your configuration files into the Elektra key space: +```puppet +kdbmount { 'system/sw/samba': # Elektra mount path + file => '/etc/samba/smb.conf', # path to configuration file + plugins => ['ini'] # list of Elektra plugins used for mounting +} +``` +Now all configuration settings defined in `/etc/samba/smb.conf` are available +under the Elektra path `system/sw/samba`. So `system/sw/samba/global/workgroup` +refers to the setting `workgroup` in section `global` in config file +`/etc/samba/smb.conf`. + +Now we can manipulate smb.conf settings: +```puppet +# add a new logging parameter +kdbkey { 'system/sw/samba/global/logging': + value => 'syslog@1 file' +} + +# remove the 'debuglevel' setting +kdbkey { 'system/sw/samba/global/debuglevel': + ensure => absent +} +``` + +**Autorequires**: A order relation ship ('requires', 'before'...) between the +`kdbmount` and `kdbkey` definitions is not required. This is added implicitly. + +We often manipulate settings under a certain Elektra path. To avoid using the +the full Elektra path over and over again, we can use the `prefix` parameter` +together with resource defaults here: +```puppet +class samba::config { + $mountpoint = 'system/sw/samba' + + kdbmount { $mountpoint: + file => '/etc/samba/smb.conf', + plugins => ['ini', 'enum'] # use the enum check plugin + } + + Kdbkey { + prefix => $mountpoint + } + + # the Elektra path is concatenated by `prefix` and `name` parameters + kdbkey { 'global/workgroup': + value => 'MY_WORKGROUP' + } + + # it can be even be more readable (Note: ';' at the end) + kdbkey { + 'global/logging': value => 'syslog@1 file'; + 'global/log level': value => '3 auth:10'; + } + + # sections are created automatically + kdbkey { + 'my_share/path': + value => '/var/data/my_share', + # goes in smb.conf as comment line + comment => 'This is my share definition'; + + 'my_share/comment': + value => 'This is my share'; + + 'my_share/guest ok': + value => 'yes', + # only allow 'yes' or 'no' + check => { 'enum' => ['yes', 'no'] }; + } +``` + +## Reference + +Obtained by `puppet doc` + +### kdbkey + +Manage libelekra keys. + +This resource type allows to define and manipulate keys of libelektra's +key database. + +#### Parameters + +* `name`: The fully qualified name of the key. +* `ensure`: The basic property that the resource should be in. +* `value`: Desired value of the key. +* `prefix`: Prefix for the key name (optional). +* `check`: Add value validation. +* `comments`: Comments for this key. +* `user`: Define or modify key in the context of given user. +* `metadata`: Metadata for this key supplied as Hash of key-value pairs. +* `purge_meta_keys`: Manage complete set of metadata keys. +* `provider`: The specific backend to use for this `kdbkey` resource. + +##### Parameter Details + +* `check`: Add value validation. + + This property allows to define certain restrictions to be applied on the + key value, which are automatically checked on each key database write. These + validation checks are performed by Elektra itself, so modifications done + by other applications will be also restricted to the defined value + specifications. + + The value for this property can be either a single String or a Hash + of settings. The following plugins were tested with puppet-elektra: + + * `path`: check for an absolute path name + + The 'path' plugin does not require any additional settings + so it is enough to just pass 'path' as 'check' value. + ```puppet + kdbkey { 'system/sw/myapp/setting1': + check => 'path', + value => '/some/absolute/path/will/pass' + } + ``` + Note: this does not check if the path really exists (instead it just + issues a warning). The check will fail, if the given value is not an + absolute path. + + * `network`: check for a valid IP address + + The network plugin checks if the supplied value is valid IP address. + ```puppet + kdbkey { 'system/sw/myapp/myip': + check => 'ipaddr', + value => ${given_myip} + } + ``` + to check for valid IPv4 addresses use + ```puppet + kdbkey { 'system/sw/myapp/myip': + check => { 'ipaddr' => 'ipv4' }, # works with 'ipv6' too + value => $given_myip + } + ``` + + * `type`: type checks + + The `type` plugin checks if the supplied key value conforms to a defined + data type (e.g. numeric value). Additionally, it is able to check if + the supplied key value is within an allowed range. + ```puppet + kdbkey { 'system/sw/myapp/port': + check => { 'type' => 'unsigned_long' }, + value => $given_port + } + + kdbkey { 'system/sw/myapp/num_instances': + check => { + 'type' => 'short', + 'type/min' => 1, + 'type/max' => 20 + }, + value => $given_num_instance + } + ``` + + * `range`: checks if value is within one ore more ranges + + ```puppet + kdbkey { 'system/sw/myapp/value': + check => { 'range' => '1-10,12-20' }, # <1, 11 and >20 is not allowed + value => $value + } + ``` + + * `enum`: define a list of valid values + + The enum plugin check it the supplied value is within a predefined set + of values. Two different formats are possible: + ```puppet + kdbkey { 'system/sw/myapp/scheduler': + # as string, values seperated with ', ' and encloseed by ' + check => { 'enum' => "'ondemand', 'performance', 'energy saving'" }, + value => $given_scheduler + } + + kdbkey { 'system/sw/myapp/notification': + # as array of strings + check => { 'enum' => ['off', 'email', 'slack', 'irc'] }, + value => $given_notification + } + ``` + + * `validation`: perform regular expression checks + + The validation plugin checks if the supplied value matches a predefined + regular expression: + ```puppet + kdbkey { 'system/sw/myapp/email': + check => { + 'validation' => '^[a-z0-9._]+@mycompany.com$' + 'validation/message' => 'we require an internal email address here', + 'validation/ignorecase' => '', # existence of flag is enough + } + ... + } + ``` + + For further plugins see the Elektra + [plugin documentation](https://www.libelektra.org/plugins/readme). + + Note: for each 'check/xxx' metadata, required by the Elektra plugins, just + remove the 'check/' part and add it to the 'check' property here. + (e.g. validation plugin: 'check/validation' => 'validation' ...) + +* `comments`: comments for this key + + Comments form a critical part of documentation. May configuration file + formats support adding comment lines. Libelektra plugins parse comments + and add them as metadata keys to the corresponding keys. This attribute + allows to manage those comment lines. + + Multi-line comments (those including a newline character) are implicitly + converted to a multi-line comment. + +* `ensure`: The basic property that the resource should be in. + + Valid values are `present`, `absent`. + +* `metadata`: Metadata for this key supplied as Hash of key-value pairs. + + The concrete behaviour is defined by the parameter `purge_meta_keys`. + The default case (`purge_meta_keys` => false) is to manage the specified + metadata keys only. Already present but not specified metadata keys will not + be removed. If `purge_meta_keys` is set to true, already present but not + specified metadata keys will be removed. + + Examples: + ```puppet + kdbkey { 'system/sw/app/s1': + metadata => { + 'owner' => 'me', + 'other meta' => 'you' + } + } + ``` + +* `name`: The fully qualified name of the key + + (**Namevar:** If omitted, this parameter's value defaults to the resource's title.) + + Elektra manages its keys within several namespaces ('system', 'user', + 'dir'... see + [Elektra-namespaces](https://www.libelektra.org/manpages/elektra-namespaces) + for details.) + + Cascading key names (keys starting with a '/') are probably not optimal + here, as they are implicitly converted to a key name with the 'dir', + 'user' or 'system' namespace. + +* `prefix`: Prefix for the key name (optional) + + If given, this value will prefix the given libelektra key name. + e.g.: + ```puppet + kdbkey { 'puppet/x1': + prefix => 'system/test', + value => 'hello' + } + ``` + This will manage the key 'system/test/puppet/x1'. + + Prefix and name are joined with a '/', if prefix does not end with '/' + or name does not start with '/'. + + Both, name and prefix parameter are used to uniquely identify a + libelektra key. + +* `provider`: The specific backend to use for this `kdbkey` resource. + + You will seldom need to specify this --- Puppet will usually + discover the appropriate provider for your platform. + + Default: `ruby` if Ruby bindings are installed + + Available providers are: + + * kdb: manage keys through `kdb` command + + * Required binaries: `kdb`. + * Supported features: `user`. + + * ruby: manage keys through libelektra Ruby API + + * Supported features: `user`. + +* `purge_meta_keys`: manage complete set of metadata keys + + If set to true, kdbkey will remove all unspecifed metadata keys, ensuring + only the specified set of metadata keys will exist. Otherwise, + unspecified metadata keys will not be touched. + + Valid values are `true`, `false`, `yes`, `no`. + +* `user`: define/modify key in the context of given user. + + This is only relevant, if key name referes to a user context, thus is + either cascading (starting with a '/') or is within the 'user' + namespace (starting with 'user/'). + +* `value`: Desired value of the key. + + This can be any type, however elektra currently + just manages to store String values only. Therefore all types are + implicitly converted to Strings. + + If value is an array, the key is managed as an Elektra array. Therefore + a subkey named `/#` will be created for each array element. + + + +### kdbmount + +Manage libelekra global key-space. + +This resource type allows to define and manipulate libelektra's global key +database. Libelektra allows to 'mount' external configuration files into +its key database. A specific libelektra backend plugin is for reading and +writing the configuration file. + +#### Parameters + +* `name`: The fully qualified mount path within the libelektra key database. +* `ensure`: The basic property that the resource should be in. +* `file`: The configuration file to mount into the Elektra key database. +* `plugins`: A list of libelektra plugins with optional configuration settings +* `provider`: The specific backend to use for this `kdbmount` resource. +* `resolver`: The resolver plugin to use for mounting. +* `add_recommended_plugins`: If set to true, Elektra will add recommended + +##### Parameter Details + +* `add_recommended_plugins`: If set to true, Elektra will add recommended + plugins to the mounted backend configuration. + Recommended plugins are defined by metadata of specified plugins. E.g. the + `hosts` plugins recommends `glob`, `error` and `network`. So, if mounting a + file with the `hosts` plugin and this parameter set to `true` all four + plugins will be used for mounting. + Default: true + + Valid values are `true`, `false`, `yes`, `no`. + +* `ensure`: The basic property that the resource should be in. + + Valid values are `present`, `absent`. + +* `file`: (**mandatory**) The configuration file to mount into the Elektra + key database. + +* `name`: The fully qualified mount path within the libelektra key database. + +* `plugins`: (**mandatory**) A list of libelektra plugins with optional + configuration settings + use for mounting. + + The following value formats are acceped: + - a string value describing a single plugin name + - an array of string values each defining a single plugin + - a hash of plugin names with corresponding configuration settings + e.g. + ```puppet + [ 'ini' => { + 'delimiter' => " " + 'array' => '' + }, + 'type' + ] + ``` + +* `provider`: The specific backend to use for this `kdbmount` resource. + You will seldom need to specify this --- Puppet will usually discover the + appropriate provider for your platform. + + Available providers are: + + * `kdb`: kdbmount through kdb command + + * Required binaries: `kdb`. + + * `ruby`: kdbmount through libelektra Ruby API + + * Default for `kernel` == `Linux`. + +* `resolver`: The resolver plugin to use for mounting. + + Default: 'resolver' + + + + + +## Limitations + +The 'libelektra' Puppet module was only tested with Puppet 3.x (3.8). Since +Puppet 4.x uses its own Ruby runtime, the system installed Elektra Ruby bindings +can't be used by 'libelektra'. Thus the fallback provider `kdb` for both +resource types (kdbkey and kdbmount) are usable only. + +## Development + +This module is hosted under +https://github.com/ElektraInitiative/puppet-libelektra + +Contributions welcome ;) + +## Release Notes + +Currently the 'libelektra' Puppet module is under heavy development. No releases +till now. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..02609e3 --- /dev/null +++ b/Rakefile @@ -0,0 +1,32 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' +require 'metadata-json-lint/rake_task' + +if RUBY_VERSION >= '1.9' + require 'rubocop/rake_task' + RuboCop::RakeTask.new +end + +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.relative = true +PuppetLint.configuration.ignore_paths = ['spec/**/*.pp', 'pkg/**/*.pp'] + +desc 'Validate manifests, templates, and ruby files' +task :validate do + Dir['manifests/**/*.pp'].each do |manifest| + sh "puppet parser validate --noop #{manifest}" + end + Dir['spec/**/*.rb', 'lib/**/*.rb'].each do |ruby_file| + sh "ruby -c #{ruby_file}" unless ruby_file =~ %r{spec/fixtures} + end + Dir['templates/**/*.erb'].each do |template| + sh "erb -P -x -T '-' #{template} | ruby -c" + end +end + +desc 'Run metadata_lint, lint, validate, and spec tests.' +task :test do + [:metadata_lint, :lint, :validate, :spec].each do |test| + Rake::Task[test].invoke + end +end diff --git a/examples/hosts.pp b/examples/hosts.pp new file mode 100644 index 0000000..3842aeb --- /dev/null +++ b/examples/hosts.pp @@ -0,0 +1,33 @@ + +kdbmount { 'system/network/hosts': + ensure => present, + file => 'myhosts', + plugins => 'hosts' +} + +kdbkey { 'dragon': + prefix => 'system/network/hosts/ipv4', + value => '192.168.1.140', + check => 'network' +} + +kdbkey { 'dragon/office': + ensure => present, + # value => '192.168.1.140', + prefix => 'system/network/hosts/ipv4', +} + +kdbkey { 'dragon/dell': + ensure => present, + prefix => 'system/network/hosts/ipv4', +} + +kdbkey { 'blacksheep': + prefix => 'system/network/hosts/ipv4', + value => '192.168.1.118', + check => 'network', + comments => 'headless virtualization host + other + + comment' +} diff --git a/examples/init.pp b/examples/init.pp new file mode 100644 index 0000000..23f7520 --- /dev/null +++ b/examples/init.pp @@ -0,0 +1,14 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation +# errors and view a log of events) or by fully applying the test in a virtual +# environment (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: +# https://docs.puppet.com/guides/tests_smoke.html +# + +#include ::libelektra + diff --git a/examples/kdbkey.pp b/examples/kdbkey.pp new file mode 100644 index 0000000..68ae6bc --- /dev/null +++ b/examples/kdbkey.pp @@ -0,0 +1,213 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation +# errors and view a log of events) or by fully applying the test in a virtual +# environment (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: +# https://docs.puppet.com/guides/tests_smoke.html +# + +#include ::libelektra + +$ns = 'user/test/puppet' + +kdbkey { "${ns}/x1": + ensure => present, + value => 'hello world x1 ...' +} + +kdbkey { "${ns}/x2": + ensure => absent +} + +kdbkey { "${ns}/x3": + ensure => present +} + +kdbkey { "${ns}/x4": + ensure => present, + value => 'x4 value ...', + purge_meta_keys => true, + metadata => { + 'meta1' => 'm1 value', + 'meta2' => 'm2 value', + #'meta3' => 'm3 value' + }, + comments => 'hello world' +} + +kdbkey { "${ns}-test/section1/setting1": + ensure => present, + value => 'hello ini world ...', + metadata => { + 'comments' => '#1', + 'comments/#0' => '# this is the first comment line', + 'comments/#1' => '# this is the second comment line' + } +} + +kdbkey { "${ns}-test/section1/setting2": + ensure => present, + value => 'some value ...', + comments => ' +this setting will do the most important stuff +with a multi line comment +here comes the setting +m line1 +m line2' +} + + +kdbkey { "${ns}-test/section2/setting1": + value => 'asdf', + comments => '' + # before => Kdbkey["${ns}-test/section2"] +} + + +# +# prefix tests +# + +# results in "${ns-test}/prefixtest/s1" +kdbkey { '/prefixtest/s1': + prefix => "${ns}-test", + value => 'hello prefix' +} + +# results in "${ns-test}/prefixtest/s12" +# will lead to duplicate resource error if it matches the above one (eaven +# with a different resource title) +kdbkey { 'something else': + name => '/prefixtest/s12', + prefix => "${ns}-test", + value => 'hello prefix' +} + + +# +# set keys in the context of a given user +# +# kdbkey { 'user/test/puppet/usertest/x1': +# value => 'asdf', +# user => 'bernhard', +# #provider => 'kdb' +# } + + +# +# validation +# +$ns_validation = 'user/test/puppet-val' + +# we require some validation plugins activated +# on the mountpoint corresponding to our settings +kdbmount { $ns_validation: + file => 'puppet-val.ini', + plugins => ['ini', 'type', 'enum', 'validation', 'range'], +} + +# ensure our setting is of type 'short' +# (see '$> kdb info type' for other types) +kdbkey { 'spec/x1': + prefix => $ns_validation, + value => 11, + check => { + 'range' => '0-10' + }, + provider => 'ruby' +} + +kdbkey { 'spec/x2': + prefix => $ns_validation, + value => '5', + check => {'type' => 'short' } +} + +# the type plugin is also aware of doing range checks +kdbkey { 'spec/x3': + prefix => $ns_validation, + value => 10, + check => { + 'type' => 'short', + 'type/min' => 0, # lower bound + 'type/max' => 10 # upper bound + } +} + +# enums with array of values +kdbkey { 'spec/enumx': + prefix => $ns_validation, + check => {'enum' => ['low', 'middle', 'high']}, + value => 'low', +} + +# or specify allowed values with on string +# (Note: allowed values have to be enclosed in single quotes and +# seperated by ", ") +kdbkey { 'spec/enum_x2': + prefix => $ns_validation, + check => { 'enum' => "'one', 'two', three'" }, + value => 'one' +} + +# ensure only valid absolute path names are used +kdbkey { 'spec/path_key': + prefix => $ns_validation, + check => 'path', + value => '/this/is/an/abolute/path' +} + +# do regular expression checks on key settings +kdbkey { 'spec/regex_key': + prefix => $ns_validation, + check => { + 'validation' => '^hello (world|master)$', + #'validation/ignorecase' => 0, + }, + value => 'hello world', +} + +kdbkey { 'spec/short2': + comments => " + + this is a simple short value + + no bound checks are performed", + prefix => $ns_validation, + check => { type => short }, + value => 5 +} + + + +# +# autorequire +# +$mount_ar = 'user/test/puppet-ar' + +kdbkey { 's1/x1': + ensure => present, + prefix => $mount_ar, +} + +kdbkey { 's3/x2': + prefix => $mount_ar, + value => "hello" +} + +kdbkey { + "$mount_ar/s2/x3": value => 'xx3', metadata => {'internal/ini/key/number' => '1'}; + "$mount_ar/s2/x4": value => 'xx4', metadata => {'internal/ini/key/number' => '2'}; + "$mount_ar/s2/x5": value => 'xx5', metadata => {'internal/ini/key/number' => '3'}; + "$mount_ar/s2/x6": value => 'xx6', metadata => {'internal/ini/key/number' => '4'}; + "$mount_ar/s2/x7": value => 'xx7', metadata => {'internal/ini/key/number' => '5'}; +} + +kdbmount { $mount_ar: + file => 'puppet-arxx.ini', + plugins => ['ini', 'type'] +} diff --git a/examples/kdbmount.pp b/examples/kdbmount.pp new file mode 100644 index 0000000..d0391d5 --- /dev/null +++ b/examples/kdbmount.pp @@ -0,0 +1,36 @@ + + + +kdbmount { 'system/sw/ssh/sshd': + ensure => present, + file => '/etc/ssh/sshd_config', + plugins => [ + 'ini' => { + 'array' => '', + 'delimiter' => ' ' + }, + ] + #plugins => ['sync', 'ini'] +} + +kdbmount { '/test/cascading': + ensure => present, + file => 'test.ini', + plugins => 'ini' +} + +kdbmount { '/test/cas2': + file => '/tmp/test.ini', + resolver => 'noresolver' +} + + +kdbmount { 'system/jenkins': + file => '/tmp/jenkins.xml', + plugins => [ + 'augeas' => { + #'lens' => '/usr/share/augeas/lenses/dist/xml.aug' + 'lens' => 'Xml.lns' + } + ] +} diff --git a/examples/multifile.pp b/examples/multifile.pp new file mode 100644 index 0000000..7aff7e8 --- /dev/null +++ b/examples/multifile.pp @@ -0,0 +1,19 @@ + + + +kdbmount { 'user/test/mm': + file => 'mmtest.json', + #file => 'mmtest.ini', + plugins => ['json', 'type', 'enum'] + #plugins => ['ini', 'type', 'enum'] +} + + +kdbkey { + 'user/test/mm/s1': value => "ss1"; + 'user/test/mm/s2': value => 'ss2'; + 'user/test/mm/section abc/x1': value => 'sec_x1'; + 'user/test/mm/section abc/x2': value => 'sec_x2'; + 'user/test/mm/section xyz/y1': value => 'sec_y1'; + 'user/test/mm/section xyz/y2': value => 'sec_y2'; +} diff --git a/lib/puppet/provider/kdbkey/common.rb b/lib/puppet/provider/kdbkey/common.rb new file mode 100644 index 0000000..d4335ae --- /dev/null +++ b/lib/puppet/provider/kdbkey/common.rb @@ -0,0 +1,71 @@ +# encoding: UTF-8 +## +# @file +# +# @brief common functions for all kdbkey provider +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# + +class Puppet::Provider::KdbKeyCommon < Puppet::Provider + + # just used for debugging + attr_accessor :verbose + @verbose = false + + + def skip_this_metakey?(metakey, keep_if_specified = false) + # skip modifing these keys at all times (even if user wants to) + return true if metakey.start_with? "internal/" + + # if user specifies these meta keys, let them do so + unless @resource[:metadata].nil? + unless keep_if_specified and @resource[:metadata].include? metakey + return true if metakey == "order" + return true if /^comments?\/#/ =~ metakey or metakey == "comments" + end + end + return false + end + + def is_special_meta_key?(metakey) + return true if metakey.start_with? "internal/" + return true if metakey.start_with? "comment" + return true if metakey == "order" + return false + end + + def array_key_name(name, index) + index_str = index.to_s + (1..(index.to_s.size - 1)).each { index_str = "_#{index_str}" } + "#{name}/##{index_str}" + end + + def get_spec_key_name(keyname = @resource[:name]) + return keyname.gsub(/^\w*\//, "spec/") + end + + def specified_checks_to_meta(value) + # ensure we have a Hash + value = {value => ""} if value.is_a? String + + spec_to_set = {} + value.each do |check_key, check_value| + if check_value.is_a? Array + # if value is an Array define an Elektra array + check_value.each_with_index do |v, index| + spec_to_set["check/#{check_key}/##{index}"] = v + end + # at least the 'enum' plugin requires to have the last array index + # set at its root key => check/enum = #x + spec_to_set["check/#{check_key}"] = "##{check_value.size - 1}" + else + spec_to_set["check/#{check_key}"] = check_value + end + end + spec_to_set + end + + +end diff --git a/lib/puppet/provider/kdbkey/kdb.rb b/lib/puppet/provider/kdbkey/kdb.rb new file mode 100644 index 0000000..34edd04 --- /dev/null +++ b/lib/puppet/provider/kdbkey/kdb.rb @@ -0,0 +1,291 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Kdb provider for type kdbkey for managing libelektra keys +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# + +require_relative 'common' +require 'tempfile' + +module Puppet + Type.type(:kdbkey).provide :kdb, :parent => Puppet::Provider::KdbKeyCommon do + desc "kdb through kdb command" + + has_feature :user + + commands :kdb => "kdb" + + def run_kdb(args, params = {:combine => true, :failonfail => true}) + cmd_line = [command(:kdb)] + args + params[:uid] = @resource[:user] unless @resource[:user].nil? + execute(cmd_line, params) + end + + def create + self.value= @resource[:value] + self.check= @resource[:check] unless @resource[:check].nil? + self.metadata= @resource[:metadata] unless @resource[:metadata].nil? + self.comments= @resource[:comments] unless @resource[:comments].nil? + end + + def destroy + run_kdb ["rm", @resource[:name]] + # remove possible array elements + list_keys.each do |x| + if x =~ /#{@resource[:name]}\/#_*\d+/ + run_kdb ["rm", x] + end + end + end + + def exists? + Puppet.debug "kdbkey/kdb exists? #{@resource[:name]}" + output = execute([command(:kdb), "get", @resource[:name]], + :failonfail => false) + output.exitstatus == 0 + end + + def list_keys + output = run_kdb ["ls", "--color=never", @resource[:name]] + return output.split + end + + def value + elems = list_keys + + unless elems.include? "#{@resource[:name]}/#0" + # single value key + return [get_key_value(@resource[:name])] + else + # Array key + value = [] + elems.select do |x| + x =~ /^#{@resource[:name]}\/#_*\d+$/ + end.each do |x| + value << get_key_value(x) + end + return value + end + + end + + def get_key_value(key) + return run_kdb ["sget", "--color=never", key, "''"] + end + + def value=(value) + remove_from_this_index = 0 + if not value.is_a? Array + set_key_value @resource[:name], value + + elsif value.size == 1 + set_key_value @resource[:name], value[0] + + else + set_key_value @resource[:name], '' + value.each_with_index do |elem_value, index| + set_key_value array_key_name(@resource[:name], index), elem_value + end + remove_from_this_index = value.size + end + # remove possible "old" array keys + output = run_kdb ["ls", "--color=never", @resource[:name]] + output.split.each do |x| + if x =~ /^#{@resource[:name]}\/#(\d+)$/ + index = $1.to_i + if index >= remove_from_this_index + run_kdb ['rm', x] + end + end + end + end + + def set_key_value(key, value) + run_kdb ["set", key, value] + end + + def read_metadata_values + read_metadata_values_from_key @resource[:name] + end + + def read_metadata_values_from_key(key_to_read_from) + puts "read meta data from key '#{key_to_read_from}'" if @verbose + @metadata_values = {} + Tempfile.open("key") do |file| + run_kdb ["export", key_to_read_from, "ni", file.path] + # reopen file + file.open + metadata_reached = false + file.each do |line| + line.chomp! + puts "read meta: '#{line}'" if @verbose + if line == "[]" + metadata_reached = true + next + end + next if metadata_reached == false + # end of metadata reached + break if line.empty? + + key_name, key_value = line.split(" = ") + key_name.strip! + + puts "use meta: '#{key_name}' => '#{key_value}'" if @verbose + @metadata_values[key_name] = key_value.to_s # ensure we have a string + end + file.close! + end + return @metadata_values + end + + def metadata + read_metadata_values unless @metadata_values.is_a? Hash + ret = @metadata_values.reject do |k,v| + # do not keep this key_name + delete = ( + # if it is an internal key (unless specified) + skip_this_metakey?(k, true) or + # or unless purge_meta_keys == true or k is specified + not( + @resource[:metadata].nil? or @resource.purge_meta_keys? or + @resource[:metadata].include? k + ) + ) + delete + end + puts "metadata is: #{ret}" if @verbose + ret + end + + def metadata=(value) + read_metadata_values unless @metadata_values.is_a? Hash + puts "having metadata: #{@metadata_values}" if @verbose + value.each do |k,v| + @metadata_values[k] = v + end + if @resource.purge_meta_keys? + @metadata_values.keep_if do |k,v| + keep = (@resource[:metadata].include?(k) or is_special_meta_key?(k)) + puts "keep this meta: '#{k}' #{keep}" if @verbose + keep + end + end + puts "updated metadata: #{@metadata_values}" if @verbose + end + + def comments + read_metadata_values unless @metadata_values.is_a? Hash + comments = {} + @metadata_values.each do |meta, value| + if /^comments?\/#(\d+)/ =~ meta + value = value[1..-1] if value[0] == '"' + value = value[0..-2] if value[-1] == '"' + comments[$1] = value.sub(/^#/, '') + end + end + # we get a hash, with + # #num => line + # so sort by #num, take the lines and join with newline + comments = comments.sort_by{|k,v| k}.map{|e|e[1]}.join "\n" + comments + end + + def comments=(value) + self.metadata unless @metadata_values.is_a? Hash + comment_lines = value.split "\n" + + # remove all comment meta keys + @metadata_values.delete_if { |k,v| k.start_with? "comment" } + + @metadata_values["comments"] = "##{comment_lines.size}" + comment_lines.each_with_index do |line, index| + @metadata_values[array_key_name "comments", index] = line + end + end + + def check + @spec_meta_values = read_metadata_values_from_key(get_spec_key_name) + specs = {} + @spec_meta_values.each do |k,v| + # we are interested in meta keys starging with 'check/' + if /^check\/(.*)$/ =~ k + check_name = $1 + # if it is an elektra Array, convert it to a Ruby array + # while preserve order + if /^(\w+)\/#(\d+)$/ =~ check_name + check_name, index = $1, $2.to_i + specs[check_name] = [] unless specs[check_name].is_a? Array + specs[check_name][index] = v + else + specs[check_name] = v + end + end + end + if specs.size == 1 and specs.values[0].to_s.empty? + specs = specs.keys[0] + end + puts "spec_keys: #{specs}" if @verbose + return specs + end + + def check=(value) + self.check unless @spec_meta_values.is_a? Hash + + spec_to_set = specified_checks_to_meta value + @spec_meta_values.merge! spec_to_set + @spec_meta_values.delete_if do |k,v| + (k.start_with? "check" and not spec_to_set.include? k) + end + Tempfile.open("speckey") do |file| + file.puts + file.puts " = " + file.puts + file.puts "[]" + @spec_meta_values.each do |k,v| + file.puts " #{k} = #{v}" + end + file.flush + begin + file.rewind + file.each do |line| + puts "import spec: #{line}" + end + end if @verbose + file.close + run_kdb ["import", get_spec_key_name, "ni", file.path] + file.unlink + end + end + + def flush + return unless @metadata_values.is_a? Hash + Tempfile.open("key") do |file| + file.puts + if not @resource[:value].is_a? Array + file.puts " = #{@resource[:value]}" + else + file.puts " = #{@resource[:value][0]}" + end + file.puts + file.puts "[]" + @metadata_values.each do |k,v| + file.puts " #{k} = #{v}" + end + file.flush + begin + file.rewind + file.each do |line| + puts "import: #{line}" + end + end if @verbose + file.close + run_kdb ["import", @resource[:name], "ni", file.path] + file.unlink + end + end + end +end diff --git a/lib/puppet/provider/kdbkey/ruby.rb b/lib/puppet/provider/kdbkey/ruby.rb new file mode 100644 index 0000000..26caa65 --- /dev/null +++ b/lib/puppet/provider/kdbkey/ruby.rb @@ -0,0 +1,441 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Ruby provider for type kdbkey for managing libelektra keys +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# +require 'etc' +require_relative 'common' + +module Puppet + Type.type(:kdbkey).provide :ruby, :parent => Puppet::Provider::KdbKeyCommon do + desc "kdb through libelektra Ruby API" + + # static class var for checking if we are able to use this provider + @@have_kdb = true + @@is_fake_ks = false + + has_feature :user + + begin + # load libelektra Ruby binding extension + require 'kdb' + rescue LoadError + @@have_kdb = false + end + + # make this provider always to be default (aslong it is useable + #defaultfor :kernel => :Linux + def self.default? + @@have_kdb + end + # if we can load the 'kdb' extension + confine :true => @@have_kdb + + # remember all opened kdb handles + # since there is not suitable way to a proper provider instance + # cleanup. + # The flush method is only called, if the underlying resource was + # modified. + # All opened handles will be closed on 'self.post_resource_eval' + # which is done once per provider class. + @@open_handles = [] + + # just used during testing to inject a mock + def use_fake_ks(ks) + @ks = ks + @is_fake_ks = true + end + + # allow access to internal key, used during testing + attr_reader :resource_key + + + def do_asuser(proc_obj) + unless @resource[:user].nil? + Puppet::Util::SUIDManager.asuser(@resource[:user]) do + + old_user = ENV['USER'] + old_home = ENV['HOME'] + old_xdg = ENV['XDG_CONFIG_HOME'] + + if @resource[:user] =~ /^\d+/ + # we got a numeric user argument try to convert to user name + begin + user = Etc.getpwuid(@resource[:user].to_i).name + rescue + user = @resource[:user] + end + else + user = @resource[:user] + end + + ENV['USER'] = user + begin + # if passwd entry for user does not exist, this will trigger an + # ArgumentError + ENV['HOME'] = Etc.getpwnam(user).dir + rescue + ENV['HOME'] = '' + end + + ENV['XDG_CONFIG_HOME'] = '' + + begin + Puppet.debug("do_asuser: euid: #{Process.euid} " + + "user: #{@resource[:user]} " + + "HOME: #{ENV['HOME']} " + + "USER: #{ENV['USER']} ") + proc_obj.call + rescue + ENV['USER'] = old_user + ENV['HOME'] = old_home + ENV['XDG_CONFIG_HOME'] = old_xdg + end + end + else + proc_obj.call + end + + end + + def create + @resource_key = Kdb::Key.new @resource[:name] + self.value= @resource[:value] unless @resource[:value].nil? + self.check= @resource[:check] unless @resource[:check].nil? + self.metadata= @resource[:metadata] unless @resource[:metadata].nil? + self.comments= @resource[:comments] unless @resource[:comments].nil? + @ks << @resource_key + end + + def destroy + @ks.delete @resource[:name] unless @resource_key.nil? + # check if there are array keys left + @ks.each do |x| + if x.name =~ /^#{@resource[:name]}\/#_*\d+$/ + @ks.delete x + end + end + end + + # is called first for each managed resource + # stores the queried key for later modifications + def exists? + Puppet.debug "kdbkey/ruby exists? #{@resource[:name]}" + + # this is the first method call for a managed resource + # so, here we have to do a kdb.open + # all opened kdb objects are used by later methods so keep them + # + # note: for the moment we do a kdb.open/get/set for EACH managed + # kdbkey resource separately. This results in an opened kdb handle + # and keySet for each manged key. This strategy is required, since + # we might have modified the underlying Elektra key space + # (a changed/added mountpoint after the actual kdb.open). + # + # It would be better if we could share our handles and keysets, but: + # - one shared handle and keyset is definitely too less, for the following reasons + # - actually there is no way to guarantee that all kdbmount modifications happen + # BEFORE the first kdb.open call + # - since we not really know here, which resource keys we have to manage, we end + # up with fetching the whole Elektra key space, which would be way too much + # - a better strategy would be to use one handle and keyset per mountpoint. But for + # the moment this is too complicated (e.g. how to proceed with cascading keys?) + # + open_proc = Proc.new do + @kdb_handle = Kdb.open + @@open_handles << @kdb_handle + @ks = Kdb::KeySet.new + @cascading_key = Kdb::Key.new @resource[:name].gsub(/^\w+\//, '/') + puts "do kdb.get ks, #{@cascading_key.name}" if @verbose + @kdb_handle.get @ks, @cascading_key + Puppet.debug "reading from config file '#{@cascading_key.value}'" + @ks.pretty_print if @verbose + end + + unless @is_fake_ks + do_asuser open_proc + end + + @resource_key = @ks.lookup @resource[:name] + puts "resource key nil? #{@resource_key.nil?}" if @verbose + return !@resource_key.nil? + end + + def value + return nil if @resource_key.nil? + return [@resource_key.value] if @ks.lookup("#{@resource_key.name}/#0").nil? + + # array value + value = [] + @ks.each do |x| + if x.name =~ /^#{@resource_key.name}\/#_*\d+$/ + value << x.value + end + end + value + end + + def value=(value) + if @resource_key.nil? + return + end + + remove_from_this_index = 0 + if not value.is_a? Array + @resource_key.value= value.to_s + + elsif value.size == 1 + @resource_key.value= value[0].to_s + + else + @resource_key.value= '' + value.each_with_index do |elem_value, index| + elem_key_name = array_key_name @resource_key.name, index + elem_key = @ks.lookup elem_key_name + if elem_key.nil? + elem_key = Kdb::Key.new elem_key_name + @ks << elem_key + end + elem_key.value= elem_value.to_s + end + remove_from_this_index = value.size + end + + # remove possible "old" array keys + i = remove_from_this_index + while not (key = @ks.lookup(array_key_name @resource_key.name, i)).nil? + i += 1 + @ks.delete key + end + end + + # get metadata values as Hash + # note: in order not to trigger an refresh cycle, we have to be careful which + # keys should be returned. If 'purge_meta_keys?' is not set, we have to remove + # the not-specified metakeys from the result set. + def metadata + #key.meta.to_h unless key.nil? ruby 1.9 does not have Enumerable.to_h :( + res = Hash.new + @resource_key.meta.each do |e| + next if skip_this_metakey? e.name, true + + # if purge_meta_keys is NOT set to true, remove all unspecified keys + # otherwise, Puppet will think we have to change something, so just + # keep those, which might have to be changed + unless @resource.purge_meta_keys? or @resource[:metadata].nil? + next unless @resource[:metadata].include? e.name + end + + res[e.name] = e.value + end unless @resource_key.nil? + + return res + end + + # set metadata values + # if 'purge_meta_keys?' == true, also remove all not specified keys but not + # too much (keeping internal ones) + def metadata=(value) + # update metadata + value.each { |k, v| + @resource_key.set_meta k, v + } unless @resource_key.nil? + + # do we have to purge all unspecified keys? + if @resource.purge_meta_keys? + @resource_key.meta.each do |metakey| + next if skip_this_metakey? metakey.name + + @resource_key.del_meta metakey.name unless value.include? metakey.name + end + end + end + + # currently Elektra plugins implement a not consistent way of specifying + # comments. So store the used metakey name to use the same one when writing the + # comments. see https://github.com/ElektraInitiative/libelektra/issues/1375 + @comments_key_name = "comments" + + # get key comments as one string + # merge the Elektra 'comments?/#' array + def comments + comments = "" + first = true # used for splitting lines + # search for all meta keys which names starts with 'comments/#' + # and concat its values line by line + @resource_key.meta.each do |e| + if /^(comments?)\/#_*\d+$/ =~ e.name + puts "update comments key name to #{$1}" if @verbose + @comments_key_name = $1 + comments << "\n" unless first + comments << e.value.sub(/^# ?/, '') + first = false + end + end + return comments + end + + # update comments + # + def comments=(value) + default_comment_start = '#' + # why do we have to init this inst var again??? + @comments_key_name ||= "comments" + # split specified comment into lines + comment_lines = value.split "\n" + # update all comment lines + comment_lines.each_with_index do |line, index| + puts "comments keyname: #{@comments_key_name}" if @verbose + # currently hosts plugin treats #0 comment as inline comment + if @resource_key.has_meta? "#{array_key_name @comments_key_name, index}/start" + comment_start = "" + else + comment_start = default_comment_start + end + @resource_key.set_meta array_key_name(@comments_key_name, index), "#{comment_start}#{line}" + if index == 0 + @resource_key.set_meta "#{array_key_name @comments_key_name, index}/space", "1" + end + end + + # iterate over all meta keys and remove all comment keys which + # represent a comment line, which does not exist any more + @resource_key.meta.each do |e| + if e.name.match(/^#{@comments_key_name}\/#_*(\d+)$/) + index = $1.to_i + if comment_lines[index].nil? + @resource_key.del_meta e.name + end + end + end + + # the (old) ini plugin comments strategy uses a 'comments' metakey + # to store the last comments array index. This has to be updated. + if comment_lines.size > 0 and @comments_key_name == "comments" + @resource_key.set_meta "comments", "##{comment_lines.size - 1}" + else + @resource_key.del_meta "comments" + end + @resource_key.pretty_print if @verbose + end + + # get all 'check/*' meta keys of the corresponding 'spec/' key + # + def check + spec_hash = {} + spec_key = @ks.lookup get_spec_key_name + unless spec_key.nil? + spec_key.meta.each do |m| + if /^check\/(.*)$/ =~ m.name + check_name = $1 + if /^(\w+)\/#_*\d+$/ =~ check_name + spec_hash[$1] = [] unless spec_hash[$1].is_a? Array + spec_hash[$1] << m.value + else + spec_hash[check_name] = m.value + end + end + end + end + # special case: if we get just one key and its value + # is "", return this as a string + if spec_hash.size == 1 and spec_hash.values[0] == "" + spec_hash = spec_hash.keys[0] + end + return spec_hash + end + + # update 'check/*' meta data on the corresponding 'spec/' key + # + def check=(value) + Puppet.debug "setting spec: #{value}" + spec_key = Kdb::Key.new get_spec_key_name + + if @ks.lookup(spec_key).nil? + @ks << spec_key + else + spec_key = @ks.lookup spec_key + end + + spec_to_set = specified_checks_to_meta value + + # set meta data on spec_key + spec_to_set.each do |spec_name, spec_value| + spec_key[spec_name] = spec_value + # also add the check meta data to resource_key directly, they will get + # removed by the 'spec' plugin (if the plugin placement bug is fixed ;) + # This is required, since the check is only evaluated if the key has the + # appropriate metadata attached. If the spec_key is created with the same + # keyset, the resources value will be set before the check can be performed + # so we might end up with an invalid value for the setting. + @resource_key[spec_name] = spec_value + end + + # remove all not specified meta keys from spec_key starting with 'check' + spec_key.meta.each do |e| + if e.name.start_with? "check" and !spec_to_set.include? e.name + spec_key.del_meta e.name + # perform same operation on resource_key + @resource_key.del_meta e.name + end + end + end + + # generate an error string from a Kdb::Key + def key_get_error_msg(key) + return nil unless key.is_a? Kdb::Key + + msg = "" + if key.has_meta? 'error' + msg += key['error/description'] + "\n" + msg += "Reason: #{key['error/reason']}\n" + msg += "Error number: ##{key['error/number']}\n" + msg += "Module: #{key['error/module']}\n" + msg += "Configfile: #{key['error/configfile']}\n" + msg += "Mountpoint: #{key['error/mountpoint']}\n" + end + return msg + end + + + # flush is call if a resource was modified + # thus this method is perfectly suitable for our db.set method which will + # finally bring the changes to disk + # also do a kdbclose for this handle + def flush + close_proc = Proc.new do + begin + Puppet.debug "kdbkey/ruby: flush #{@resource[:name]}" + @kdb_handle.set @ks, @cascading_key + rescue + # we only care about the error message here, warnings could be + # misleading, especially if they do not concern the key we are + # manipulating + raise Puppet::Error.new key_get_error_msg(@cascading_key) + ensure + @@open_handles.delete @kdb_handle + @kdb_handle.close + end + end + + unless @is_fake_ks + do_asuser close_proc + end + end + + # provider de-init hook + # this is our last chance to close remaining kdb handles + def self.post_resource_eval + Puppet.debug "kdbkey/ruby: closing kdb db" + @@open_handles.delete_if do |handle| + handle.close + true + end + end + + end +end diff --git a/lib/puppet/provider/kdbmount/kdb.rb b/lib/puppet/provider/kdbmount/kdb.rb new file mode 100644 index 0000000..27b5ed9 --- /dev/null +++ b/lib/puppet/provider/kdbmount/kdb.rb @@ -0,0 +1,147 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Kdb provider for type kdbmount for managing libelektra key database +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# + +module Puppet + Type.type(:kdbmount).provide :kdb do + desc "kdbmount through kdb command" + + commands :kdb => "kdb" + + mk_resource_methods + + def self.instances + mounts = [] + get_active_mountpoints.each do |hash| + if hash + mounts << new(hash) + end + end + return mounts + end + + def self.prefetch(defined_mountpoints) + #defined_mountpoints.each do |name, res| + # puts "defined mp: name: #{name}, file: #{res[:file]}" + #end + instances.each do |prov_inst| + if resource = defined_mountpoints[prov_inst.name] + resource.provider = prov_inst + end + end + end + + def create + #puts "kdb create" + cmd_args = ["mount"] + cmd_args << "-R" + cmd_args << @resource[:resolver] + cmd_args << "-W" if @resource[:add_recommended_plugins] + cmd_args << @resource[:file] + cmd_args << @resource[:name] # mountpoint + puts "plugins: #{@resource[:plugins]}" + if @resource[:plugins].is_a? Array + if @resource[:plugins][0].is_a? Hash + @resource[:plugins][0].each do |plugin, params| + puts "here, params: #{params.inspect}" + cmd_args << plugin + config_line = '' + params.each do |k, v| + config_line << "," unless config_line.empty? + config_line << "#{k}=#{v}" + end + cmd_args << config_line unless config_line.empty? + end + else + @resource[:plugins].each do |e| + # build plugin config cmdline argument + if e.is_a? Hash + config_line = '' + e.each do |k,v| + config_line << "," unless config_line.empty? + config_line << "#{k}=#{v}" + end + cmd_args << config_line + else + # plain plugin name + cmd_args << e + end + end + end + end + cmd_args.flatten! + puts "cmd_args: #{cmd_args.inspect}" + kdb(cmd_args) + end + + def destroy + kdb ["umount", @resource[:name]] + end + + #def exists? + # this is defined in Type as we use prefetch + #end + + #def file + # puts "getting file" + #end + + def file=(value) + # puts "setting file to #{value}" + # @property_hash[:file] = value + # changing file is simply done via recreation (for NOW) + destroy + create + end + + #def plugins=(value) + # puts "setting plugins #{value}" + #end + + #def flush + # puts "do flush of #{@resource[:name]}" + # puts @property_hash + # puts "plugins: #{@resource[:plugins]}" + #end + + private + + def self.get_active_mountpoints + mp = [] + lines = kdb(["mount"]).split "\n" + lines.each do |mount_line| + mp << parse_mount_line(mount_line) + end + return mp + end + + def self.parse_mount_line(mount_line) + hash = nil + + if /^(.+) on (.+) with name (.+)$/ =~ mount_line + (file, path, _name) = $~[1,3] + #puts "got: file: #{file}, path: #{path}, name: #{name}" + hash = {} + + hash[:provider] = self.name + # assuming path is the correct value for our 'name' var + hash[:name] = path + hash[:file] = file + hash[:ensure] = :present + else + raise Puppet::Error, "'kdb mount' invalid line: #{mount_line}" + end + + return hash + end + + + + end +end diff --git a/lib/puppet/provider/kdbmount/ruby.rb b/lib/puppet/provider/kdbmount/ruby.rb new file mode 100644 index 0000000..489ad93 --- /dev/null +++ b/lib/puppet/provider/kdbmount/ruby.rb @@ -0,0 +1,367 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Ruby provider for type kdbmount for managing libelektra key database +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# + +module Puppet + Type.type(:kdbmount).provide :ruby do + desc "kdbmount through libelektra Ruby API" + + @@have_kdb = true + + begin + require 'kdbtools' + rescue LoadError + @@have_kdb = false + end + + defaultfor :kernel => :Linux + confine :true => @@have_kdb + + # generate getter and setter + mk_resource_methods + + + # find all existing instances + # + def self.instances + mounts = [] + get_active_mountpoints.each do |hash| + if hash + mounts << new(hash) + end + end + return mounts + end + + + def self.prefetch(defined_mountpoints) + #defined_mountpoints.each do |name, res| + # Puppet.debug "defined mp: name: #{name}, file: #{res[:file]}" + #end + instances.each do |prov_inst| + if resource = defined_mountpoints[prov_inst.name] + resource.provider = prov_inst + end + end + end + + + def create + Puppet.debug "kdbmount:ruby: create #{@resource}" + perform_kdb_action Kdbtools::MOUNTPOINTS_PATH, + &method(:set_mount_backend_config) + end + + + def destroy + perform_kdb_action Kdbtools::MOUNTPOINTS_PATH, + &method(:set_unmount_backend_config) + end + + + def recreate + perform_kdb_action Kdbtools::MOUNTPOINTS_PATH, + &method(:reset_backend_config) + end + + + #def exists? + # this is defined in Type as we use prefetch + #end + + + #def file + # puts "getting file" + #end + + def file=(value) + begin + #Kdb.open do |kdb| + backend_root = Kdb::Key.new Kdbtools::MOUNTPOINTS_PATH + backend_root.add_basename @resource[:name] + + # mountconf = Kdb::KeySet.new + # kdb.get mountconf, backend_root + perform_kdb_action backend_root do |mountconf| + + if path_key.nil? + raise Puppet::Error, "path key not found in backend config" + end + + path_key.value = @resource[:file] + + # kdb.set mountconf, backend_root + end + rescue + Puppet.debug "could not set file within backend, fallback to recreate mountpoint" + # fallback, recreate mountpoint + recreate + end + end + + + def plugins=(value) + # this is pretty the same as building the backend and replace the current + # backend config with the new one + recreate + end + + def resolver=(value) + recreate + end + + #def flush + # puts "do flush of #{@resource[:name]}" + # puts @property_hash + # puts "plugins: #{@resource[:plugins]}" + #end + + + def resolve_plugins(plugins) + result = {} + plugins.each do |plugin| + backend = Kdbtools::MountBackendBuilder.new + backend.add_plugin Kdbtools::PluginSpec.new(plugin) + backend.resolve_needs @resource[:add_recommended_plugins] + backend.to_add.each do |ps| + result[plugin] ||= [] + result[plugin] << ps.name + result[plugin] << ps.refname if ps.name != ps.refname + end + end + return result + end + + + # convert the Puppet given :plugins value to a more suitable + # hash: + # pluginname => plugin config settings + # + # Puppet will give us an array of values, combining plugin names and + # config settings. e.g. + # ["ini", {"delimiter" => " ", "setting2" => "aa"}, "type"] + # + # e.g: + # ini => { + # delimiter => " " + # array => "" + # }, + # type => { } + # + def convert_plugin_settings(plugins) + config = {} + cur_plugin = nil + if plugins.is_a? Array and plugins.size == 1 and plugins[0].is_a? Hash + # if we have a single array element and this is a Hash, user has passed + # a Hash object to plugins property + config = plugins[0] + elsif plugins.is_a? Array + plugins.each do |e| + if e.is_a? String + cur_plugin = e + config[e] = {} + elsif e.is_a? Hash + config[cur_plugin] = e + else + raise Puppet::Error, "invalid plugins configuration given" + end + end + end + return config + end + + + private + + # get all active mountpoint + # + def self.get_active_mountpoints + mp = [] + Kdb.open do |kdb| + mountconf = Kdb::KeySet.new + kdb.get mountconf, Kdbtools::MOUNTPOINTS_PATH + + backends = Kdbtools::Backends.get_backend_info mountconf + + backends.each do |mount| + backend_key = Kdb::Key.new Kdbtools::MOUNTPOINTS_PATH + backend_key.add_basename mount.mountpoint + backend_ks = mountconf.cut backend_key + + hash = {} + hash[:provider] = self.name + hash[:name] = mount.mountpoint + hash[:file] = mount.path + hash[:ensure] = :present + hash[:plugins] = get_mountoint_plugin_config backend_ks + mp << hash + end + end + Puppet.debug mp + return mp + end + + + # get configured plugins with their config settings from a + # given backend + # + def self.get_mountoint_plugin_config(backend) + plugins = {} + backend.each do |key| + # we only search for the plugin keys + if /\/(error|get|set)plugins\// =~ key.fullname + # parse the plugin key name + if /^#([0-9]+)#(\w+)(#(\w+)#)?$/ =~ key.basename + plugin_name = $2 + #ref_number = $1 # unused + ref_name = $4 + # skip resolver plugins + next if ref_name == "resolver" + next if plugin_name == "resolver" + next if plugin_name == "sync" # TODO is it save to ignore sync + + #puts "matching plugin: #{$2}, num: #{$1}, refname: #{$4}" + + plugins[plugin_name] = {} unless plugins.include? plugin_name + + # check for config keys + config_ks = backend.cut Kdb::Key.new key.name + "/config" + config_ks.each do |config_key| + # ignore the first dir key + next if config_key == Kdb::Key.new(key.name + "/config") + plugins[plugin_name][config_key.basename] = config_key.value + end + end + end + end + # convert the Hash to an array Puppet gives us + plugins = plugins.to_a.flatten.reject {|e| e.empty? } + #puts "#{plugins}" + plugins + end + + + # helper function to modify Elektra key database + # helps to avoid multiple Kdb.open/close sequences + # + def perform_kdb_action path, &block + Kdb.open do |kdb| + mountconf = Kdb::KeySet.new + kdb.get mountconf, path + + yield mountconf + + # write new mount config + kdb.set mountconf, path + end + end + + + # create a new mount point and add it to the existing + # mount config (fetched from system/elektra/mountpoints + # use with perform_kdb_action + # + def set_mount_backend_config(mountconf) + + backend = Kdbtools::MountBackendBuilder.new + + # mountpoint + mpk = Kdb::Key.new @resource[:name] + unless mpk.is_valid? + raise Puppet::Error, "invalid mountpoint: #{@resource[:name]}" + end + + # add new mount point, checks for mountpoint validity and + # already existing mountpoint + backend.set_mountpoint mpk, mountconf + + # add the resolver plugin + backend.add_plugin Kdbtools::PluginSpec.new @resource[:resolver] + + backend.use_config_file @resource[:file] + + backend.need_plugin "storage" + + plugins = convert_plugin_settings(@resource[:plugins]) + plugins_to_mount = {} + # for each plugin get all dependent (and if req. recommended) plugins + resolved_plugins = resolve_plugins plugins.keys + + plugins.each do |name, config| + # if user has specified a plugin configuration, we have to use this + # plugin for mounting + unless config.empty? + plugins_to_mount[name] = config + end + + # check if this plugin would be added by any other plugin through + # dependency or recommends lists. + # If so do not explicetly for mounting, since this might lead to + # ordering or placement errors + use_plugin = true + resolved_plugins.each do |other, depends| + next if other == name + if depends.include? name + use_plugin = false + end + end + + if use_plugin + plugins_to_mount[name] = config + end + end + + all_used_plugins = resolved_plugins.values.flatten.uniq.sort + if plugins.keys.sort != all_used_plugins + Puppet.notice "#{@resource}: using additional plugins: #{(all_used_plugins - plugins.keys.sort)}" + end + + #puts "should plugins: #{plugins}" + #puts "actually used: #{plugins_to_mount}" + + # add user requested plugins + plugins_to_mount.each do |p_name, p_config| + ps = Kdbtools::PluginSpec.new p_name + p_config.each do |k, v| + ps.append_config Kdb::KeySet.new Kdb::Key.new("user/#{k}", value: v) + end + backend.add_plugin ps + end + + # resolv all required plugins (without recommended (false)) + backend.resolve_needs @resource[:add_recommended_plugins] + + begin + # add new backend to mount config + backend.serialize mountconf + rescue + raise Puppet::Error, "unable to create mountpoint; #{$!}" + end + end + + + # remove existing mount point from existing mount config + # use with perform_kdb_action + # + def set_unmount_backend_config(mountconf) + Kdbtools::Backends.umount @resource[:name], mountconf + end + + + # recreate mount config + # use with perform_kdb_action + # + def reset_backend_config(mountconf) + set_unmount_backend_config mountconf + set_mount_backend_config mountconf + end + + + end +end diff --git a/lib/puppet/type/kdbkey.rb b/lib/puppet/type/kdbkey.rb new file mode 100644 index 0000000..22561ea --- /dev/null +++ b/lib/puppet/type/kdbkey.rb @@ -0,0 +1,398 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Custom puppet type kdbkey for managing libelektra keys +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# +require 'puppet/parameter/boolean' + +Puppet::Type.newtype(:kdbkey) do + @doc = <<-EOT + Manage libelekra keys. + + This resource type allows to define and manipulate keys of libelektra's + key database. + EOT + + feature :user, "ability to define/modify keys in the context of a specific user" + + + ensurable + + # prefix parameter + # + # Note: this has to be defined BEFORE the name parameter, since we reference + # this prefix parameter within the names 'muge' and 'validate' methods + newparam(:prefix) do + desc <<-EOT + Prefix for the key name (optional) + + If given, this value will prefix the given libelektra key name. + e.g.: + + kdbkey { 'puppet/x1': + prefix => 'system/test', + value => 'hello' + } + + This will manage the key 'system/test/puppet/x1'. + + Prefix and name are joined with a '/', if prefix does not end with '/' + or name does not start with '/'. + + Both, name and prefix parameter are used to uniquely identify a + libelektra key. + EOT + + isnamevar + + defaultto "" + + validate do |name| + unless name.nil? or name.empty? + unless name =~ /^(\/|spec|proc|dir|user|system)/ + raise ArgumentError, "'%s' is not a valid basename" % name + end + end + end + end + + + # name parameter + newparam(:name) do + desc <<-EOT + The fully qualified name of the key + + Elektra manages its keys within several namespaces ('system', 'user', + 'dir'... see https://www.libelektra.org/manpages/elektra-namespaces + for more details.) + + Cascading key names (keys starting with a '/') are probably not optimal + here, as they are implicitly converted to a key name with the 'dir', + 'user' or 'system' namespace. + EOT + + # add the prefix, if given + munge do |value| + if resource[:prefix].nil? + value + else + fullname = resource[:prefix] + fullname += "/" unless fullname[-1] == "/" or value[0] == "/" + fullname += value + fullname.gsub! "//", "/" + @resource.title = fullname + end + end + + # if no prefix is given, we have to validate the key name + validate do |name| + if resource[:prefix].nil? or resource[:prefix].empty? + unless name =~ /^(spec|proc|dir|user|system)?\/.+/ + raise ArgumentError, "'%s' is not a valid libelektra key name" % name + end + end + end + + isnamevar + end + + # this is required, since we've defined to parameter as 'namevar' + # it's used to assign the name from the resource title, whereas the default + # implementation will raise an error if two name vars are given + # (see type.rb for details) + def self.title_patterns + [ [ /(.*)/m, [ [:name] ] ] ] + end + + + newproperty(:value, :array_matching => :all) do + desc <<-EOT + Desired value of the key. This can be any type, however elektra currently + just manages to store String values only. Therefore all types are + implicitly converted to Strings. + + If value is an array, the key is managed as an Elektra array. Therefore + a subkey named `*name*/#` will be created for each array element. + EOT + + def change_to_s(current_value, new_value) + def single_elem_as_string(v) + return "" if v.nil? + if v.is_a? Array and v.size == 1 + return v[0].to_s + else + return v.to_s + end + end + + "value changed '#{single_elem_as_string current_value}' to '#{single_elem_as_string new_value}'" + end + end + + newproperty(:metadata) do + desc <<-EOT + Metadata for this key supplied as Hash of key-value pairs. The concret + behaviour is defined by the parameter `purge_meta_keys`. The default + case (`purge_meta_keys` => false) is to manage the specified metadata + keys only. Already present but not specified metadata keys will not be + removed. If `purge_meta_keys` is set to true, already present but not + specified metadata keys will be removed. + + Examples: + kdbkey { 'system/sw/app/s1': + metadata => { + 'owner' => 'me', + 'other meta' => 'you' + } + } + EOT + + validate do |metadata| + if !metadata.is_a? Hash + raise ArgumentError, "Hash required" + else + super metadata + end + end + end + + newparam(:purge_meta_keys, + :boolean => true, + :parent => Puppet::Parameter::Boolean) do + desc <<-EOT + manage complete set of metadata keys + + If set to true, kdbkey will remove all unspecifed metadata keys, ensuring + only the specified set of metadata keys will exist. Otherwise, + unspecified metadata keys will not be touched. + EOT + end + + newproperty(:comments) do + desc <<-EOT + comments for this key + + Comments form a critical part of documentation. May configuration file + formats support adding comment lines. Libelektra plugins parse comments + and add them as metadata keys to the corresponding keys. This attribute + allows to manage those comment lines. + + Multi-line comments (those including a newline character) are implicitly + converted to a multi-line comment. + EOT + + def change_to_s(current_value, new_value) + # limit max string length + current_value = "#{current_value[0,20]}..." if current_value.size > 24 + new_value = "#{new_value[0,20]}..." if new_value.size > 24 + # replace new lines with $ + current_value.gsub! "\n", '$ ' + new_value.gsub! "\n", '$ ' + + if current_value.empty? + return "comments defined to '#{new_value}'" + elsif new_value.empty? + return "comments removed" + else + return "comments changed '#{current_value}' to '#{new_value}'" + end + end + end + + newproperty(:check) do + desc <<-EOT + Add value validation. + + This property allows to define certain restrictions to be applied on the + key value, which are automatically checked on each key database write. These + validation checks are performed by Elektra itself, so modifications done + by other applications will be also restricted to the defined value + specifications. + + The value for this property can be either a single String or a Hash + of settings. + e.g. path plugin + the 'path' plugin does not require any additional settings + so it is enough to just pass 'path' as 'check' value. + + kdbkey { 'system/sw/myapp/setting1': + check => 'path', + value => '/some/absolute/path/will/pass' + } + + Note: this does not check if the path really exists (instead it just + issues a warning). The check will fail, if the given value is not an + absolute path. + + e.g. network plugin + + The network plugin checks if the supplied value is valid IP address. + + kdbkey { 'system/sw/myapp/myip': + check => 'ipaddr', + value => ${given_myip} + } + + to check for valid IPv4 addresses use + + kdbkey { 'system/sw/myapp/myip': + check => { 'ipaddr' => 'ipv4' }, # works with 'ipv6' too + value => ${given_myip} + } + + e.g. type plugin + + The type plugin checks if the supplied key value conforms to a defined + data type (e.g. numeric value). Additionally, it is able to check if + the supplied key value is within an allowed range. + + kdbkey { 'system/sw/myapp/port': + check => { 'type' => 'unsigned_long' }, + value => ${given_port} + } + + kdbkey { 'system/sw/myapp/num_instances': + check => { + 'type' => 'short', + 'type/min' => 1, + 'type/max' => 20 + }, + value => ${given_num_instance} + } + + e.g. enum plugin + + The enum plugin check it the supplied value is within a predefined set + of values. Two different formats are possible: + + kdbkey { 'system/sw/myapp/scheduler': + check => { 'enum' => "'ondemand', 'performance', 'energy saving'" }, + value => ${given_scheduler} + } + + kdbkey { 'system/sw/myapp/notification': + check => { 'enum' => ['off', 'email', 'slack', 'irc'] }, + value => ${given_notification} + } + + e.g. validation plugin + + The validation plugin checks if the supplied value matches a predefined + regular expression: + + kdbkey { 'system/sw/myapp/email': + check => { + 'validation' => '^[a-z0-9\._]+@mycompany.com$' + 'validation/message' => 'we require an internal email address here', + 'validation/ignorecase' => '', # existence of flag is enough + } + ... + } + + + For further check plugins see the Elektra documentation. + + Note: for each 'check/xxx' metadata, required by the Elektra plugins, just + remove the 'check/' part and add it to the 'check' property here. + (e.g. validation plugin: 'check/validation' => 'validation' ...) + EOT + + validate do |value| + # setting specifications for spec/ keys does not make any sense + # so we do not allow it + if @resource[:name].start_with? "spec/" + raise ArgumentError, "setting specifications on a 'spec' key "\ + "is not allowed and does not make sense" + end + unless value.is_a? Hash or value.is_a? String + raise ArgumentError, "Hash required" + else + super value + end + end + end + + # param user + # + # This is currently only supported by Provider 'kdb'. + # However, it seams the 'feature' stuff is evaluated only for once for all + # instances, so we can not really say, use provider 'kdb' for those with + # 'user' set and provider 'ruby' for all other instances. This is not working + # or at least for me it was not working. So we do it manually. + newparam(:user) do #, :required_features => ["user"]) do + desc <<-EOT + define/modify key in the context of given user. + + This is only relevant, if key name referes to a user context, thus is + either cascading (starting with a '/') or is within the 'user' + namespace (starting with 'user/'). + EOT + + end + + autorequire(:kdbmount) do + get_autorequire_path_names true + end + + autorequire(:kdbkey) do + get_autorequire_path_names false + end + + def get_autorequire_path_names(include_self) + if self[:name].is_a? String + + # split name into path elements, so token separated by '/' not including + # escaped '/' occurrences + # Thus, when we detect a '\\' at the end of a token, we do not want + # to split this up + names = self[:name].split '/' + remember = nil + names.collect! do |token| + next unless token.is_a? String + next if token.empty? + if token[-1] == '\\' + remember ||= "" + remember << "/" unless remember.empty? + remember << token + next + elsif !remember.nil? + ret = remember << "/" + token + remember = nil + ret + else + token + end + end + # the previous escaped / token joining returns nils, so remove them + names.compact! + + # generate an array where each element is joined (by a '/') with its + # previous elements + req_resources = [names.shift] + names.each do |n| + req_resources << req_resources.last + "/" + n + end + + # if include_self == false remove the last entry (equals :name) + req_resources.delete self[:name] unless include_self + + # if we have a cascading key, we could access any possible Elektra + # namespace, thus we autorequire all of them + if self[:name][0] == '/' + ns_res = [] + ["system", "user", "spec", "dir"].each do |ns| + req_resources.each do |name| + ns_res << ns + '/' + name + end + end + req_resources = ns_res + end + req_resources + end + end + +end diff --git a/lib/puppet/type/kdbmount.rb b/lib/puppet/type/kdbmount.rb new file mode 100644 index 0000000..97e3179 --- /dev/null +++ b/lib/puppet/type/kdbmount.rb @@ -0,0 +1,205 @@ +# encoding: UTF-8 +## +# @file +# +# @brief Custom puppet type kdbkey for managing libelektra keys +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# +# +require 'puppet/parameter/boolean' + + +Puppet::Type.newtype(:kdbmount) do + @doc = <<-EOT + Manage libelekra global key-space. + + This resource type allows to define and manipulate libelektra's global key + database. Libelektra allows to 'mount' external configuration files into + its key database. A specific libelektra backend plugin is for reading and + writing the configuration file. + ... + EOT + + RECOMMENDED_PLUGINS = ["sync"] + + + ensurable + + + newparam(:name) do + desc <<-EOT + The fully qualified mount path within the libelektra key database. + + TODO: describe if it is safe or not to use cascading keys? + EOT + + validate do |name| + # TODO: which namespaces are safe to use? + #unless name =~ /^(spec|proc|dir|user|system)?\/.+/ + unless name =~ /^(spec|dir|user|system)?\/.+/ + raise ArgumentError, "%s is not a valid libelektra key name" % name + end + end + + isnamevar + end + + + newproperty(:file) do + desc <<-EOT + The configuration file to mount into the Elektra key database. + EOT + # TODO: do we have any restrictions on this? + end + + + #newproperty(:resolver) do + newparam(:resolver) do + desc <<-EOT + The resolver plugin to use for mounting. + Default: 'resolver' + EOT + + defaultto "resolver" + + validate do |value| + unless @resource.class.plugin_name_is_valid? value + raise ArgumentError, "'%s' is not a valid plugin name" % value + end + end + end + + + newparam(:add_recommended_plugins, + :boolean => true, + :parent => Puppet::Parameter::Boolean) do + desc <<-EOT + If set to true, Elektra will add recommended plugins to the mounted + backend configuration. + Recommended plugins are: #{RECOMMENDED_PLUGINS.join ', '} + Default: true + EOT + defaultto :true + end + + + # for now we do not support changing plugins and there settings + # so we use a param for this NOW + newproperty(:plugins, :array_matching => :all) do + #newparam(:plugins) do + desc <<-EOT + A list of libelektra plugins with optional configuration settings + use for mounting. + + The following value formats are acceped: + - a string value describing a single plugin name + - an array of string values each defining a single plugin + - a hash of plugin names with corresponding configuration settings + e.g. + [ 'ini' => { + 'delimiter' => " " + 'array' => '' + }, + 'type' + ] + + EOT + + validate do |value| + if value.is_a? String + unless @resource.class.plugin_name_is_valid? value + raise ArgumentError, "'%s' is not a valid plugin name" % value + end + end + end + # this can't be done here, since we get each value at once for + # munge, thus one munge call for each array entry. + #munge do |plugin| + #end + + # customized insync? method to handle more complex cases. + # a plugin can have dependencies and can recommend other plugins, therefore + # during mounting a plugin, Elektra might add additional plugins. So the + # is and should in two subsequent runs might differ. + # This method checks, + # - if we have to add a newly specified plugin (not found in the current + # mounted plugin list) + # - if we really have to remove a plugin + # - if plugin config settings have changed + def insync?(is) + #puts "insync? is: #{is}, should #{should}" + return false unless provider.respond_to? :resolve_plugins + + # convert to plugins-config Hash + my_is = provider.convert_plugin_settings is + my_should = provider.convert_plugin_settings should + + + # fist, check if all :should plugins are in :is plugins array + # so, is there a plugin missing? + return false if my_should.keys.any? { |p| not my_is.include? p } + + # pass the :should plugins list to libelektra to get a list plugins that + # will be used when mounting is done with these + # (honores :add_recommended_plugins parameter) + resolved = provider.resolve_plugins my_should.keys + will_use_plugins = resolved.values.flatten.uniq + + #puts "resolved: #{resolved}" + #puts "will_use_plugins: #{will_use_plugins}" + + # now, check if plugins should be removed + # if we have mounted a plugin, which is not in the list of plugins which + # will be used when mounting with the :should plugins, we have to remove + # it + my_is.keys.each do |is_plugin| + return false unless will_use_plugins.include? is_plugin + end + + # now do the reverse order, check if all will use are actually used + # (this is possible if someone switches :add_recommended_plugins + # from false to true) + will_use_plugins.each do |p| + return false unless my_is.include? p + end + + # finally, check if some plugin configuration has changed + my_should.each do |plugin, config| + return false unless my_is.include? plugin + return false unless my_is[plugin] == config + end + + true + end + + # TODO: add nice formating messages when changing plugins + #def change_to_s(cur_value, new_value) + # return "changed: will_use_plugins: #{@will_use_plugins}" + #end + + end + + + def exists? + #puts "type kdbmount exists? #{self[:name]}" + @provider.get(:ensure) != :absent + end + + def self.plugin_name_is_valid?(name) + /^\w+$/ =~ name + end + + validate do + # make :file and :plugins properties mandatory if one of them are used + if @parameters.include?(:plugins) + self.fail("file property missing") unless @parameters.include?(:file) + end + + if @parameters.include?(:file) + self.fail("plugins property missing") unless @parameters.include?(:plugins) + end + end + + +end diff --git a/manifests/init.pp b/manifests/init.pp new file mode 100644 index 0000000..200bc48 --- /dev/null +++ b/manifests/init.pp @@ -0,0 +1,48 @@ +# Class: libelektra +# =========================== +# +# Full description of class libelektra here. +# +# Parameters +# ---------- +# +# Document parameters here. +# +# * `sample parameter` +# Explanation of what this parameter affects and what it defaults to. +# e.g. "Specify one or more upstream ntp servers as an array." +# +# Variables +# ---------- +# +# Here you should define a list of variables that this module would require. +# +# * `sample variable` +# Explanation of how this variable affects the function of this class and if +# it has a default. e.g. "The parameter enc_ntp_servers must be set by the +# External Node Classifier as a comma separated list of hostnames." (Note, +# global variables should be avoided in favor of class parameters as +# of Puppet 2.6.) +# +# Examples +# -------- +# +# @example +# class { 'libelektra': +# servers => [ 'pool.ntp.org', 'ntp.local.company.com' ], +# } +# +# Authors +# ------- +# +# Author Name +# +# Copyright +# --------- +# +# Copyright 2016 Your name here, unless otherwise noted. +# +class libelektra { + + +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..4c7c9e7 --- /dev/null +++ b/metadata.json @@ -0,0 +1,15 @@ +{ + "name": "libelektra-libelektra", + "version": "0.8.1", + "author": "Bernhard Denner", + "summary": "manage your configuration through libelektra", + "license": "BSD License", + "source": "github", + "project_page": "github doc", + "issues_url": "github issues", + "dependencies": [ + {"name":"puppetlabs-stdlib","version_requirement":">= 1.0.0"} + ], + "data_provider": null +} + diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb new file mode 100644 index 0000000..ace36ed --- /dev/null +++ b/spec/classes/init_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' +describe 'libelektra' do + context 'with default values for all parameters' do + it { should contain_class('libelektra') } + end +end diff --git a/spec/fixtures/manifests/site.pp b/spec/fixtures/manifests/site.pp new file mode 100644 index 0000000..e69de29 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2d0dc05 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,5 @@ +require 'puppetlabs_spec_helper/module_spec_helper' + +RSpec.configure do |config| + config.mock_framework = :rspec +end diff --git a/spec/unit/provider/kdbkey_common_spec.rb b/spec/unit/provider/kdbkey_common_spec.rb new file mode 100644 index 0000000..83ce8d2 --- /dev/null +++ b/spec/unit/provider/kdbkey_common_spec.rb @@ -0,0 +1,64 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' +require_relative 'key_helper.rb' + +require 'puppet/provider/kdbkey/common.rb' + +describe Puppet::Provider::KdbKeyCommon do + subject { described_class.new } + + RSpec.shared_examples "key => spec-key" do |keyname, expected| + it "for key '#{keyname}'" do + subject.resource = create_resource :name => keyname + expect( + subject.get_spec_key_name + ).to eq expected + end + end + + context "should get corresponding spec-key from key name" do + include_examples "key => spec-key", "system/x1/x2", "spec/x1/x2" + include_examples "key => spec-key", "user/x1/x2", "spec/x1/x2" + include_examples "key => spec-key", "/x1/x2", "spec/x1/x2" + include_examples "key => spec-key", "dir/x1/x2", "spec/x1/x2" + include_examples "key => spec-key", "system/x1", "spec/x1" + include_examples "key => spec-key", "system/\\some\\/escaped/x1", + "spec/\\some\\/escaped/x1" + end + + + + RSpec.shared_examples "array element index" do |index, expected| + it "for index element #{index}" do + expect(subject.array_key_name keyname, index).to eq "#{keyname}/#{expected}" + end + end + + context "should get correct elektra array key names" do + let(:keyname) { "system/sw/test" } + + include_examples "array element index", 0, "#0" + include_examples "array element index", 1, "#1" + include_examples "array element index", 9, "#9" + include_examples "array element index", 10, "#_10" + include_examples "array element index", 11, "#_11" + include_examples "array element index", 19, "#_19" + include_examples "array element index", 20, "#_20" + include_examples "array element index", 90, "#_90" + include_examples "array element index", 100, "#__100" + include_examples "array element index", 286, "#__286" + include_examples "array element index", 1000, "#___1000" + include_examples "array element index", 10000, "#____10000" + include_examples "array element index", 100000, "#_____100000" + end + + +end diff --git a/spec/unit/provider/kdbkey_kdb_spec.rb b/spec/unit/provider/kdbkey_kdb_spec.rb new file mode 100644 index 0000000..6d500b5 --- /dev/null +++ b/spec/unit/provider/kdbkey_kdb_spec.rb @@ -0,0 +1,26 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' +require_relative 'key_helper.rb' +require_relative 'key_kdb_helper.rb' +require 'kdb' + + +describe Puppet::Type.type(:kdbkey).provider(:kdb) do + + let(:h) { KdbKeyProviderHelperKDB.new 'user/test/puppet-rspec/' } + let(:provider) { described_class.new } + let(:keyname) { "#{h.test_prefix}x1" } + before :example do + provider.resource = create_resource :name => keyname + end + + it_behaves_like "a kdbkey provider", true +end diff --git a/spec/unit/provider/kdbkey_ruby_spec.rb b/spec/unit/provider/kdbkey_ruby_spec.rb new file mode 100644 index 0000000..b098b40 --- /dev/null +++ b/spec/unit/provider/kdbkey_ruby_spec.rb @@ -0,0 +1,30 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' +require_relative 'key_helper.rb' +require_relative 'key_ruby_helper.rb' +require 'kdb' + + +describe Puppet::Type.type(:kdbkey).provider(:ruby) do + + let(:h) { KdbKeyProviderHelper.new 'user/test/puppet-rspec/'} + let(:keyname) { "#{h.test_prefix}x1" } + #let(:ks) { Kdb::KeySet.new } + let(:provider) { described_class.new } + + before :example do + provider.use_fake_ks h.ks + provider.resource = create_resource :name => keyname + end + + it_behaves_like "a kdbkey provider" + +end diff --git a/spec/unit/provider/kdbmount_kdb_spec.rb b/spec/unit/provider/kdbmount_kdb_spec.rb new file mode 100644 index 0000000..190fc6a --- /dev/null +++ b/spec/unit/provider/kdbmount_kdb_spec.rb @@ -0,0 +1,18 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' +require 'kdb' + + + + +describe Puppet::Type.type(:kdbmount).provider(:kdb) do + +end diff --git a/spec/unit/provider/key_helper.rb b/spec/unit/provider/key_helper.rb new file mode 100644 index 0000000..9d0156b --- /dev/null +++ b/spec/unit/provider/key_helper.rb @@ -0,0 +1,696 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +# currently metadaa key for comments used by ini +# but hosts uses 'comment' +COMMENT = 'comments' + + +def create_resource(params) + Puppet::Type.type(:kdbkey).new(params) +end + +# since both provider should do the same thing we can use the +# same spec/tests for both of them +# +# but not each test case is possible for the :kdb provider, +# thus use not_testable_with_kdb = true when executed with :kdb +# +RSpec.shared_examples "a kdbkey provider" do |not_testable_with_kdb| + + it "should be a child of Puppet::Provider" do + expect(described_class.new).to be_a_kind_of(Puppet::Provider) + end + + context "should check if resource exists" do + it "should return false on exists? if resource does not exist'" do + h.ensure_key_is_missing keyname + expect(provider.exists?).to eq(false) + end + + it "should return true on exists? if resource exists'" do + h.ensure_key_exists keyname + expect(provider.exists?).to eq(true) + end + + end + + context "should create key" do + before :example do + h.ensure_key_is_missing keyname + h.ensure_key_is_missing provider.get_spec_key_name + end + + it "with defined name" do + provider.create + provider.flush + expect(h.check_key_exists keyname).to eq true + end + + it "with defined name and value" do + value = "my value" + provider.resource = create_resource :name => keyname, :value => value + + provider.create + provider.flush + + expect(h.check_key_exists keyname).to eq true + expect(h.key_get_value keyname).to eq value + end + + it "with defined name, value and metadata" do + value = "my value" + meta = {'meta1' => 'v1', 'meta2' => 'v2' } + provider.resource = create_resource :name => keyname, + :value => value, + :metadata => meta + + provider.create + provider.flush + + expect(h.check_key_exists keyname).to eq true + expect(h.key_get_value keyname).to eq value + meta.each do |k, v| + expect(h.key_get_meta keyname, k).to eq v + end + end + + it "with defined name, value, metadata and comments" do + value = "my value" + meta = {'meta1' => 'v1', 'meta2' => 'v2' } + comments = "my comment" + + provider.resource = create_resource :name => keyname, + :value => value, + :metadata => meta, + :comments => comments + + provider.create + provider.flush + + expect(h.check_key_exists keyname).to eq true + expect(h.key_get_value keyname).to eq value + meta.each do |k, v| + expect(h.key_get_meta keyname, k).to eq(v) + end + expect(h.key_get_comment keyname).to eq comments + end + + it "with defined name, value, meta, comments and spec" do + value = "5" + meta = {"nvmcs" => "some value"} + comments = "other comments" + checks = {'type' => 'short'} + + provider.resource = create_resource :name => keyname, + :value => value, + :metadata => meta, + :comments => comments, + :check => checks + + provider.create + provider.flush + + expect(h.check_key_exists keyname).to eq true + expect(h.key_get_value keyname).to eq value + meta.each do |k, v| + expect(h.key_get_meta keyname, k).to eq(v) + end + expect(h.key_get_comment keyname).to eq comments + expect(h.check_key_exists provider.get_spec_key_name).to eq true + expect( + h.key_get_meta provider.get_spec_key_name, 'check/type' + ).to eq 'short' + end + + + end + + context "should remove key on destroy" do + it "for single value keys" do + h.ensure_key_exists keyname + # we have to call exists? first + provider.exists? + provider.destroy + provider.flush + + expect(h.check_key_exists keyname).to eq false + end + + it "for array value keys" do + h.ensure_key_exists keyname, '' + h.ensure_key_exists "#{keyname}/#0" 'one' + h.ensure_key_exists "#{keyname}/#1" 'one' + + provider.exists? + provider.destroy + provider.flush + + expect(h.check_key_exists keyname).to eq false + expect(h.check_key_exists "#{keyname}/#0").to eq false + expect(h.check_key_exists "#{keyname}/#1").to eq false + end + end + + context "with existing key" do + before :example do + h.ensure_key_exists keyname, "test" + provider.exists? + end + + context "should read the key's value" do + it "for a single string value" do + expect(provider.value).to eq ["test"] + end + + it "for an Array of strings" do + h.ensure_key_exists keyname, '' + h.ensure_key_exists "#{keyname}/#0", 'one' + h.ensure_key_exists "#{keyname}/#1", 'two' + h.ensure_key_exists "#{keyname}/#2", 'three' + + expect(provider.value).to eq ['one', 'two', 'three'] + end + end + + context "should update the key value" do + it "to an arbitrary string" do + expect(h.key_get_value keyname).to eq "test" + provider.value= ["some string value"] + provider.flush + expect(h.key_get_value keyname).to eq "some string value" + end + + it "to an empty string" do + expect(h.key_get_value keyname).to eq "test" + provider.value= [""] + provider.flush + expect(h.key_get_value keyname).to eq "" + end + + it "to an truth value" do + provider.value= [true] + provider.flush + expect(h.key_get_value keyname).to eq "true" + end + + it "to a numerical value" do + provider.value= [5] + provider.flush + expect(h.key_get_value keyname).to eq "5" + end + + it "to an array of strings" do + expect(h.key_get_value keyname).to eq "test" + provider.value= ['one', 'two'] + provider.flush + expect(h.key_get_value keyname).to eq '' + expect(h.key_get_value "#{keyname}/#0").to eq 'one' + expect(h.key_get_value "#{keyname}/#1").to eq 'two' + end + + it "to an array of different types" do + provider.value= ["string", 3, true, 5.5] + provider.flush + expect(h.key_get_value keyname).to eq '' + expect(h.key_get_value "#{keyname}/#0").to eq 'string' + expect(h.key_get_value "#{keyname}/#1").to eq '3' + expect(h.key_get_value "#{keyname}/#2").to eq 'true' + expect(h.key_get_value "#{keyname}/#3").to eq '5.5' + end + + it "to an array while removing old array values" do + h.ensure_key_exists keyname, '' + h.ensure_key_exists "#{keyname}/#0", '1' + h.ensure_key_exists "#{keyname}/#1", '2' + h.ensure_key_exists "#{keyname}/#2", '3' + h.ensure_key_exists "#{keyname}/#3", '4' + h.ensure_key_exists "#{keyname}/#4", '5' + + provider.value= ['one', 'two'] + provider.flush + + expect(h.key_get_value keyname).to eq '' + expect(h.key_get_value "#{keyname}/#0").to eq 'one' + expect(h.key_get_value "#{keyname}/#1").to eq 'two' + expect(h.check_key_exists "#{keyname}/#2").to eq false + expect(h.check_key_exists "#{keyname}/#3").to eq false + expect(h.check_key_exists "#{keyname}/#4").to eq false + end + + it "to an string value while removing old array values" do + h.ensure_key_exists keyname, '' + h.ensure_key_exists "#{keyname}/#0", '1' + h.ensure_key_exists "#{keyname}/#1", '2' + h.ensure_key_exists "#{keyname}/#2", '3' + h.ensure_key_exists "#{keyname}/#3", '4' + h.ensure_key_exists "#{keyname}/#4", '5' + + provider.value= "my new string value" + provider.flush + + expect(h.key_get_value keyname).to eq "my new string value" + expect(h.check_key_exists "#{keyname}/#0").to eq false + expect(h.check_key_exists "#{keyname}/#1").to eq false + expect(h.check_key_exists "#{keyname}/#2").to eq false + expect(h.check_key_exists "#{keyname}/#3").to eq false + expect(h.check_key_exists "#{keyname}/#4").to eq false + end + end + + context "and existing metadata" do + let(:metadata) { {"m1" => "v1", "m2" => "v2"} } + before :example do + h.ensure_meta_exists keyname, "m1", metadata["m1"] + h.ensure_meta_exists keyname, "m2", metadata["m2"] + provider.resource[:metadata]= metadata + end + + context "should get metadata values" do + it "as a hash" do + got_meta = provider.metadata + + expect(got_meta).to be_a_kind_of Hash + expect(got_meta["m1"]).to eq "v1" + expect(got_meta["m2"]).to eq "v2" + end + + # otherwise, Puppet will think we have to update something and + # triggers an update for metadata. + it "but not include unspecified keys if 'purge_meta_keys' is not set" do + h.ensure_meta_exists keyname, "m3", "xxx" + + got_meta = provider.metadata + + expect(got_meta.include? "m1").to eq true + expect(got_meta.include? "m2").to eq true + expect(got_meta.include? "m3").to eq false + end + + it "and ignore 'internal' metakeys" do + h.ensure_meta_exists keyname, "internal/ini/order", "5" + h.ensure_meta_exists keyname, "internal/ini/parent", "xxx" + + got_meta = provider.metadata + + expect(got_meta.include? "internal/ini/order").to eq false + expect(got_meta.include? "internal/ini/parent").to eq false + end + + it "and ignore 'internal' metakeys with 'purge_meta_keys' set" do + h.ensure_meta_exists keyname, "internal/ini/order", "5" + h.ensure_meta_exists keyname, "internal/ini/parent", "xxx" + h.ensure_meta_exists keyname, "comment/#0", "xxx" + h.ensure_meta_exists keyname, "comments/#0", "xxx" + h.ensure_meta_exists keyname, "comments", "#1" + h.ensure_meta_exists keyname, "order", "5" + + provider.resource[:purge_meta_keys] = true + got_meta = provider.metadata + + expect(got_meta.include? "internal/ini/order").to eq false + expect(got_meta.include? "internal/ini/parent").to eq false + expect(got_meta.include? "comment/#0").to eq false + expect(got_meta.include? "comments/#0").to eq false + expect(got_meta.include? "comments").to eq false + expect(got_meta.include? "order").to eq false + end + + it "and ignore 'special' metakeys with 'purge_meta_key' unless specified" do + h.ensure_meta_exists keyname, "comments/#0", "xxx" + h.ensure_meta_exists keyname, "comments", "#1" + + metadata["comments/#0"] = "xxx" + metadata["comments"] = "#1" + provider.resource[:metadata] = metadata + provider.resource[:purge_meta_keys] = true + + got_meta = provider.metadata + + expect(got_meta.include? "comments/#0").to eq true + expect(got_meta.include? "comments").to eq true + end + end + + context "should update the metadata" do + it "with missing metadata key" do + metadata["m3"] = "v3" + provider.resource[:metadata]= metadata + provider.metadata= metadata + provider.flush + + got_meta = provider.metadata + + expect(got_meta.include? "m3").to eq true + expect(got_meta["m3"]).to eq "v3" + end + + it "with existing metadata" do + got_meta = provider.metadata + + expect(got_meta.include? "m1").to eq true + expect(got_meta.include? "m2").to eq true + expect(got_meta["m1"]).to eq "v1" + expect(got_meta["m2"]).to eq "v2" + end + end + + context "should purge not specified metadata if 'purge_meta_keys' is set" do + before :example do + h.ensure_meta_exists keyname, "r1", "to remove" + h.ensure_meta_exists keyname, "r2", "to remove" + provider.resource[:purge_meta_keys] = true + end + + def has_expected_but_not_specified(got_meta) + expect(got_meta.include? "m1").to eq true + expect(got_meta.include? "m2").to eq true + expect(got_meta.include? "r1").to eq false + expect(got_meta.include? "r2").to eq false + + expect(got_meta["m1"]).to eq "v1" + expect(got_meta["m2"]).to eq "v2" + end + + it "while updating specified" do + h.ensure_meta_exists keyname, "m1", "old value" + provider.metadata= metadata + provider.flush + + got_meta = provider.metadata + + expect(h.check_meta_exists keyname, "m1").to eq true + expect(h.check_meta_exists keyname, "m2").to eq true + expect(h.check_meta_exists keyname, "r1").to eq false + expect(h.check_meta_exists keyname, "r2").to eq false + has_expected_but_not_specified got_meta + end + + it "while ignoring comments, which are not modified" do + h.ensure_comment_exists keyname, "some comment" + + provider.metadata= metadata + provider.flush + got_meta = provider.metadata + + has_expected_but_not_specified got_meta + expect(h.check_comment_exists keyname).to eq true + expect(h.key_get_comment keyname).to eq "some comment" + end + + it "while ignoring comments, which are added too (before)" do + provider.comments= "some comment" + provider.metadata= metadata + provider.flush + + got_meta = provider.metadata + + has_expected_but_not_specified got_meta + expect(h.check_comment_exists keyname).to eq true + expect(h.key_get_comment keyname).to eq "some comment" + end + + it "while ignoring comments, which are added too (after)" do + provider.metadata= metadata + provider.comments= "some comment" + provider.flush + + got_meta = provider.metadata + + has_expected_but_not_specified got_meta + expect(h.check_comment_exists keyname).to eq true + expect(h.key_get_comment keyname).to eq "some comment" + end + + unless not_testable_with_kdb + it "while ignoring 'internal/' metadata keys" do + h.ensure_meta_exists keyname, "internal/test1", "to keep" + + provider.metadata= metadata + provider.flush + got_meta = provider.metadata + + has_expected_but_not_specified got_meta + expect(h.check_meta_exists keyname, "internal/test1").to eq true + expect(h.key_get_meta keyname, "internal/test1").to eq "to keep" + end + end + + it "while ignoring 'order' metadata" do + h.ensure_meta_exists keyname, "order", "5" + + provider.metadata= metadata + provider.flush + got_meta = provider.metadata + + has_expected_but_not_specified got_meta + expect(h.check_meta_exists keyname, "order").to eq true + expect(h.key_get_meta keyname, "order").to eq "5" + end + end + end + + context "should handle comments" do + it "and fetch the comment string" do + h.ensure_comment_exists keyname, "my comment" + + expect(provider.comments).to eq "my comment" + end + + it "and fetch a multiline comment string at once" do + expected_comment = < "one", + "check/enum/#1" => "two", + "check/enum/#2" => "three" + } + exp_checks.each do |k,v| + h.ensure_meta_exists provider.get_spec_key_name, k, v + end + + got_check = provider.check + + expect(got_check.include? "enum").to eq true + expect(got_check.include? "enum/#0").to eq false + expect(got_check["enum"]).to eq exp_checks.values + end + + it "set spec for a single String check" do + h.ensure_meta_is_missing provider.get_spec_key_name, "check/path" + + provider.check= "path" + provider.flush + + expect( + h.check_meta_exists provider.get_spec_key_name, "check/path" + ).to eq true + expect( + h.key_get_meta provider.get_spec_key_name, "check/path" + ).to eq "" + end + + it "set spec for a single Hash check" do + h.ensure_meta_is_missing provider.get_spec_key_name, "check/type" + + provider.check= {"type" => "long"} + provider.flush + + expect( + h.check_meta_exists provider.get_spec_key_name, "check/type" + ).to eq true + expect( + h.key_get_meta provider.get_spec_key_name, "check/type" + ).to eq "long" + end + + it "set spec for multiple checks" do + exp_check = { + "type" => "long", + "type/min" => "5", + "type/max" => "10" + } + + exp_check.keys.each do |c| + h.ensure_meta_is_missing provider.get_spec_key_name, "check/#{c}" + end + + provider.check= exp_check + provider.flush + + exp_check.each do |c, v| + expect( + h.key_get_meta provider.get_spec_key_name, "check/#{c}" + ).to eq v + end + end + + context "set spec to/from array values" do + let(:spec_key) { provider.get_spec_key_name } + let(:exp_check) do { + "enum/#0" => "one", + "enum/#1" => "two", + "enum/#2" => "three" + } + end + + it "set spec for a single check with multiple values (array)" do + exp_check.each do |c, v| + h.ensure_meta_is_missing spec_key, "check/#{c}" + end + + provider.check= {"enum" => exp_check.values} + provider.flush + + exp_check.each do |c, v| + expect( + h.key_get_meta spec_key, "check/#{c}" + ).to eq v + end + expect(h.key_get_meta spec_key, "check/enum").to eq "#2" + end + + it "update spec for a single check with multiple values (array)" do + { "enum" => "#3", + "enum/#0" => "x0", + "enum/#1" => "x1", + "enum/#2" => "x2", + "enum/#3" => "x3"}.each do |k,v| + h.ensure_meta_exists spec_key, "check/#{k}", v + end + + provider.check= { "enum" => exp_check.values } + provider.flush + + exp_check.each do |c, v| + expect( + h.key_get_meta spec_key, "check/#{c}" + ).to eq v + end + expect(h.key_get_meta spec_key, "check/enum").to eq "#2" + expect( + h.check_meta_exists spec_key, "check/enum/#3" + ).to eq false + end + + it "set spec and removes all other 'check/' meta keys" do + missing_check = { "enum" => "'one', 'two'", + "enum/#0" => "x1", + "enum/#1" => "x2", + "type" => "short" + } + missing_check.each do |k,v| + h.ensure_meta_exists spec_key, "check/#{k}", v + end + # to ensure, non 'check' metakeys are not touched + h.ensure_meta_exists spec_key, "other/xy", "xxx" + + provider.check= "path" + provider.flush + + missing_check.each do |k,v| + expect(h.check_meta_exists spec_key, "check/#{k}").to eq false + end + expect(h.check_meta_exists spec_key, "check/path").to eq true + + expect(h.check_meta_exists spec_key, "other/xy").to eq true + expect(h.key_get_meta spec_key, "other/xy").to eq "xxx" + end + end + end +end diff --git a/spec/unit/provider/key_kdb_helper.rb b/spec/unit/provider/key_kdb_helper.rb new file mode 100644 index 0000000..c125afc --- /dev/null +++ b/spec/unit/provider/key_kdb_helper.rb @@ -0,0 +1,121 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require_relative 'key_ruby_helper.rb' + +class KdbKeyProviderHelperKDB < KdbKeyProviderHelper + # alias methods from kdb_ruby_helper + # this allows us to wrap these methods + # + alias ks_ensure_key_exists ensure_key_exists + alias ks_ensure_meta_exists ensure_meta_exists + alias ks_ensure_comment_exists ensure_comment_exists + alias ks_ensure_key_is_missing ensure_key_is_missing + alias ks_ensure_meta_is_missing ensure_meta_is_missing + alias ks_ensure_comment_is_missing ensure_comment_is_missing + alias ks_check_key_exists check_key_exists + alias ks_check_meta_exists check_meta_exists + alias ks_check_comment_exists check_comment_exists + alias ks_key_get_value key_get_value + alias ks_key_get_meta key_get_meta + alias ks_key_get_comment key_get_comment + + def initialize(test_prefix) + super test_prefix + end + + def do_on_kdb + raise ArgumentError, "block required" unless block_given? + + Kdb.open do |kdb| + # make cascading + @test_prefix.gsub!(/^\w+\//, '/') + @ks = Kdb::KeySet.new + kdb.get @ks, @test_prefix + result = yield + kdb.set @ks, @test_prefix + return result + end + end + + + def ensure_key_exists(keyname, value = "test") + do_on_kdb do + ks_ensure_key_exists keyname, value + end + end + + def ensure_meta_exists(keyname, meta, value = "test") + do_on_kdb do + ks_ensure_meta_exists keyname, meta, value + end + end + + def ensure_comment_exists(keyname, comment = "test") + do_on_kdb do + ks_ensure_comment_exists keyname, comment + end + end + + def ensure_key_is_missing(keyname) + do_on_kdb do + ks_ensure_key_is_missing keyname + end + end + + def ensure_meta_is_missing(keyname, meta) + do_on_kdb do + ks_ensure_meta_is_missing keyname, meta + end + end + + def ensure_comment_is_missing(keyname) + do_on_kdb do + ks_ensure_comment_is_missing keyname + end + end + + def check_key_exists(name) + do_on_kdb do + ks_check_key_exists name + end + end + + def check_meta_exists(keyname, meta) + do_on_kdb do + ks_check_meta_exists keyname, meta + end + end + + def check_comment_exists(keyname) + do_on_kdb do + ks_check_comment_exists keyname + end + end + + def key_get_value(keyname) + do_on_kdb do + ks_key_get_value keyname + end + end + + def key_get_meta(keyname, meta) + do_on_kdb do + ks_key_get_meta keyname, meta + end + end + + def key_get_comment(keyname) + do_on_kdb do + ks_key_get_comment keyname + end + end + +end + diff --git a/spec/unit/provider/key_ruby_helper.rb b/spec/unit/provider/key_ruby_helper.rb new file mode 100644 index 0000000..0fdf961 --- /dev/null +++ b/spec/unit/provider/key_ruby_helper.rb @@ -0,0 +1,133 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + + +# helper methods for testing the kdbkey providers + +class KdbKeyProviderHelper + attr :test_prefix + attr :ks + + def initialize(_test_prefix, _ks = Kdb::KeySet.new) + @test_prefix = _test_prefix + @ks = _ks + end + + def ensure_key_exists(keyname, value = "test") + key = @ks.lookup keyname + if key.nil? + @ks << key = Kdb::Key.new(keyname) + end + key.value = value + end + + def ensure_meta_exists(keyname, meta, value = "test") + key = @ks.lookup keyname + if key.nil? + key = Kdb::Key.new(keyname) + @ks << key + end + key.set_meta meta, value + end + + def ensure_comment_exists(keyname, comment = "test") + key = @ks.lookup keyname + if key.nil? + key = Kdb::Key.new keyname + @ks << key + end + # delete old comment first + key.meta.each do |e| + key.del_meta e if e.name.start_with? COMMENT + end + lines = comment.split "\n" + key[COMMENT] = "##{lines.size}" + lines.each_with_index do |line, index| + key[COMMENT+"/##{index}"] = line + end + end + + def ensure_key_is_missing(keyname) + unless @ks.lookup(keyname).nil? + @ks.delete keyname + end + end + + def ensure_meta_is_missing(keyname, meta) + key = @ks.lookup keyname + key.del_meta meta unless key.nil? + end + + def ensure_comment_is_missing(keyname) + key = @ks.lookup keyname + unless key.nil? + key.meta.each do |m| + key.del_meta m if m.name.start_with? COMMENT + end + end + end + + def check_key_exists(name) + !@ks.lookup(name).nil? + end + + def check_meta_exists(keyname, meta) + key = @ks.lookup keyname + unless key.nil? + return key.has_meta? meta + end + false + end + + def check_comment_exists(keyname) + key = @ks.lookup keyname + unless key.nil? + return (key.has_meta?(COMMENT) or key.has_meta?(COMMENT+"/#0")) + end + false + end + + def key_get_value(keyname) + key = @ks.lookup keyname + unless key.nil? + return key.value + end + nil + end + + def key_get_meta(keyname, meta) + key = @ks.lookup keyname + unless key.nil? + return key[meta] + end + nil + end + + def key_get_comment(keyname) + comment = nil + key = @ks.lookup keyname + unless key.nil? + key.meta.find_all do |e| + #e.name.start_with? COMMENT+"/#" + e.name =~ /#{COMMENT}+\/#\d+$/ + end.each do |c| + comment = [] if comment.nil? + if c.value.start_with? "#" + comment << c.value[1..-1] + else + comment << c.value + end + end + return comment.join "\n" unless comment.nil? + end + nil + end +end + + diff --git a/spec/unit/type/kdbkey_spec.rb b/spec/unit/type/kdbkey_spec.rb new file mode 100644 index 0000000..224bdeb --- /dev/null +++ b/spec/unit/type/kdbkey_spec.rb @@ -0,0 +1,284 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' + + +describe Puppet::Type.type(:kdbkey) do + + context "property 'name'" do + let(:name) { "user/test/puppet/x1" } + it "exists and is mandatory" do + expect(described_class.new(:name => name)[:name]).to eq(name) + expect { described_class.new() }.to raise_error(ArgumentError) + end + + RSpec.shared_examples "kdbkey valid key names" do |name| + it "accepts the key name '#{name}'" do + expect(described_class.new(:name => name)[:name]).to eq(name) + end + end + + context "accepts valid libelektra key names" do + # cascading key name + include_examples "kdbkey valid key names", "/test/puppet" + # absolute, by namespace key names + include_examples "kdbkey valid key names", "spec/test/puppet" + include_examples "kdbkey valid key names", "proc/test/puppet" + include_examples "kdbkey valid key names", "dir/test/puppet" + include_examples "kdbkey valid key names", "user/test/puppet" + include_examples "kdbkey valid key names", "system/test/puppet" + end + + RSpec.shared_examples "kdbkey invalid key names" do |name| + it "rejects the invalid key name '#{name}'" do + expect { + described_class.new(:name => name) + }.to raise_error(Puppet::ResourceError) + end + end + + context "rejects invalid libelektra key names" do + include_examples "kdbkey invalid key names", "" + include_examples "kdbkey invalid key names", "hello/world" + include_examples "kdbkey invalid key names", "invalid-name-space" + include_examples "kdbkey invalid key names", "test/xy" + end + end + + context "property 'prefix'" do + let(:name) { "/test/puppet/x1" } + let(:prefix) { "user" } + it "exists and is optional with default value ''" do + expect(described_class.new(:name => name, + :prefix => prefix)[:prefix] + ).to eq(prefix) + expect(described_class.new(:name => name)[:prefix]).to eq("") + end + + RSpec.shared_examples "kdbkey valid key prefix" do |prefix| + it "accepts the key prefix '#{prefix}'" do + expect(described_class.new(:name => "/test", + :prefix => prefix)[:prefix] + ).to eq(prefix) + end + end + + context "accepts valid libelektra key name prefix" do + # cascading key name + include_examples "kdbkey valid key prefix", "/test/puppet" + # absolute, by namespace key names + include_examples "kdbkey valid key prefix", "spec/test/puppet" + include_examples "kdbkey valid key prefix", "proc/test/puppet" + include_examples "kdbkey valid key prefix", "dir/test/puppet" + include_examples "kdbkey valid key prefix", "user/test/puppet" + include_examples "kdbkey valid key prefix", "system/test/puppet" + # without trailing / + include_examples "kdbkey valid key prefix", "system" + end + + RSpec.shared_examples "kdbkey invalid key prefix" do |prefix| + it "rejects the invalid key prefix '#{prefix}'" do + expect { + described_class.new(:name => "/test", + :prefix => prefix) + }.to raise_error(Puppet::ResourceError) + end + end + + context "rejects invalid libelektra key names" do + include_examples "kdbkey invalid key prefix", "hello/world" + include_examples "kdbkey invalid key prefix", "invalid-name-space" + include_examples "kdbkey invalid key prefix", "test/xy" + end + + RSpec.shared_examples "prefix + name" do |prefix, name, expected| + it "with '#{prefix}' (name: '#{name}')" do + expect(described_class.new(:name => name, :prefix => prefix)[:name]).to eq(expected) + end + end + + context "prefixes name property" do + include_examples "prefix + name", "user", "/test/puppet/x1", "user/test/puppet/x1" + include_examples "prefix + name", "system", "/test/puppet/x1", "system/test/puppet/x1" + include_examples "prefix + name", "system/test", "/puppet/x1", "system/test/puppet/x1" + include_examples "prefix + name", "system/", "/test/puppet/x1", "system/test/puppet/x1" + include_examples "prefix + name", "system/", "test/puppet/x1", "system/test/puppet/x1" + include_examples "prefix + name", "system", "test/puppet/x1", "system/test/puppet/x1" + end + + it "updates 'title' to match new 'name'" do + expect(described_class.new(:prefix => "user/test", + :name => "/puppet/x1").title + ).to eq("user/test/puppet/x1") + end + end + + context "property 'value'" do + let(:params) { {:name => "user/test/puppet/x1", :value => "some value"} } + let(:params_wo_value) { {:name => "user/test/puppet/x1"} } + let(:params_w_array) { {:name => "user/test/puppet/x1", :value => ['one', 'two']} } + it "exists and is optional" do + expect(described_class.new(params_wo_value)[:value]).to be_nil + end + it "returns an array of values" do + expect(described_class.new(params)[:value]).to eq([params[:value]]) + expect(described_class.new(params_w_array)[:value]).to eq(params_w_array[:value]) + end + + end + + + context "property 'metadata'" do + let(:params) { {:name => "user/test/puppet/x1", :value => ""} } + it "exists and is optional" do + expect(described_class.new(params)[:metadata]).to be_nil + end + + it "only accepts hash values" do + p1 = params + + p1[:metadata] = "" + expect { described_class.new(p1) }.to raise_error(Puppet::ResourceError) + + p1[:metadata] = "not a hash" + expect { described_class.new(p1) }.to raise_error(Puppet::ResourceError) + + p1[:metadata] = 1 + expect { described_class.new(p1) }.to raise_error(Puppet::ResourceError) + + p1[:metadata] = { + 'meta1' => 'value 1', + 'meta2' => 'value 2' + } + expect(described_class.new(p1)[:metadata]).to eq(p1[:metadata]) + end + end + + + context "parameter 'purge_meta_keys'" do + let(:params) { {:name => "user/test/puppet/x1"} } + it "exists and is default false" do + expect(described_class.new(params)[:purge_meta_keys]).to be_falsy + end + + it "only accepts boolean values" do + p = params + + p[:purge_meta_keys] = "this is not a truth value" + expect { described_class.new(p) }.to raise_error(Puppet::ResourceError) + + p[:purge_meta_keys] = "true" + expect(described_class.new(p)[:purge_meta_keys]).to be true + + p[:purge_meta_keys] = true + expect(described_class.new(p)[:purge_meta_keys]).to be true + + p[:purge_meta_keys] = "false" + expect(described_class.new(p)[:purge_meta_keys]).to be false + end + end + + + context "parameter 'comments'" do + let(:params) { {:name => "user/test/puppet/x1"} } + it "exists and is optional" do + expect(described_class.new(params)[:comments]).to be_nil + end + end + + context "property 'check'" do + let(:params) { {:name => "user/test/puppet/x1"} } + it "exists and is optional" do + expect(described_class.new(params)[:check]).to be_nil + end + + it "accepts a Hash" do + params[:check] = {"type" => "short"} + expect(described_class.new(params)[:check]).to eq params[:check] + end + + it "accepts a String" do + params[:check] = "path" + expect(described_class.new(params)[:check]).to eq params[:check] + end + + it "rejects values for a key within the 'spec' namespace" do + params[:name] = "spec/test/puppet/x1" + params[:check] = "path" + expect { + described_class.new(params) + }.to raise_error(Puppet::Error) + end + + it "rejects values for a key within the 'spec' namespace "\ + "and prefix is used" do + params[:prefix] = "spec/test" + params[:name] = "puppet/x1" + params[:check] = "path" + expect { + described_class.new(params) + }.to raise_error(Puppet::Error) + end + end + + context "parameter 'user'" do + let(:params) { {:name => "user/test/puppet/x1"} } + it "exists and is optional" do + expect(described_class.new(params)[:user]).to be_nil + end + end + + context "auto requires kdbmounts" do + + RSpec.shared_examples "autorequire kdbmounts" do |keyname, expected| + it "each possible mountpoint for '#{keyname}'" do + t = described_class.new({:name => keyname}) + autoreq = {} + t.class.eachautorequire do |type,block| + autoreq[type] = t.instance_eval(&block) + end + + expect(autoreq.include? :kdbmount).to be true + expect(autoreq.include? :kdbkey).to be true + + expect(autoreq[:kdbmount]).to eq expected + + # kdbkey should not require itself + expected.delete keyname + expect(autoreq[:kdbkey]).to eq expected + end + end + + include_examples "autorequire kdbmounts", "user/test", ["user", "user/test"] + include_examples "autorequire kdbmounts", + "system/test", + ["system", "system/test"] + + include_examples "autorequire kdbmounts", + "system/test/hello/world", + ["system", "system/test", "system/test/hello", "system/test/hello/world"] + + include_examples "autorequire kdbmounts", + 'system/test/hello\/escaped\/world/a/b', + ["system", "system/test", 'system/test/hello\/escaped\/world', + 'system/test/hello\/escaped\/world/a', + 'system/test/hello\/escaped\/world/a/b'] + + include_examples "autorequire kdbmounts", "/test/puppet", [ + "system/test", "system/test/puppet", + "user/test", "user/test/puppet", + "spec/test", "spec/test/puppet", + "dir/test", "dir/test/puppet" + ] + + end + + +end diff --git a/spec/unit/type/kdbmount_spec.rb b/spec/unit/type/kdbmount_spec.rb new file mode 100644 index 0000000..9643c6c --- /dev/null +++ b/spec/unit/type/kdbmount_spec.rb @@ -0,0 +1,129 @@ +# encoding: UTF-8 +## +# @file +# +# @brief +# +# @copyright BSD License (see LICENSE or http://www.libelektra.org) +# + +require 'spec_helper' + + +describe Puppet::Type.type(:kdbmount) do + + context "property 'name'" do + let(:name) { "user/test/puppet" } + it "exists and is mandatory" do + expect(described_class.new(:name => name)[:name]).to eq(name) + expect { described_class.new() }.to raise_error(ArgumentError) + end + + RSpec.shared_examples "valid key names" do |name| + it "accepts the key name '#{name}'" do + expect(described_class.new(:name => name)[:name]).to eq(name) + end + end + + context "accepts valid libelektra key names" do + # cascading key name + include_examples "valid key names", "/test/puppet" + # absolute, by namespace key names + include_examples "valid key names", "spec/test/puppet" + #include_examples "valid key names", "proc/test/puppet" + include_examples "valid key names", "dir/test/puppet" + include_examples "valid key names", "user/test/puppet" + include_examples "valid key names", "system/test/puppet" + end + + RSpec.shared_examples "invalid key names" do |name| + it "rejects the invalid key name '#{name}'" do + expect { + described_class.new(:name => name) + }.to raise_error(Puppet::ResourceError) + end + end + + context "rejects invalid libelektra key names" do + include_examples "invalid key names", "" + include_examples "invalid key names", "hello/world" + include_examples "invalid key names", "invalid-name-space" + include_examples "invalid key names", "test/xy" + end + end + + context "property 'file'" do + let(:params) { { + :name => "user/test/puppet", + :file => "some value"} + } + it "exists" do + expect(described_class.new(params)[:file]).to eq(params[:file]) + end + + let(:params) { { + :name => "user/test/puppet"} + } + it "is optional" do + expect(described_class.new(params)[:file]).to be_nil + end + end + + + context "property 'plugins'" do + let(:params) { { + :name => "user/test/puppet" + } + } + it "exists and is optional if file is not used" do + expect(described_class.new(params)[:plugins]).to be_nil + end + + it "exists and is mandatory if file is set" do + params[:file] = 'somefile.txt' + expect { described_class.new(params) }.to raise_error(Puppet::Error) + end + + it "accepts a string" do + params[:file] = 'somefile.ini' + params[:plugins] = "ini" + expect(described_class.new(params)[:plugins]).to eq ["ini"] + end + + it "accepts an array of strings" do + params[:file] = 'somefile.ini' + params[:plugins] = ["ini", "type"] + expect(described_class.new(params)[:plugins]).to eq ["ini", "type"] + end + + it "accepts an array with plugin name with corresponding configuration settings" do + params[:file] = 'somefile.ini' + params[:plugins] = ["ini", {"seperator" => " ", "array" => ""}] + expect(described_class.new(params)[:plugins]).to eq params[:plugins] + end + + it "accepts a Hash with plugin name with corresponding configuration settings" do + params[:file] = 'somefile.ini' + params[:plugins] = {"ini" => {"seperator" => " ", "array" => ""}} + # we always get an Array back + expect(described_class.new(params)[:plugins]).to eq [params[:plugins]] + end + + RSpec.shared_examples "invalid plugin names" do |plugins| + it "rejects invalid plugin names '#{plugins}'" do + expect { + described_class.new(:name => params[:name], :plugins => plugins) + }.to raise_error(Puppet::ResourceError) + end + end + + context "rejects invalid plugin names" do + include_examples "invalid plugin names", "invalid plugin" + include_examples "invalid plugin names", " ini" + include_examples "invalid plugin names", [" ini"] + include_examples "invalid plugin names", ["ini", "type "] + include_examples "invalid plugin names", ["ini", "type", "$doesnotexist%"] + end + end + +end