There is a language pattern I would like to use in puppet and am having trouble figuring out how to implement it. Let's say I have a parameterized class:
class ntp (
$ensure = 'present',
$ntp_package_ensure = 'present',
$ntp_package_name = 'ntp',
$ntp_config_ensure = 'file',
$ntp_config_path = '/etc/ntp.conf',
$ntp_service_ensure = 'running',
$ntp_service_enable = true,
$ntp_service_name = 'ntpd',
) {
I would then really like to be able to say:
if $ensure == 'absent' {
$ntp_package_ensure = 'absent',
$ntp_config_ensure = 'absent',
$ntp_service_ensure = 'stopped',
}
But because puppet variables (constants?) are immutable I'm getting syntax errors. So What is puppets way of doing this? The only solution I can see right now is to add a monolith if statement that partitions the code into an ensure section and an else section.
The most straight forward workaround is to actually introduce new local variables.
case $ensure {
'absent': {
$my_ntp_package_ensure = 'absent'
$my_ntp_config_ensure = 'absent'
$my_ntp_service_ensure = 'stopped'
}
'present': {
...
}
default: {
$my_ntp_package_ensure = $ntp_package_ensure
$my_ntp_config_ensure = $ntp_config_ensure
$my_ntp_service_ensure = $ntp_service_ensure
}
}
Then only use the local counterparts instead of the parameters proper.
I recommend to refrain from such an API design and instead ask a new question concerning the actual scenario and how to structure the ntp module better.
Related
So I came across this general problem and didn't find an answer yet.
Problem: The input value can have optional variables, like the case below, group_memberships is an optional input, at the moment I make it an empty string input for this to work.
But if I comment it out like shown below and run it, I would get the error:
The given key does not identify an element in this collection value.
Basically it's complaining that I don't have list_of_users.test_user.group_memberships.Is there a way to tell terraform if the input is not declared, just ignore it? I know I can leave it the way it is but user can potentially have many optional values, and making lots of empty input doesn't really make sense.
Thanks! First post question, sorry about poor layout for the code : )
in my .tfvars file:
list_of_users = {
regular_user = {
email = "pdv#abc.com",
group_memberships = "regular_group"
},
test_user = {
email = "test#abc.com",
// group_memberships = "" <------ Currently can work if not comment out, looking for solution that I can remove those reduent empty declariation
},
admin_user = {
email = "admin#abc.com",
group_memberships = "admin_group"
}
}
in .tf file:
variable "list_of_users" {}
resource "user_api_from_provider" "user_generate" {
for_each = var.list_of_users
email = each.value["email"]
group_memberships = each.value["group_memberships"] !=""? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
There is support for this as a Terraform "experiment" (it's implemented, but could change or be removed in future versions). You have to declare in your module that you're using the experiment:
terraform {
# Optional attributes and the defaults function are
# both experimental, so we must opt in to the experiment.
experiments = [module_variable_optional_attrs]
}
And then you would use it in your case like this:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string)
}))
}
Now, if group_membership isn't defined for a given user, that field will have the value of null, so you can now do:
resource "user_api_from_provider" "user_generate" {
...
group_memberships = each.value.group_memberships != null ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
Alternatively, if you don't want to use the experiment, you should be able to do this (untested):
resource "user_api_from_provider" "user_generate" {
...
group_memberships = contains(each.value, "group_memberships") ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
As of Terraform v1.3 the Optional Object Type Attributes feature is official, which means it is no longer an experiment and the syntax is considered stable.
As mentioned in previous comments, you can now do something like:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string, "")
}))
}
In the above example, using the default value ("") allows the Terraform code in the project/module to function as if there is always a value even if it is omitted from the input variables.
I am using node-config in basically all my projects and most of the time I come across the problem of parsing booleans and numbers which are set as environment variables.
E.g.
default.js
module.exports = {
myNumber = 10,
myBool = true
}
custom-environment-variables.js
module.exports = {
myNumber = "MY_NUMBER",
myBool = "MY_BOOL"
}
Now, the obvious problem is that if I override the default values with custom values set as environment variables they will be a string value instead of a number or boolean value. So now, to make sure in my code that the types are correct. I always have to do type conversion and for booleans use a proper library e.g. yn. The problem is I have to do this conversion every time I use config.get() for example +config.get("myNumber") or yn(config.get("myBool")).
Is there a better and more elegant way to do this?
One solution I see would be to add a type property to an environment variable as it is done here with format. This would allow to do something like this...
custom-environment-variables.js
module.exports = {
myNumber = {
name: "MY_NUMBER",
type: "number"
},
myBool = {
name: "MY_BOOL",
type: "boolean"
}
}
node-config would handle the type conversions and there would be no need to do it all the time in the code when getting it. Of course there would be the requirement to implement a proper parser for booleans but those already exist and could be used here.
By default, environment variables will be parsed as string.
In node-config, we could override this behaviour with __format as shown below.
We don't need any additional libraries. Normal json datatypes like boolean, number, nested json etc., should work well.
Taking an easy to relate example.
config/default.json
{
"service": {
"autostart": false
}
}
custom-environment-variables.json
{
"service": {
"autostart": {
"__name": "AUTOSTART",
"__format": "json"
}
}
}
Now we can pass environment variables when we like to override and no type conversation should be needed for basic types.
This feature is now supported in node-config v3.3.2, see changelog
I use this method:
const toBoolean = (dataStr) => {
return !!(dataStr?.toLowerCase?.() === 'true' || dataStr === true);
};
You can add cases if you want 0 to resolve to true as well:
const toBoolean = (dataStr) => {
return !!(dataStr?.toLowerCase?.() === 'true' || dataStr === true || Number.parseInt(dataStr, 10) === 0);
};
I'm trying to automate the Prometheus node_exporter and my Prometheus Server.
For the node_exporter I've written a module to install all the needed packages, set the $::ipaddress based on facter and some more..
Now I'd like to make sure that the collected informations ($hostname, $job_name, [...]) from the applying node are exported into the respective remote Prometheus configfile, but I want to have this step done asynchronously, so for example with a puppet agent run afterwards on the Prometheus Server.
I've tried to orientate the classes towards the puppetlabs/logrotate module, which is basically doing the following:
logrotate/init.pp
class logrotate (
String $ensure = present,
Boolean $hieramerge = false,
Boolean $manage_cron_daily = true,
Boolean $create_base_rules = true,
Boolean $purge_configdir = false,
String $package = 'logrotate',
Hash $rules = {},
) {
do some stuff
}
logrotate/rules.pp
class logrotate::rules ($rules = $::logrotate::rules){
#assert_private()
create_resources('logrotate::rule', $rules)
}
logrotate/rule.pp
define logrotate::rule(
Pattern[/^[a-zA-Z0-9\._-]+$/] $rulename = $title,
Enum['present','absent'] $ensure = 'present',
Optional[Logrotate::Path] $path = undef,
(...)
) {
do some stuff
}
Shortened my ni_trending (node_exporter) & ni_prometheus modules currently look very similar to logrotate:
ni_trending/init.pp
class ni_trending (
$hostname = $::fqdn,
$listen_address = $::ipaddress,
$listen_port = 51118,
) {
) inherits ni_trending::params {
anchor { 'ni_trending::start': }
->class { 'ni_trending::package': }
->class { 'ni_trending::config':
(...)
listen_address => $listen_address,
listen_port => $listen_port,
(...)
}
->class { 'ni_trending::service': }
->class { ' ni_trending::prometheus':
(...)
hostname => $hostname,
listen_port => $listen_port,
(...)
}
->anchor { 'ni_trending::end': }
}
ni_trending/prometheus.pp
class ni_trending::prometheus (
Hash $options = {},
) {
ni_prometheus::nodeexporterrule { 'node_exporter' :
ensure => pick_default($options['ensure'], 'present'),
hostname => pick_default($options['hostname'], $ni_trending::hostname),
listen_port => pick_default($options['hostname'], $ni_trending::listen_port),
}
}
ni_prometheus/nodeexporterrules.pp
class ni_prometheus::nodeexporterrules ($rules = $::ni_prometheus::nodeexporterrules) {
create_resources('ni_prometheus::nodeexporterrule', $nodeexporterrules)
}
ni_prometheus/nodeexporterrule.pp
define ni_prometheus::nodeexporterrule (
$job_name = $title,
Enum['present','absent'] $ensure = 'present',
$hostname = $hostname,
$listen_port = $listen_port,
) {
file_line { "prometheus-${job_name}" :
path => "/etc/prometheus/${job_name}.list",
after => 'hosts:',
line => "${hostname}:${listen_port}",
}
}
But this will just work when I apply the node_exporter locally on the Prometheus Master - not in the case that an external machine has the ni_trending::prometheus class included, which makes sense to me - because it clearly feels that something is missing. :-) How can I get this working?
Thanks!
This sounds like a job for exported resources (that makes two in one day!). This is a facility for one node's catalog building to generate resources that can be applied to other nodes (and also, optionally, to the exporting node itself). I'm still not tracking the details of what you want to manage where, so here's a more generic example: maintaining a local hosts file.
Generic example
Suppose we want to automatically manage a hosts file listing all our nodes under management. Puppet has a built-in resource, Host, representing one entry in a hosts file. We make use of that by having every node under management export an appropriate host resource. Something like this would go inside a class included on every node:
##host { "$hostname": ip => $ipaddress; }
The ## prefix marks the resource as exported. It is not applied to the current target node, unless by the mechanism I will describe in a moment. the $hostname and $ipaddress are just facts presented by the target node, and they are resolved in that context. Note, too, that the resource title is globally unique: each target node has a different hostname, therefore all the exported Host resources that apply to different target nodes will have distinct titles.
Then, separately, every node that wants all those Host entries applied to it will import them in its own catalog by using an exported resource collector:
<<|Host|>>
The nodes that export those resources can also collect some or all of them. Additionally, there are ways to be more selective about which resources are collected; see the link above.
I am new to puppet and I am trying to write a module to manage .bashrc file of 10 users. The following code is ok to manage the file of 1 user. However, I am unable to change the code to manage files for 10 users. I tried using defined types and variable with no luck. Can sombody please suggest me the right way to do this.
init.pp:
class profile (
$bashrc = $profile::params::bashrc,
$bashrc_host = $profile::params::bashrc_host,
) inherits profile::params {
anchor { 'profile::begin': } ->
class { '::profile::config': } ->
anchor { 'profile::end': }
}
config.pp:
class profile::config inherits profile {
file { $bashrc:
ensure => file,
source => "puppet:///$bashrc_host",
}
params.pp:
class profile::params {
$bashrc_host = "modules/profile/$fqdn_user1_bashrc"
}
case $::osfamily {
'RedHat': {
$bashrc = '/home/user1/.bashrc'
}
}
This is not at all a job for a class. As you noted yourself in your most recent comment, this calls for a define actually.
Please don't use verbs in the names of your defines. Instead of defineuser, just do
define profile::user($host_name) {
}
Off the top of my hat, I'm not aware of a good pattern to use module parameters in your defines. You can however use the following pattern:
class profile(
$default_shell = $profile::params::default_shell,
$default_prompt = $profile::params::default_prompt,
$users = {}
) inherits profile::params {
$defaults = { shell => $default_shell, prompt => $default_prompt }
create_resources('profile::user', $users, $defaults)
}
What happens is
values are taken from params, or hiera, or the invoking manifest
these values are gathered in the $defaults array
for any resource in the $users hash that has no shell or prompt, this default is used
If your aim of this module is to learn puppet then:
Add a param user to your class profile::params
class profile::params {
$bashrc_host = "modules/profile/$fqdn_user1_bashrc"
$user = 'user1',
}
case $::osfamily {
'RedHat': {
$bashrc = "/home/$user/.bashrc"
}
}
After this, you can use a combination of array or hiera and ensure_resource This still is not the most elegant solution, but baby steps.
If your intend is to actually manage the bashrc for various users, I would recommend using a pre existing module such as account
I am trying to use the exec resource type to execute a batch file. But I want to pass the value of variable $dsn_64bit from init.pp to install.pp. Please let me know how to achieve that:
Here is my init.pp
class exec_batchfile ($dsn_64bit = "false")
{
if $::osfamily == 'windows' {
include exec_batchfile::install
}
}
Here is my install.pp
class exec_batchfile::install
{
if $dsn_64bit == true
{
$hklm_path = 'HKLM\Software\Oracle'
$Script_name = 'E:\\Path\\pupp_test64.bat'
}
else
{
$hklm_path = 'HKLM\Software\WOW6432Node\Oracle'
$Script_name = 'E:\\Path\\pupp_test.bat'
}
exec { 'exec_batchfile':
command => "${Script_name}",
path => $::path,
logoutput => true,
unless => "cmd.exe /c reg query ${hklm_path} /v inst_loc",
}
}
Thanks
Since puppet 2.7 dynamic lookup is deprecated, so your code will not work properly. Right now the recommended solution is to use fully qualified names. Please follow the link to find a comprehensive explanation.
Whenever you need to refer to a variable in another class, give the variable an explicit namespace: instead of simply referring to $packagelist, use $git::core::packagelist.