Ansible make static dns record in Microsoft DNS

If you have a heterogenous datacenter with GNU/Linux and Microsoft servers, you might run into this problem.

When you want to create dynamic dns records programmatically, you can use the nsupdate module. It doesn’t work with gsstsig auth which is the only way the AD DNS works for “secure updates” so I previously wrote a wrapper for doing so. However, when you want to create static records, it’s a little bit harder. With the help of my Windows teammates, I now have a working solution for making static records in AD DNS, complete with the reverse PTR records.

Dependencies

  • A Windows Server 2016 client with RSAT with DNS installed. Apparently regular RSAT isn’t enough. I don’t know what’s involved in installing the right components, so if anybody could share your notes for how that works, comment at the end here.
  • Winrm with kerberos auth enabled

The tricky part here was learning how to elevate privileges once getting to the Windows client.

Playbook

---
- name: playbook that creates static DNS static records, both A and PTR, through the windows utility box
  hosts: localhost
  vars_files:
  - /etc/ansible/creds/windows_service_account.yml

  tasks:

  - add_host:
      group: rsat
      name: "rsat01.ad.example.com"
      ansible_connection: winrm
      ansible_winrm_server_cert_validation: ignore
      ansible_user: "{{ win_ansible_user }}"
      ansible_ssh_pass: "{{ win_ansible_ssh_pass }}"
      ansible_port: "5986"
      ansible_win_rm_scheme: https
      ansible_winrm_transport: kerberos
      ansible_host: rsat01.ad.example.com
    changed_when: false
    no_log: true

  - set_fact:
      ansible_winrm_server_cert_validation: ignore

  - name: make static a and ptr records, ad
    win_shell: Add-DnsServerResourceRecord -ComputerName ad.example.com -ZoneName ad.example.com -A -Name newhost1 -IPv4Address 10.234.56.78 -CreatePtr
    become: yes
    become_method: runas
    become_user: "{{ win_ansible_user }}"
    delegate_to: rsat01.ad.example.com
    vars:
      ansible_winrm_transport: kerberos

...

References

  1. How to make ansible connect to windows host behind linux jump server – ExceptionsHub
  2. Add-DnsServerResourceRecord [microsoft.com]
  3. Understanding Privilege Escalation — Ansible Documentation

ansible use jump box

If you need to connect through an intermediate jump box, or bastion server, here’s how you configure the inventory file:

[other-lan]
c7-prod-app-01 
[other-lan:vars]
ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p -q ansible_user@jumpbox.otherlan.example.com"'

If the jump box can resolve the target name as is, you don’t need to specify the IP address. However, you can also force a specific IP address.

c7-prod-app-01 ansible_host=10.300.15.3

References

Weblinks

Shamelessly ripped from Ansible with a bastion host / jump box? [stackoverflow.com]

Ansible disable service fails if service is not defined

If you are trying to disable and stop a system service, and it fails out when the service is not defined, you probably just want to continue.

- name: disable service
  service:
    name: chronyd
    enabled: no
    state: stopped
  register: disable_service
  failed_when: "disable_service is failed and ('Could not find the requested service' not in disable_service.msg)"

Credit goes to Rebekah Hayes.

Ansible check if list is in list

Here’s my hack for how to check if a list’s items are all in another list.

  - name: learn current mounts
    shell: mount | grep -iE "cifs|nfs" | grep -viE '^\s*#|pipefs|proc\/fs\/nfsd' | awk '{print $3}' | sort
    args:
      warn: no
    register: mounts

  - name: learn requested mounts
    shell: grep -iE "cifs|nfs" /etc/fstab | grep -viE '^\s*#|pipefs|proc\/fs\/nfsd' | awk '{print $2}' | sort
    args:
      warn: no
    register: fstab_entries

  - name: fail when fstab_entries not in mounts
    debug:
      msg: "{{ item }}"
    failed_when:
    - 'item not in mounts.stdout_lines'
    loop: "{{ fstab_entries.stdout_lines | flatten(levels=1) }}"

