How to create puppet external structured facts from script, such as python or bash? - puppet

I have seen the documentation, especially External Facts and Custom Facts.
I have the following yum_repos.rb:
require 'facter'
Facter.add('yum_repos') do
setcode '/usr/bin/python3 /opt/puppetlabs/puppet/cache/lib/facter/yum_repos.py'
end
I have the following yum_repos.py:
#!/usr/bin/python3
import configparser
import os
import json
result_dict={}
yum_repos_dir = '/etc/yum.repos.d'
for yum_repo_file in os.listdir(yum_repos_dir):
if not yum_repo_file.endswith(".repo"):
continue
yum_repo_name=yum_repo_file.replace(".repo","")
config = configparser.ConfigParser()
config.read(os.path.join(yum_repos_dir, yum_repo_file))
result_dict[yum_repo_name] = {}
for section in config.sections():
result_dict[yum_repo_name][section] = dict(config[section])
print(json.dumps(result_dict))
When I check facter, it all runs, but the yum_repos fact is a string. Why isn't it structured? I found somewhere on the internet, saying (for old versions of puppet) that stringify_facts must be set to false, so I tried that, but the behavior did not change. I believe that the default was changed in 4.0 to not stringify.
I am using puppet 6.24.0
I try to access the facts in a puppet class like this:
if $facts['yum_repos']['redhat']...['enabled'] != 1 {
...
}
And when I run puppet agent, I get the error message:
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Evaluation Error: A substring operation does not accept a String as a character index. Expected an Integer

I never figured out why puppet is stringifying the fact, but I was able to workaround by explicitly calling parsejson(), like so:
if parsejson($facts['yum_repos'])['redhat']...['enabled'] != "1" {
...
}

Related

How to use pystemd to control systemd timedated ntp service?

