Aync shell script on Ansible to handle connection reset - ssh

Despite looking at many posts on SO and Ansible's doc, I'm still failing at understanding what Ansible is doing.
My scenario is following: I need to rename the network interface Ansible is connected over to control the remote and restore connection.
My first attempts revolved around something like this:
- name: Hot Rename Main Iface
become: true
shell:
cmd: |
ip link set oldiface down
ip link set oldiface name newiface
ip link set newiface up
async: 0
poll: 0
register: asynchotrename
- name: Wait For Reconnection
wait_for_connection:
delay: 15
timeout: 180
But whatever the values I would set for async or poll, Ansible would hang indefinitely. On the remote, I could see that the interface was brought down and then nothing. So obviously, nothing was done asynchronously, and as soon as the interface was down, the script could not continue. Probably, the process was killed by the termination of the ssh session.
Then I read that when doing this, Ansible had no time to properly spawn the process and disconnect. It needed the process to wait a bit before cutting the connection short. So I modified the playbook:
- name: Hot Rename Main Iface
become: true
shell:
cmd: |
sleep 5 # <-- Wait for Ansible disconnection
ip link set oldiface down
ip link set oldiface name newiface
ip link set newiface up
async: 0
poll: 0
register: asynchotrename
- name: Wait For Reconnection
wait_for_connection:
delay: 15
timeout: 180
But this did nothing. Ansible still hangs indefinitely, while nothing happens on the remote after the ip link down statement.
Then, I figured out that maybe I had to force send the subprocess to the background, even if this would mean not making use of Ansible's asynchronous feature and so not being able to possibly come back later to check if everything went fine (although of course if that's the case, chances are that the remote is unreachable anyway). I still kept the async and poll values, just to ensure that Ansible would disconnect properly, even if obviously it would do this only once the script had returned. At least, this would prevent some errors that I would have to mask with ignore_errors: true.
I may try without someday, to see if I can just remove these async and poll entirely. (Edit: Done, and it works. No errors to mask.)
The complete playbooks steps ended being (for those interrested, although I'm not going to explain in this post why I had to order the statements this way):
- name: Hot Rename Main Iface
become: true
shell:
cmd: |
(
sleep 5 && \
ip link set oldiface down && \
ip link set oldiface name newiface && \
ip link set newiface up && \
nmcli networking off && \
sleep 1 && \
nmcli networking on && \
sleep 5 && \
systemctl restart sshd
)&
async: 90
poll: 0
register: asynchotrename
- name: Wait For Reconnection
wait_for_connection:
delay: 15
timeout: 180
But then I read that if I use poll: 0, I have to manually cleanup the async job cache. So I added this task:
- name: Cleanup Leftover Async Files
async_status:
jid: "{{ asynchotrename.ansible_job_id }}"
mode: cleanup
result: FAILED! => {"ansible_job_id": "603790343886.29503", "changed": false, "finished": 1, "msg": "could not find job", "started": 1}
I'm totally puzzled. Ansible doesn't even seem to consider the task as an async job.
How to spawn an asynchronous task in Ansible??

During research regarding Ansible doesn't return job_id for async task I've setup a small test on a RHEL 7.9.9 system with Ansible 2.9.25 and Python 2.7.5 which seems to be working so far.
- name: Start async job
systemd:
name: network
state: restarted
async: 60 # 1min
poll: 0
register: network_restart
- name: Wait shortly before check
pause:
seconds: 5
- name: Check async status
async_status:
jid: "{{ network_restart.ansible_job_id }}"
changed_when: false
register: job_result
until: job_result.finished
retries: 6
delay: 10
Because of your comment
Ansible had no time to properly spawn the process and disconnect. It needed the process to wait a bit before cutting the connection short.
and the documentation of Run tasks concurrently: poll = 0
If you want to run multiple tasks in a playbook concurrently, use async with poll set to 0. When you set poll: 0, Ansible starts the task and immediately moves on to the next task without waiting for a result.
I've included the
- name: Wait shortly before check
pause:
seconds: 5
resulting into an execution of
TASK [Start async job] *****************************************************************************************************************************************
changed: [test1.example.com]
Saturday 06 November 2021 17:20:43 +0100 (0:00:02.287) 0:00:10.228 *****
Pausing for 5 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
TASK [Wait shortly] ********************************************************************************************************************************************
ok: [test1.example.com]
Saturday 06 November 2021 17:20:48 +0100 (0:00:05.057) 0:00:15.285 *****
TASK [Check async status] **************************************************************************************************************************************
ok: [test1.example.com]
As you can see the pausing message came almost instantly and seconds before task name message.
On the test host it is seen that the network interface restarted
sudo systemctl status network
● network.service - LSB: Bring up/down networking
Loaded: loaded (/etc/rc.d/init.d/network; bad; vendor preset: disabled)
Active: active (exited) since Sat 2021-11-06 17:20:46 CET; 874ms ago
Regarding
... as soon as the interface was down, the script could not continue. Probably, the process was killed by the termination of the ssh session.
I am too renaming interfaces, frequently during baseline setups like
- name: Make sure main network interface is named correctly
shell:
cmd: nmcli conn mod "ens192" connection.id "eth0"
- name: Gather current interface configuration
shell:
cmd: nmcli conn show eth0
register: nmcli_conn
- name: STDOUT nmcli_conn
debug:
msg: "{{ nmcli_conn.stdout_lines }}"
I have only to make sure before that the interfaces can be managed by NetworkManager. An asynchronous task isn't necessary in my setups to have a reliable restart of the network interfaces, also not for restarting sshd.
By using NetworkManager more advanced task are possilbe later like
- name: Configure DNS resolver
nmcli:
conn_name: eth0
type: ethernet
dns4_search:
- dns.example.com
state: present

Related

Can we send data to wazuh-indexer using filebeat and without agent in Wazuh?

I am trying to send data from filebeat to wazuh-indexer directly but I get connection errors between filebeat and elasticsearch. Following is my filebeat configuration:
filebeat.inputs:
- input_type: log
paths:
- /home/siem/first4.log
enable: true
output.elasticsearch:
hosts: ["192.168.0.123:9200"]
protocol: https
index: "test"
username: admin
password: admin
ssl.certificate_authorities:
- /etc/filebeat/certs/root-ca.pem
ssl.certificate: "/etc/filebeat/certs/filebeat-1.pem"
ssl.key: "/etc/filebeat/certs/filebeat-1-key.pem"
setup.template.json.enabled: false
setup.ilm.overwrite: true
setup.ilm.enabled: false
setup.template.name: false
setup.template.pattern: false
#setup.template.json.path: '/etc/filebeat/wazuh-template.json'
#setup.template.json.name: 'wazuh'
#filebeat.modules:
# - module: wazuh
# alerts:
# enabled: true
# archives:
# enabled: false
Following is the error:
2023-01-30T09:29:18.634Z ERROR [publisher_pipeline_output] pipeline/output.go:154 Failed to connect to backoff(elasticsearch(https://192.168.0.123:9200)): Get "https://192.168.0.123:9200": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
2023-01-30T09:29:18.635Z INFO [publisher_pipeline_output] pipeline/output.go:145 Attempting to reconnect to backoff(elasticsearch(https://192.168.0.123:9200)) with 1 reconnect attempt(s)
2023-01-30T09:29:18.635Z INFO [publisher] pipeline/retry.go:219 retryer: send unwait signal to consumer
2023-01-30T09:29:18.635Z INFO [publisher] pipeline/retry.go:223 done
2023-01-30T09:29:46.177Z INFO [monitoring] log/log.go:145 Non-zero metrics in the last 30s
Can anyone tell what mistake am I doing?
Yes, you could send logs directly using Filebeat without a Wazuh agent but that way you won't benefit from the Wazuh analysis engine.
With your current configuration, the logs will be ingested under filebeat-<version>-<date>. Make sure to create an index pattern for these events.
As your logs indicate, there's a connectivity issue between Filebeat and the Wazuh indexer. To diagnose the problem:
Try running the following call to make sure you can reach the Wazuh indexer:
curl -k -u admin:admin https://192.168.0.123:9200
Run a Filebeat test output:
filebeat test output

Ansible: How to use examples from the documentation?

I'm starting to learn Ansible and for this I copy and paste examples from the documentation. For example this one
- name: Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents
ansible.builtin.uri:
url: http://www.example.com
return_content: yes
register: this
failed_when: "'AWESOME' not in this.content"
which I've found in uri module documentation.
Every single time I do this, whatever the module I get:
ERROR! 'ansible.builtin.uri' is not a valid attribute for a Play
The error appears to have been in '/home/alfrerra/test2.yml': line 1, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents
^ here
I have only 2 playbooks that only ping successfully:
-
name: ping localhost
hosts: localhost
tasks:
- name: ping test
ping
and
---
- name: ping localhost
hosts: localhost
tasks:
- name: ping test
ping
So I adapted the example to match these 2 examples, but to no avail so far.
I'm sure it's nothing much but it's driving me crazy.
As already mentioned within the comments, the documentation are to show how to use certain parameter of certain modules within a single task. They are not mentioned to work in a copy and paste manner by itself or only.
You may have a look into the following minimal example playbook
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Check that a page returns a status 200 and fail if the word 'iana.org' is not in the page contents
uri:
url: http://www.example.com
return_content: yes
environment:
http_proxy: "localhost:3128"
https_proxy: "localhost:3128"
register: this
failed_when: "'iana.org' not in this.content"
- name: Show content
debug:
msg: "{{ this.content }}"
resulting into the output of the page content.
Please take note that the web page of the example domain is connected and delivers a valid result, but it does not contain the word AWESOME. To address this the string to lookup was changed to the page owner iana.org and to not let the task fail. Furthermore and since working behind a proxy it was necessary to address the proxy configuration and which you probably need to remove again.
Your first example is a single task and failing therefore with ERROR! ... not a valid attribute for a Play. Your second and
third examples are playbooks with a single task and therefore executing.
Documentation
Module format and documentation - EXAMPLES block
... show users how your module works with real-world examples in multi-line plain-text YAML format. The best examples are ready for the user to copy and paste into a playbook.
Ansible playbooks
failed_when must be a comparison or a boolean.
example :
- name: Check that a page returns AWESOME is not in the page contents
ansible.builtin.uri:
url: http://www.example.com
return_content: yes
register: this
failed_when: this.rc == 0;
It will execute the task only if the return value is equal to 0

Icinga2 event plugin command starting a rundeck job via api

i made myself a test environment in icinga2 with a tomcat server. I would like to combine the two softwares rundeck and icinga. My idea is to start a rundeck job, when icinga detects a problem. In my case I have a tomcat server, where i fill up the swap memory, which should start the rundeck job to clear the swap.
I am using the Icinga2 Director for managing. I created an event plugin command, which should execute the rundeck api command as a script, called "rundeckapi". It looks like this:
#/usr/lib64/nagios/plugins/rundeckapi
#!/bin/bash
curl --location --request POST 'rundeck-server:4440/api/38/job/9f04657a-eaab-4e79-a5f3-00d3053f6cb0/run' \
--header 'X-Rundeck-Auth-Token: GuaoD6PtH5BhobhE3bAPo4mGyfByjNya' \
--header 'Content-Type: application/json' \
--header 'Cookie: JSESSIONID=node01tz8yvp4gjkly8kpj18h8u5x42.node0' \
--data-raw '{
"options": {
"IP":"192.168.4.13"
}
}'
(I also tried to just paste the command in the command field in the director, but this didn't work either.)
I placed it in the /usr/lib64/nagios/plugins/ directory and set the configuration in icinga for the command as following:
#zones.d/director-global/command.conf
object EventCommand "SWAP clear" {
import "plugin-event-command"
command = [ PluginDir + "/rundeckapi" ]
}
The service template looks like this:
#zones.d/master/service_templates.conf
template Service "SWAP" {
check_command = "swap"
max_check_attempts= "5"
check_interval = 1m
retry_interval = 15s
check_timeout = 10s
enable_notifications = true
enable_active_checks = true
enable_passive_checks = true
enable_event_handler = true
enable_flapping = true
enable_perfdata = true
event_command = "SWAP clear"
command_endpoint = host_name
}
Then I added the service to the host.
I enabled the debug mode and started to fill the SWAP and watched at the debug.log, with tail -f /var/log/icinga2/debug.log | grep 'event handler' and found this:
notice/Checkable: Executing event handler 'SWAP clear' for checkable 'centos_tomcat_3!SWAP'
The centos_tomcat_3 is the host for testing. IT seems like the event handler is executing the the script, but when I look at the rundeck server, i can't find a running job. When i start the rundeckapi script manually it works and i can see the job on rundeck.
I already read the documentation from icinga, but i didn't help.
I would be very thankful if someone could help me.
Thanks in advance.
Define the plugin as an event handler and assign it to the host.
I tested using this docker environment modified with Rundeck official image + a NGINX host:
version: '2'
services:
icinga2:
#image: jordan/icinga2
build:
context: ./
dockerfile: Dockerfile
restart: on-failure:5
# Set your hostname to the FQDN under which your
# sattelites will reach this container
hostname: icinga2
env_file:
- secrets_sql.env
environment:
- ICINGA2_FEATURE_GRAPHITE=1
# Important:
# keep the hostname graphite the same as
# the name of the graphite docker-container
- ICINGA2_FEATURE_GRAPHITE_HOST=graphite
- ICINGA2_FEATURE_GRAPHITE_PORT=2003
- ICINGA2_FEATURE_GRAPHITE_URL=http://graphite
# - ICINGA2_FEATURE_GRAPHITE_SEND_THRESHOLDS=true
# - ICINGA2_FEATURE_GRAPHITE_SEND_METADATA=false
- ICINGAWEB2_ADMIN_USER=admin
- ICINGAWEB2_ADMIN_PASS=admin
#- ICINGA2_USER_FULLNAME=Icinga2 Docker Monitoring Instance
- DEFAULT_MYSQL_HOST=mysql
volumes:
- ./data/icinga/cache:/var/cache/icinga2
- ./data/icinga/certs:/etc/apache2/ssl
- ./data/icinga/etc/icinga2:/etc/icinga2
- ./data/icinga/etc/icingaweb2:/etc/icingaweb2
- ./data/icinga/lib/icinga:/var/lib/icinga2
- ./data/icinga/lib/php/sessions:/var/lib/php/sessions
- ./data/icinga/log/apache2:/var/log/apache2
- ./data/icinga/log/icinga2:/var/log/icinga2
- ./data/icinga/log/icingaweb2:/var/log/icingaweb2
- ./data/icinga/log/mysql:/var/log/mysql
- ./data/icinga/spool:/var/spool/icinga2
# Sending e-mail
# See: https://github.com/jjethwa/icinga2#sending-notification-mails
# If you want to enable outbound e-mail, edit the file mstmp/msmtprc
# and configure to your corresponding mail setup. The default is a
# Gmail example but msmtp can be used for any MTA configuration.
# Change the aliases in msmtp/aliases to your recipients.
# Then uncomment the rows below
# - ./msmtp/msmtprc:/etc/msmtprc:ro
# - ./msmtp/aliases:/etc/aliases:ro
ports:
- "80:80"
- "443:443"
- "5665:5665"
graphite:
image: graphiteapp/graphite-statsd:latest
container_name: graphite
restart: on-failure:5
hostname: graphite
volumes:
- ./data/graphite/conf:/opt/graphite/conf
- ./data/graphite/storage:/opt/graphite/storage
- ./data/graphite/log/graphite:/var/log/graphite
- ./data/graphite/log/carbon:/var/log/carbon
mysql:
image: mariadb
container_name: mysql
env_file:
- secrets_sql.env
volumes:
- ./data/mysql/data:/var/lib/mysql
# If you have previously used the container's internal DB use:
#- ./data/icinga/lib/mysql:/var/lib/mysql
rundeck:
image: rundeck/rundeck:3.3.12
hostname: rundeck
ports:
- '4440:4440'
nginx:
image: nginx:alpine
hostname: nginx
ports:
- '81:80'
Rundeck side:
To access Rundeck open a new tab in your browser using the http://localhost:4440 web address. You can access with user: admin and password: admin.
Create a new project and create a new job, I created the following one, you can import it to your instance:
- defaultTab: nodes
description: ''
executionEnabled: true
id: c3e0860c-8f69-42f9-94b9-197d0706a915
loglevel: INFO
name: RestoreNGINX
nodeFilterEditable: false
options:
- name: opt1
plugins:
ExecutionLifecycle: null
scheduleEnabled: true
sequence:
commands:
- exec: echo "hello ${option.opt1}"
keepgoing: false
strategy: node-first
uuid: c3e0860c-8f69-42f9-94b9-197d0706a915
Now go to the User Icon (up to right) > Profile, now click on the + icon ("User API Tokens" section) and save the API key string, useful to create the API call script from the Icinga2 container.
Go to the Activity page (left menu) and click on the "Auto Refresh" checkbox.
Incinga2 side:
You can enter Icinga 2 by opening a new tab in your browser using the http://localhost URL, I defined username: admin and password: admin in the docker-compose file.
Add the following script as a command at /usr/lib/nagios/plugins path with the following content (it's a curl api call like your scenario, the API key is the same generated in the third step from "Rundeck side" section of this step-by-step):
#!/bin/bash
curl --location --request POST 'rundeck:4440/api/38/job/c3e0860c-8f69-42f9-94b9-197d0706a915/run' --header 'X-Rundeck-Auth-Token: Zf41wIybwzYhbKD6PrXn01ZMsV2aT8BR' --header 'Content-Type: application/json' --data-raw '{ "options": { "opt1": "world" } }'
Also make the script executable: chmod +x /usr/lib/nagios/plugin/restorenginx
In the Icinga2 browser tab, go to the Icinga Director (Left Menu) > Commands. On the "Command Type" list select "Event Plugin Command", on the "Command Name" textbox type "restorenginx" and on the "Command" textbox put the full path of the script (/usr/lib/nagios/plugins/restorenginx). Then click on the "Store" button (bottom) and now click on Deploy (up).
Check how looks.
This is the config preview (at zones.d/director-global/commands.conf):
object EventCommand "restorenginx" {
import "plugin-event-command"
command = [ "/usr/lib/nagios/plugins/restorenginx" ]
}
Now, create the host template (In my example I'm using an Nginx container to monitoring), go to Icinga Director (Left Menu) > Hosts, and select "Host Templates". Then click on the + Add link (up). On the Name type the host template name, I used "nginxSERVICE", on the "check command" textbox put the command to check the host alive (I used "ping"). Now in the Event command textbox select the Command created in the previous step.
Check how looks.
Now it's time to create the host (based on the previous steps template). Go to Icinga Direcrector (Left Menu) > Hosts and select "Host". Then click on the + Add link (up). On the hostname type the server hostname (nginx, defined on the docker-compose file), In "Imports" select the template is created in the previous step ("nginxSERVICE"), type anything on the "Display name" textbox, and in the "Host address" add the Nginx container IP. Click on the "Store" button" and then on the "Deploy" link at the top.
Check how looks.
To enable the Event Hander on the host, go to the "Overview" (Left menu) > Hosts, select "NGINX", scroll down on the right section and enable "Event Handler" on the "Feature Commands" section.
Check how looks.
Nginx side (it's time to test the script):
Stop the container and go to the Rundeck Activity page browser tab, you'll see the job launched by the Icinga2 monitoring tool.

Kubernetes dashboard authentication on atomic host

I am a total newbie in terms of kubernetes/atomic host, so my question may be really trivial or well discussed already - but unfortunately i couldn't find any clues how to achieve my goal - that's why i am here.
I have set up kubernetes cluster on atomic hosts (right now i have just one master and one node). I am working in the cloud network, on the virtual machines.
[root#master ~]# kubectl get node
NAME STATUS AGE
192.168.2.3 Ready 9d
After a lot of fuss i managed to set up the kubernetes dashboard UI on my master.
[root#master ~]# kubectl describe pod --namespace=kube-system
Name: kubernetes-dashboard-3791223240-8jvs8
Namespace: kube-system
Node: 192.168.2.3/192.168.2.3
Start Time: Thu, 07 Sep 2017 10:37:31 +0200
Labels: k8s-app=kubernetes-dashboard
pod-template-hash=3791223240
Status: Running
IP: 172.16.43.2
Controllers: ReplicaSet/kubernetes-dashboard-3791223240
Containers:
kubernetes-dashboard:
Container ID: docker://8fddde282e41d25c59f51a5a4687c73e79e37828c4f7e960c1bf4a612966420b
Image: gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.3
Image ID: docker-pullable://gcr.io/google_containers/kubernetes-dashboard-amd64#sha256:2c4421ed80358a0ee97b44357b6cd6dc09be6ccc27dfe9d50c9bfc39a760e5fe
Port: 9090/TCP
Args:
--apiserver-host=http://192.168.2.2:8080
Limits:
cpu: 100m
memory: 300Mi
Requests:
cpu: 100m
memory: 100Mi
State: Running
Started: Fri, 08 Sep 2017 10:54:46 +0200
Last State: Terminated
Reason: Error
Exit Code: 2
Started: Thu, 07 Sep 2017 10:37:32 +0200
Finished: Fri, 08 Sep 2017 10:54:44 +0200
Ready: True
Restart Count: 1
Liveness: http-get http://:9090/ delay=30s timeout=30s period=10s #success=1 #failure=3
Volume Mounts: <none>
Environment Variables: <none>
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
No volumes.
QoS Class: Burstable
Tolerations: <none>
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1d 32m 3 {kubelet 192.168.2.3} Warning MissingClusterDNS kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.
1d 32m 2 {kubelet 192.168.2.3} spec.containers{kubernetes-dashboard} Normal Pulled Container image "gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.3" already present on machine
32m 32m 1 {kubelet 192.168.2.3} spec.containers{kubernetes-dashboard} Normal Created Created container with docker id 8fddde282e41; Security:[seccomp=unconfined]
32m 32m 1 {kubelet 192.168.2.3} spec.containers{kubernetes-dashboard} Normal Started Started container with docker id 8fddde282e41
also
[root#master ~]# kubectl cluster-info
Kubernetes master is running at http://localhost:8080
kubernetes-dashboard is running at http://localhost:8080/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard
Now, when i tried connecting to the dashboard (i tried accessing the dashbord via the browser on windows virtual machine in the same cloud network) using the adress:
https://192.168.218.2:6443/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard
I am getting the "unauthorized". I believe it proves that the dashboard is indeed running under this address, but i need to set up some way of accessing it?
What i want to achieve in the long term:
i want to enable connecting to the dashboard using the login/password (later, when i learn a bit more, i will think about authenticating by certs or somehting more safe than password) from the outside of the cloud network. For now, connecting to the dashboard at all would do.
I know there are threads about authenticating, but most of them are mentioning something like:
Basic authentication is enabled by passing the
--basic-auth-file=SOMEFILE option to API server
And this is the part i cannot cope with - i have no idea how to pass options to API server.
On the atomic host the api-server,kube-controller-manager and kube-scheduler are running in containers, so I get into the api-server container with command:
docker exec -it kube-apiserver.service bash
I saw few times that i should edit .json file in /etc/kubernetes/manifest directory, but unfortunately there is no such file (or even a directory).
I apologize if my problem is too trivial or not described well enough, but im new to (both) IT world and the stackoverflow.
I would love to provide more info, but I am afraid I would end up including lots of useless information, so i decided to wait for your instructions in that regard.
Check out wiki pages of kubernetes dashboard they describe how to get access to dashboard and how to authenticate to it. For quick access you can run:
kubectl proxy
And then go to following address:
http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy
You'll see two options, one of them is uploading your ~/.kube/config file and the other one is using a token. You can get a token by running following command:
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep service-account-token | head -n 1 | awk '{print $1}')
Now just copy and paste the long token string into dashboard prompt and you're done.

Nagios host notifications not sending via email or logging

I am re-doing our nagios infrastructure with puppet but I am currently stopped at a seemingly simple problem (most likely a config issue).
Using puppet, I spit out some basic nagios config files on disk. Nagios reloads fine and everything looks okay in the UI but, when I mark a host down, it does not send a notification.
nagios.log shows:
[1470699491] EXTERNAL COMMAND:
PROCESS_HOST_CHECK_RESULT;divcont01;1;test notification
[1470699491] PASSIVE HOST CHECK: divcont01;1;test notification
[1470699491] HOST ALERT: divcont01;DOWN;HARD;1;test notification
In production (where I have changed nothing), I see in nagios.log (after marking a host down in ui):
[1470678186] EXTERNAL COMMAND:
PROCESS_HOST_CHECK_RESULT;PALTL12;1;test ey
[1470678187] PASSIVE HOST CHECK: PALTL12;1;test ey
[1470678187] HOST ALERT: PALTL12;DOWN;HARD;1;test ey
[1470678187] HOST NOTIFICATION:
pal_infra;PALTL12;DOWN;host-notify-by-pom;test ey
[1470678187] HOST NOTIFICATION:
pal_infra;PALTL12;DOWN;host-notify-by-email;test ey
[1470678192] HOST ALERT: PALTL12;UP;HARD;1;PING OK - Packet loss = 0%,
RTA = 0.81 ms
[1470678192] HOST NOTIFICATION:
pal_infra;PALTL12;UP;host-notify-by-pom;PING OK - Packet loss = 0%,
RTA = 0.81 ms
[1470678192] HOST NOTIFICATION:
pal_infra;PALTL12;UP;host-notify-by-email;PING OK - Packet loss = 0%,
RTA = 0.81 ms
As seen in the logs, there is a HOST NOTIFICATION logged and sent directly after the HOST ALERT in prod. I have been exhaustively comparing config files today and I cannot find a reason why the new config stops short of the notification.
I have verified that notifications are enabled at the top level. I have verified that email can be sent from this box (though, I am using the logs to verify functionality, not email). I have also tried multiple other google suggestions (and will continue my search too).
Relevant config details below. Please pardon the verbosity of my configuration and lackluster stack-overflow formatting. Thank you in advance.
hosts/divcont01.cfg:
define host {
address snip
host_name divcont01
use generic-host-puppetized
}
host-templates/generic-host-puppetized.cfg:
define host {
check_command check-host-alive
check_interval 1
contact_groups generic-contactgroup
checks_enabled 1
event_handler_enabled 0
flap_detection_enabled 0
name generic-host-puppetized
hostgroups +generic-host-puppetized
max_check_attempts 4
notification_interval 4
notification_options d,u,r
notification_period 24x7
notifications_enabled 1
process_perf_data 0
register 0
retain_nonstatus_information 1
retain_status_information 1
}
hostgroups/generic-host-puppetized.cfg:
define hostgroup {
hostgroup_name generic-host-puppetized
}
contactgroups/generic-contactgroup.cfg
define contactgroup {
contactgroup_name generic-contactgroup
members generic-puppetized-contact
}
contacts/generic-puppetized-contact.cfg
define contact {
use generic-contact
contact_name generic-puppetized-contact
email <my email>
}
objects/templates.cfg (generic-contact config only)
define contact{
use my email
name generic-contact ; The name of this contact template
service_notification_period 24x7 ; service notifications can be sent anytime
host_notification_period 24x7 ; host notifications can be sent anytime
host_notification_commands generic-puppetized-contact-host-notify-by-email-low
service_notification_commands notify-by-email,service-notify-by-pom
service_notification_options u,c,r,f ; send notifications for all service states, flapping events, and scheduled downtime events
host_notification_options d,r,f ; send notifications for all host states, flapping events, and scheduled downtime events
register 0 ; DONT REGISTER THIS DEFINITION - ITS NOT A REAL CONTACT, JUST A TEMPLATE!
}
commands/generic-puppetized-contact-host-notify-by-email-low.cfg:
define command {
command_line /etc/nagios/global/scripts/nagios-mailx.sh -t my email -s "** notification Host Alert: hostname is hoststate **" -m "***** Nagios ***** Notification Type: notification type Host: host State: hoststate Address: address Info: output Date/Time: date"
command_name generic-puppetized-contact-host-notify-by-email-low
}
Figured it out...I was building my system within another pre-existing system (dangerous, I know) and my contacts were actually pointing to a generic-contact that had its notifications disabled.
Whoops :)