I am just using a debug task that loops over all the items of the possibly shorter list, with a condition to fail when the item is not in the longer list.

Ansible find first accessible proxy and use it

If you need to find the first available http proxy and use it for a process, you can use a python snippet to discover it and use it.

https://gitlab.com/bgstack15/former-gists/tree/master/get_first_open_port.py

vars:
  http_proxies:
  - 192.168.1.5:3128
  - proxy5.internal.example.com:3128

tasks:

  - name: learn which proxy to use
    script: get_first_open_port.py {{ http_proxies | join( " " ) }}
    changed_when: false
    register: open_ports

  - set_fact:
      http_proxy: "{{ open_ports.stdout_lines[0] }}"
    when:
    - 'open_ports.stdout | length > 0'
    failed_when:
    - 'open_ports.stdout | length = 0'

  - name: use http_proxy environment variable
    script: script_needing_internet.sh -i {{ inputvar }}
    environment:
      http_proxy: "http://{{ http_proxy | default(omit) }}"

The sole output is the first hostname and port available.

#!/usr/bin/python2
# Filename: get_first_open_port.py
# Location: /etc/ansible/roles/install_sccm/files/
# Author: bgstack15
# Startdate: 2018-10-02 10:13
# Title: Script that Gets the First Open Port
# Purpose: Return to standard output the first valid host and port to use as a http proxy
# Project: projects derived from ansible role certreq
# History:
# Usage:
#    in ansible
# Reference:
#    https://stackoverflow.com/questions/19196105/python-how-to-check-if-a-network-port-is-open-on-linux
#    string split https://stackoverflow.com/questions/6670290/split-string-into-different-variables-instead-of-array-in-python
# Improve:
# Documentation:

import socket, sys
from contextlib import closing

GFOP_VERSION="2018-10-02a"

def check_socket(host, port):
   with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
      sock.settimeout(2)
      if sock.connect_ex((host,port)) == 0:
         print host + ":" + str(port)
         return True
   return False


x = 0
for myarg in sys.argv:

   # show version
   if myarg == "--version" or myarg == "-V":
      print(sys.argv[0]+" "+GFOP_VERSION)
      sys.exit(0)

   x = x + 1
   # skip the script $0 name itself
   if x > 1:
      # split on the colon
      host, port = myarg.split(":",2)
      # short-circuit upon first successful one
      if check_socket(host,int(port)):
         sys.exit(0)

Control ansible verbosity and silence with tags

I wonder if I have stumbled upon a use case that other haven’t yet.

I just learned about how to use callback plugins in ansible, which are generally responsible for output.

I found one named “selective” which means that it will show the output for only the tasks with a tag titled “print_action.” My goals included having mundane tasks be silent, not whitelisting the important tasks. Plus, it shows every task anyway if you do a -v for verbose.

Also, using callbacks took me some effort. To even allow different callback plugins, you have to add the names you want available to the [defaults] callback_whitelist variable in ansible.cfg, which is comma-space-delimited. And place the plugin in of the directories specified by callback_plugins, normally /usr/share/ansible_plugins/callback_plugins.

Next, to actually use it for a play, define ANSIBLE_STDOUT_CALLBACK with the name of the plugin to use.

So:

$ cat ansible.cfg
[default]
... OUTPUT TRUNCATED ...
callback_whitelist = short
$ ANSIBLE_STDOUT_CALLBACK=short ansible-playbook myplaybook.yml

I present to you today my customized callback plugin! It operates just like the default callback, but with two modifications, both dependent on tags of the tasks.

  • tag: verbose will cause the output to always be like the -v flag
  • tag: silent will completely hide the task output when successful. Failures and skips appear like normal.