I'm working on a python app that needs to get the NTPSynchronized parameter from system-timedated. I'd also like to be able to start and stop the NTP service by using the SetNTP method.
To communicate with timedated over d-bus I have been using this as reference: https://www.freedesktop.org/wiki/Software/systemd/timedated/
I previously got this working with dbus-python, but have since learned that this library has been deprecated. I tried the dbus_next package, but that does not have support for Python 3.5, which I need.
I came across the pystemd package, but I am unsure if this can be used to do what I want. The only documentation I have been able to find is this example (https://github.com/facebookincubator/pystemd), but I can not figure out how to use this to work with system-timedated.
Here is the code I have that works with dbus-python:
import dbus
BUS_NAME = 'org.freedesktop.timedate1`
IFACE = 'org.freedesktop.timedate1`
bus = dbus.SystemBus()
timedate_obj = bus.get_object(BUS_NAME, '/org/freedesktop/timedate1')
# Get synchronization value
is_sync = timedate_obj.Get(BUS_NAME, 'NTPSynchronized', dbus_interface=dbus.PROPERTIES_IFACE)
# Turn off NTP
timedate_obj.SetNTP(False,False, dbus_interface=IFACE)
Here's what I have so far with pystemd, but I don't think I'm accessing it in the right way:
from pystemd.systemd1 import Unit
unit = Unit(b'systemd-timesyncd.service')
unit.load()
# Try to access properties
prop = unit.Properties
prop.NTPSynchronized
Running that I get:
Attribute Error: 'SDInterface' object has no attribute 'NTPSynchronized'
I have a feeling that either the service I entered is wrong, or the way I'm accessing properties is wrong, or even both are wrong.
Any help or advice is appreciated.
Looking at the source code, it appears that using the pystemd.systemd1 Unit object has a default destination of "org.freedesktop.systemd1" + the service name (https://github.com/facebookincubator/pystemd/blob/master/pystemd/systemd1/unit.py)
This is not what I want because I am trying to access "org.freedesktop.timedate1"
So instead I instantiated it's base class SDObject from pystemd.base (https://github.com/facebookincubator/pystemd/blob/master/pystemd/base.py)
The following code allowed me to get the sync status of NTP
from pystemd.base import SDObject
obj = SDObject(
destination=b'org.freedesktop.timedate1',
path=b'/org/freedesktop/timedate1',
bus=None,
_autoload=False
)
obj.load()
is_sync = obj.Properties.Get('org.freedesktop.timedate1','NTPSynchronized')
print(is_sync)
Not sure if this is what the library author intended, but hey it works!

Twisted upgrade - now can't access site paths

I was running on Python2.6 and Twisted 15.0.0 with this:
from twisted.python import usage
from twisted.web import resource, server, static
from twisted.application import internet
class Options(usage.Options):
optParameters = [
]
def makeService(options):
# this variation works
root_resource = static.File('/tmp')
# this variation doesn't
root_resource = resource.Resource()
root_resource.putChild("here", static.File('/tmp'))
site = server.Site(root_resource)
web_svc = internet.TCPServer(8000, site)
return web_svc
But after upgrading to Python3.7 and twisted latest (18.7.0) I can't get anything at http://localhost:8000/here
No Such Resource
No such child resource.
Can't find any twisted docs or examples that say a different way to do it.
Additional:
It is starting up the service fine, else I wouldn't see the above.
For reproduction purposes, the twisted/plugins/my_plugin.py looks like:
from twisted.application.service import ServiceMaker
svc = ServiceMaker("TEST_ONE",
"svc",
"Service",
"tstsvc"
)
And executed with:
twist tstsvc
Mystery solved.
Of course it's Python3 here again with String handling.
root_resource.putChild(b"here", static.File('/tmp'))
Without the 'b' in front of the path, it never matched the url as typed.
Thought: Should the putChild() api throw an error if it's passed a str here? Seems bytes is always the right answer and the only thing that could match

Linux Command Output to Chef Attribute

The issue is I need to assign the value of Linux command to CHef Attribute.But unable to do it.
Im using the below code and not finding the result. Kindly Help what im
missing
ruby_block "something" do
block do
Chef::Resource::RubyBlock.send(:include, Chef::Mixin::ShellOut)
node.default['foo'] = shell_out("echo Hello world").stdout
end
action :create
end
log "demo" do
message lazy { node['foo'] }
end
Below is the Run logs:
Starting Chef Client, version 13.9.1
resolving cookbooks for run list: ["sample_repo"]
Synchronizing Cookbooks:
- sample_repo (0.1.4)
Installing Cookbook Gems:
Compiling Cookbooks...
Converging 2 resources
Recipe: sample_repo::default
* ruby_block[something] action create
- execute the ruby block something
* log[demo] action write
Running handlers:
Running handlers complete
Chef Client finished, 2/2 resources updated in 02 seconds
Thanks in advance
Your code is fine, the log message is not showing because the default level on the log resource is :info and by default chef-client doesn't show info-level log messages when run interactively. That said, this kind of code where you store stuff in node attributes is very brittle and probably shouldn't be used unless specifically needed. Better is to do this:
message lazy { shell_out("echo Hello world").stdout }
Also you don't need any funky mutating include stuff like you there AFAIK, the shell_out helpers are available in most contexts by default. Also you should usually use shell_out!() rather than shell_out(), the ! version automatically raises an exception if the command fails. Unless you specifically want to allow a failed command, use the ! version.

Jenkins: extended choice parameter - groovy - how to create file on the master

i have a json string defined in the groovy script part of the 'extended choice parameter' plugin. Additionally I want to write the json config in a file on the master side inside the groovy script area. I thought, maybe the job directory would be the best place?
http://hudson/hudson/job/MY_JOB/config.json
If you ask now, why i should do this; the reason behind is, i don´t want the config pre-saved somewhere else. I don´t like the idea of configuring the file outside of the job config. I want to see/adjust configs at one place - in the job config.
I need many other informations from the json config for later use in a python code section within the same job.
My questions are:
Am i following a wrong path here? Any suggestions?
can i write directly the json config on the master side? It doesn´t have to be the jenkins job directory. I don´t care about the device/directory.
if the approach is acceptable, how can i do this?
The following code doesn´t work:
def filename = "config.json"
def targetFile = new File(filename)
if (targetFile.createNewFile()) {
println "Successfully created file $targetFile"
} else {
println "Failed to create file $targetFile"
}
Remark:
hudson.FilePath looks interesting!
http://javadoc.jenkins-ci.org/hudson/FilePath.html
Thanks for your help, Simon
I got it:
import groovy.json.*
// location on the master : /srv/raid1/hudson/jobs
jsonConfigFile = new File("/srv/raid1/hudson/jobs/MY_JOB/config.json")
jsonConfigFileOnMaster = new hudson.FilePath( jsonConfigFile )
if( jsonConfigFileOnMaster.exists() ){
jsonConfigFileOnMaster.delete()
}
jsonConfigFileOnMaster.touch( System.nanoTime())
jsonFormatted = JsonOutput.toJson( localJsonString )
jsonConfigFile.write jsonFormatted

Puppet inline template with puppet:// in URL

In my Puppet module, I have something like this:
file {'myfile':
ensure => 'file',
path => '/whatever/myfile',
content => inline_template(file(
"puppet:///modules/my_module/templates/${domain}/${::hostname}_myfile.erb",
"puppet:///modules/my_module/templates/${domain}/myfile.erb"
))
}
And my spec looks like:
require 'spec_helper'
describe 'my_module' do
context 'with defaults for all parameters' do
it { should compile }
end
end
If try to run it, I get:
Failure/Error: it { should compile }
error during compilation: Evaluation Error: Error while evaluating a Function Call, Could not find any files from puppet:///modules/my_module/templates/dev.contaazul.local/myfile.erb, puppet:///modules/my_module/templates/dev.contaazul.local/myfile.erb at /home/carlos/Code/Puppet/modules/my_module/spec/fixtures/modules/my_module/manifests/init.pp:48:33 on node becker.local
Obviously, it cannot find any of the ERB templates. If I replace the puppet:// part for /home/carlos/Code/Puppet/ (where the code actually lives), it passes, but in production it is /etc/puppet/, so, it will not work.
How can I make this work?
RI Pienaar has released a code snippet for a function with this behavior. You will need to copy this code into a file in one of your modules at the path lib/puppet/parser/functions/multi_source_template.rb.
Once you do that, you should be able to use it like so:
file {'myfile':
ensure => 'file',
path => '/whatever/myfile',
content => multi_source_template(
"my_module/${domain}/${::hostname}_myfile.erb",
"my_module/${domain}/myfile.erb"
)
}
As to why the original approach doesn't work: URLs are usually used with the source property only and transferred to the agent as is. The agent consumes the URL and makes an according request to a Puppet fileserver (which is just another master).
The file function on the other hand (as used here with the content property in conjunction with inline_template) will not process URLs and expects local paths instead.
All that being said, the issue could possibly have been side-stepped by specifying both the paths for the test box and the production system, with the latter just acknowledging that /home/carlos is missing. But that would have been far from a clean solution.

Resources