In Puppet, how to use defined node variables in an if clause - variables

In a puppet class how should I test if a variable has been set in a node?
I use a VM name (like server1) and a domain name (like example.org) where users can reach the page. "example.org" won't be conveyed via a fact, so I need to pass it via a class parameter. I came up with this way to define the variable in a node block and use it in my test class for my settings.
node "VM1" {
class { 'test':
domainname => "example.org",
}
[...]
class test ($domainname) {
ini_setting {
'set_property':
ensure => present,
path => '/tmp/test.ini',
section => 'main',
setting => 'url',
value => "https://$domainname";
}
[...]
But now I want to add a condition that if $domainname isn't set then the $hostname fact should be used in its place.
ini_setting {
'set_property':
ensure => present,
path => '/tmp/test.ini',
section => 'main',
setting => 'url',
if $domainname !~ $hostname {
value => "https://$domainname";
} else {
value => "https://$hostname";
}
But now I get an error like this every time:
Error: Could not retrieve catalog from remote server: Error 500 on SERVER: Server Error: Syntax error at 'domainname'
What should I do instead?

The error message is explaining to you that if statements cannot appear inside resource declarations. There is, however, a different conditional form, called a "selector" that can appear inside resource declarations. It is Puppet's analog of the ternary ?: operator that appears in several languages.
Stylistically, though, it is usually better form to keep resource declarations as simple as possible. To that end, you should probably set a variable, conditionally, outside the resource declaration, and then use its value inside. Using your own conditional, that might look like this:
if $domainname !~ $hostname {
$url_value = "https://$domainname";
} else {
$url_value = "https://$hostname";
}
ini_setting {
'set_property':
ensure => present,
path => '/tmp/test.ini',
section => 'main',
setting => 'url',
value => $url_value;
}
Additionally, however, I note that your particular condition, repeated above, is highly suspect. In recent Puppet (version 4 and above), you should be using Puppet data types to both declare your class parameters and check them. In particular, if it is permissible to declare class test without providing a $domainname parameter, then you would declare that class like so:
# Using the Puppet v4+ type system
class test(
Optional[String] $domainname = undef
) {
# ...
, and would test whether a value was provided for $domainname like so:
if $domainname =~ Undef {
# ...
}
You cannot use the type system in earlier Puppet, but there you can rely on undefined variables to expand to nothing when you interpolate them:
# Using the Puppet v3- behavior
class test(
$domainname = undef
) {
# ...
if "$domainname" == "" {
# ...
}
# ...
}

Related

Conditonal resource attributes

To install packages, I feed in data from Hiera into a for loop. Some packages require additional arguments. For packages that do not require an argument, I've set the value to undef, however, Chocolatey reads undef and complains.
How do I get the package resource to ignore the install_options attribute when it is blank or undef?
Hiera snippet:
profile::business::packages:
office365business:
version: latest
provider: chocolatey
arguments: ['/productid:O365BusinessRetail']
xmind:
version: latest
provider: chocolatey
arguments: undef
slack:
version: latest
provider: chocolatey
arguments: undef
Class example:
class profile::business(
Hash $packages,
){
if $::kernel == 'windows' {
$packages.each | $key, $value | {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
install_options => $value['arguments'],
notify => Reboot['after_profile_business'],
}
}
reboot { 'after_profile_business' :
apply => finished,
message => 'Reboot: Business profile applied.'
}
}
}
The best I can come up with is using an if clause to apply different instances of the package resource with or without install_options, depending on the value of arguments:
$packages.each | $key, $value | {
if $value['arguments'] != 'undef' {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
install_options => $value['arguments'],
notify => Reboot['after_profile_admin'],
}
} else {
package { "install_${key}" :
name => $key,
ensure => $value['version'],
provider => $value['provider'],
notify => Reboot['after_profile_admin'],
}
}
}
However, this seems rather clunky and I'm hoping someone might be able to show me a better way?
I've seen the Puppet Selector condition example, but I do not know if this will work for me.
T.I.A
This YAML fragment ...
arguments: undef
... sets the value of the 'arguments' key to the string 'undef'. That doesn;t mean the same thing on the Puppet side as the Puppet literal undef.
There are solutions. All of the best, IMO, revolve around representing absence of data via bona fide absence of data. That avoids any need for special reserved words. So suppose your data looked like this, instead:
profile::business::packages:
office365business:
version: latest
provider: chocolatey
arguments: ['/productid:O365BusinessRetail']
xmind:
version: latest
provider: chocolatey
slack:
version: latest
provider: chocolatey
Note that there is no entry bearing the arguments key where there are in fact no arguments to specify. If you have been rigorous and thorough about defining data types, then you may need to adjust your data type for these data to accommodate that, but so much the better because that would better describe the actual data semantics. That data modification probably resolves your issue by itself, because looking up a key that does not exist in a hash that does exist should yield undef (and there's also dig() if the undefinedness can occur at a higher level of a deep data structure).
Consider also, however, that Puppet has a shortcut for declaring that resource property values are drawn from a hash. That won't quite fit your present data because your keys are not the same as the needed property names, but you could either change the keys in your data or map them at the Puppet level. The latter might look like this:
# Defining the key / property name mappings here makes them clear, and is easy to
# change if you need to update the mappings
$mappings = { 'version' => 'ensure', 'arguments' => 'install_options' }
$packages.each |$package, $properties| {
# map the keys appearing in the data to Puppet property names, based on
# the hash defined above
$filtered_props = $properties.reduce({}) |$memo, $pair| {
$mapped_key = $pair[0] in $mappings ? { true => $mappings[$pair[0]], default => $pair[0] }
$memo + { $mapped_key => $pair[1] }
}
# one declaration covering all cases
package { "install_${package}" :
name => $package,
provider => $value['provider'],
notify => Reboot['after_profile_admin'],
* => $filtered_props,
}
}

Exporting of dynamically scoped variables?

Basically, the question is more about grammars but I think that it could be more of a interesting exercise on dynamic variables.
I have a grammar role with a prototyped token (the example is simplified to demonstrate the idea):
proto token foo {*}
token foo:sym<a> {
:my $*delimiter = q<">;
\" ~ \" <value>
}
token foo:sym<b> {
:my $*delimiter = q<'>;
\' ~ \' <value>
}
token value {
.+? <?before $($*delimeter) || $($*custom-delimiter)>
}
When the role is consumed by a grammar I want the $*custom-delimiter to be set by the grammar. Of course, I can declare it everywhere where <foo> is needed. But sometimes it is ok to have it pre-initialized with a universal default. Something like:
{ $*custom-delimiter //= $default-delimiter }
in the value token would work. But external pre-declaration would still be needed.
I hoped that:
our $*custom-delimiter is export = $default-delimiter;
in the scope of module where the role is declared would work. But apparently it doesn't. So, the question is: are there any elegant solutions to this?
Actually, I also hope that the solution would allow to move declaration of $*delimiter in foo outside of the token definitions too.
As a side note: my first thought was about adding a parameter to the token. But having absolutely identical signatures for each variant is looking terrible too:
token foo:sym<a> ( $*custom-delimiter = $default-delimiter ) {
}
token foo:sym<b> ( $*custom-delimiter = $default-delimiter ) {
}
token foo:sym<c> ( $*custom-delimiter = $default-delimiter ) {
}
Another approach is to have something like:
token pre-foo ( $*custom-delimiter = $default-delimiter ) {
<foo>
}
In this case an additional method would be required in actions class to propagate $/<foo>.ast one level up.
Based on some test work I've done in one of my modules for allowing scoped settings for a module, you can do this but you will need to use the EXPORT sub.
I imagine the reason is that when doing EXPORT, we can install what is explicitly a brand new dynamic variable, rather than a new symbol linked to an extant dynamic variable — the latter of which to me makes scoping very unclear.
This seems to work okay for me.
# filename: Foo.rakumod
# no 'unit module', etc
sub EXPORT {
proto token foo {*}
token foo:a { … }
token foo:b { … }
Map.new:
'&foo' => &foo,
'$*dynamic' => my $ = 'default'
}

PHPSpec: How to handle data heavy mocks in spec?

I have a series of specs that are doing what I would like them to but I'm wondering if I'm overcomplicating things as my let function for some of them is rather large and cumbersome.
I have built specs for a series of classes that process responses from SQL or JSON API depending on the response. The specs are just checking the resultant object of the processes of each class. I've mocked the connection to return valid mock data for each type of request that the specs would trigger. I'm trying to think of a better way to provide this mock data than having rather large arrays and JSON strings just sitting in the spec files.
For example (simplified):
class CharacterProcessorSpec extends ObjectBehavior
{
public function let(AdapterInterface $adapter)
{
$characters = [
[
'name' => 'Timmy',
'class' => 'Fighter',
'level' => 1,
'race' => 'Elf',
'str' => 16,
'dex' => 14,
'con' => 18,
'int' => 10,
'wis' => 12,
'cha' => 11,
... // Rest of the minimally required fields
], [
... // Second character for processing multiple at once
]
];
$adapter->fetch(new CharacterRequest('Timmy'))->willReturn([$characters[0]]);
$adapter->fetch(new CharacterRequest('*'))->willReturn($characters);
$this->beConstructedWith($adapter);
}
public function it_should_build_requested_character_details()
{
$this->build('Timmy')->shouldReturnArrayOfCharacters();
}
public function it_should_build_all_character_details()
{
$this->buildAll()->shouldReturnArrayOfCharacters();
}
public function getMatchers()
{
return [
'returnArrayOfCharacters' => function($characters) {
foreach ($characters as $c) {
if (!$c instanceof Character) {
return false;
}
}
return true;
}
];
}
}
Is it worth me moving the arrays to a separate file and loading them in or is that a no no?
Note: The build functions are designed to not care if the adapter is for SQL or the API, it just converts the data into a consistant object. Therefore the spec does not actually define what the data is (in the example I have provided it's the same as an SQL response but I usually have the second entry formatted how the JSON response would be as it processes on a per entry basis).
Is it worth me moving the arrays to a separate file and loading them in or is that a no no?
No, is not worth this effort. You provide only data to constructor and with that it should be OK in direct definitions.

Option result in akka-http

I'm having an issue when trying to return an Option result from aka-http.
Basically it's a get that might have a 404.
pathPrefix("contacts" / Segment) { id =>
get {
contactService.getById(id).map {
case Some(c: ContactDto) => complete(OK -> toResource(c))
case None => complete(HttpResponse(NotFound))
}
}
}
Which gives me and error of:
[error] found : scala.concurrent.Future[akka.http.scaladsl.server.StandardRoute]
[error] required: akka.http.scaladsl.server.Route
[error] (which expands to) akka.http.scaladsl.server.RequestContext => scala.concurrent.Future[akka.http.scaladsl.server.RouteResult]
[error] contactService.getById(id).map {
Any help would be greatly appreciated.
The problem you are seeing here has to do with the fact that you are using a Future and not because of the Option. I'm going to assume that the call contactService.getById(id) returns a Future. As the result of any route within your routing tree needs to be a Route (short for RequestContext => Future[RouteResult]) and your Future is itself not a Route, then you need to make a small change to handle this situation. You should be able to use the onComplete directive in combination with your Future as follows:
pathPrefix("contacts" / Segment) { id =>
get {
val fut = contactService.getById(id)
onComplete(fut){
case util.Success(Some(c: ContactDto)) =>
complete(OK -> toResource(c))
case util.Success(None) =>
complete(HttpResponse(NotFound))
case util.Failure(ex) =>
complete(HttpResponse(InternalServerError))
}
}
}
This code now handles the 3 possible outcomes from the Future (success with a Some, success with a None and a failure), producing a Route for each of those cases. This should fix your problem.
#cmbaxter's answer is correct but if you're happy with the standard status codes for the three cases above (Ok, NotFound, InternalServerError) then you can simplify the code to just complete directly with your function that returns Future[Option[T]].
pathPrefix("contacts" / Segment) { id =>
get {
complete(contactService.getById(id).map(toResource))
}
}
That is assuming that toResource returns a type where a ToEntityMarshaller exists for the type returned by that function. Akka provdes the machinery for Future and Option so you just need to supply the T part. For example if you were returning json and using spray-json then you can define a JsonWriter[T] and the implicits in akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport will do the rest. See spray-json-support.
The map(toResource) may not actually be required, but I'm assuming that does additional conversion of ContactDto to some other type - if its just converting it to json or similar then you can drop it and use the in built marshalling support as described above.

Puppet and Apache: ${name} gives 'main' instead of class name

I'm trying to configure a few Apache virtual hosts with the puppetlabs/apache module. My issue is with the ${name} variable. I expected DocumentRoot to be set to /var/www/atoms.one, but instead it is set to /var/www/main.
What am I doing wrong?
My manifest:
class { apache: }
apache::vhost { 'atoms.one':
port => '80',
serveraliases => [ "*.${name}" ],
docroot => "/var/www/${name}",
directories => [
{ path => "/var/www/${name}", },
],
}
Inside a defined type's body, the special variable $name represents the name / title of the defined type instance. But you have not presented a defined-type body -- rather, you have presented a declaration of a defined-type instance, and one that appears at top scope, at that. The declaration does not create a scope for $name to mean anything different within than it does without.
I'm having trouble finding documentation for the meaning of $name outside the scope of a defined-type body, but I know from experience that inside a class it represents the class name. I suppose you are seeing the name of the top scope, for which "main" is a plausible value.
The bottom line is that $name does not provide the kind of shortcut you are trying to use it for. You could instead create a defined type wrapper of your own around apache::vhost, and do the shortcutting there. Alternatively, you could create your own variable to use instead of $name.