My plays tend to have a lot of minor tasks like deploying assistant scripts or cleaning up temp files, and I don’t want tons of scrollback in my terminal just because 5 different temp files were removed (or not) at the end of a play across 150 systems.

Verify ansible vault password

# prepare vault password file
printf 'Vault password: ' ; read -se VAULT_PASS ; printf '\n' ;
echo "${VAULT_PASS}" > "${PWFILE}"
# fail out if password is incorrect
! ansible-vault view --vault-password-file "${PWFILE}" "${VAULTFILE}" 1>/dev/null && exit 1

You can use shell to read in the password and save it to a file. Just remember to clean it up at the end!

I like to do this right before a shell loop that calls ansible with vaulted values multiple times, so I’m not prompted multiple times to enter the password.

ansible control hosts entries

Somebody should make a module of this. In the meantime, here’s a simple hosts entries task in ansible.

- name: hosts entries
  lineinfile:
    path: "{{ etc_hosts_file | default('/etc/hosts') }}"
    regexp: '^\s*{{ item.ip }}.*'
    line: "{{ item.ip }}   {{ item.hostnames | sort() | join(' ') }}"
    backup: yes
  with_items:
  - "{{ hosts_entries }}"

Use a list like so:

hosts_entries:
- { ip: '1.2.3.4', hostnames: ['example.com','www.example.com'] }
- { ip: '5.6.7.8', hostnames: ['ldap.example.com'] }

Playbook that Converts Local to AD Users

If you like removing local users in favor of the domain users, check out how to do that in shell at my post Convert Local to AD Users.

If you want to do it at scale, you can wrap it with a bit of ansible. Check out the full thing with syntax highlighting on gitlab: https://gitlab.com/bgstack15/former-gists/blob/master/cladu.yml/cladu.yml

---
# Only use one thisuser at a time, for the fail/changed logic to work correctly!
# usage: ansible-playbook -l targethost1 /etc/ansible/books/stable/cladu.yml -v -e 'thisuser=joneill'

- name: book that runs cladu
  hosts: all
  become: yes
  become_user: root
  become_method: sudo
  tasks:

  - name: copy in rpm
    copy:
      src: /etc/ansible/files/bgscripts-core-1.3-9.noarch.rpm
      dest: /tmp/
      mode: 0644

  - shell: rpm -U --nodeps /tmp/bgscripts-core-1.3-9.noarch.rpm
    args:
      warn: no
    register: this_rpm
    failed_when:
    - 'not ("is already installed" in this_rpm.stdout or "is already installed" in this_rpm.stderr or this_rpm.rc == 0)'
    changed_when:
    - 'not ("is already installed" in this_rpm.stdout or "is already installed" in this_rpm.stderr)'

  - shell: /usr/share/bgscripts/work/cladu.sh -r -g '{{ thisuser }}'
    args:
      warn: no
    register: this_shell
    changed_when:
    - 'not ("Skipped" in this_shell.stdout or "Failed" in this_shell.stdout)'
    failed_when:
    - '"Failed" in this_shell.stdout'

Playbook that resets user password

As an admin, it’s my job to reset user passwords who still use local accounts. I’m working on converting users to domain accounts, but in the mean time, here’s my little book.
Check it out with context highlighting at https://gitlab.com/bgstack15/former-gists/blob/master/reset-password.yml/reset-password.yml

---
# Dependencies: pwhashgen.py from bgscripts-core, installed on ansible server
- name: book that resets password for thisuser
  hosts: all
  become: yes
  become_user: root
  become_method: sudo
  tasks:

  - name: generate pw hash
    shell: /usr/share/bgscripts/py/pwhashgen.py "{{ thispw | default('TEMP_PASSWORD_HERE') }}"
    register: thispw
    delegate_to: localhost
    changed_when: false
    run_once: true

  - user:
      name: "{{ thisuser }}"
      password: "{{ thispw.stdout }}"

  - shell: passwd -e "{{ thisuser }}"
    args:
      warn: no