Using avahi / mDNS in a Vagrant project

I’m working on a project, with Vagrant and Ansible, to deploy a MongoDB Cluster. I needed name resolution to function between the VirtualBox VMs I was creating and didn’t want to hardcode anything in the hosts file. The solution I decided on uses avahi which essentially works like Apple Bonjour. As this solution has broader applications than just a MongoDB cluster I thought I’d share it here. The script is idempotent and is for Redhat/CentOS systems.

#!/bin/sh
set -u;
 
function is_installed() {
        PACKAGE="$1";
        yum list installed "$PACKAGE" >/dev/null ;
        return $?
}
 
is_installed epel-release || sudo yum install -y epel-release;
is_installed avahi-dnsconfd || sudo yum install -y avahi-dnsconfd;
is_installed avahi-tools || sudo yum install -y avahi-tools;
is_installed nss-mdns || sudo yum install -y nss-mdns;
sudo sed -i /etc/nsswitch.conf -e "/^hosts:*/c\hosts:\tfiles mdns4_minimal \[NOTFOUND=return\] dns myhostname"
sudo /bin/systemctl restart avahi-daemon.service;

Once installed on each host you should be able to ping the other nodes in the network. You can query the cache with the avahi-browse command to inspect the ip/hostname cache that has been built.

avahi-browse -acr

Example output;

+   eth1 IPv4 mongod6 [08:00:27:5b:4c:a8]                   Workstation          local
+   eth1 IPv4 mongod5 [08:00:27:6d:3d:80]                   Workstation          local
+   eth1 IPv4 mongod4 [08:00:27:1b:60:89]                   Workstation          local
+   eth1 IPv4 mongod3 [08:00:27:54:02:58]                   Workstation          local
+   eth1 IPv4 mongod2 [08:00:27:29:9a:bb]                   Workstation          local
+   eth1 IPv4 mongod1 [08:00:27:59:68:61]                   Workstation          local
+   eth1 IPv4 mongos3 [08:00:27:71:66:c9]                   Workstation          local
+   eth1 IPv4 mongos2 [08:00:27:18:1c:be]                   Workstation          local
+   eth1 IPv4 mongos1 [08:00:27:e5:53:33]                   Workstation          local
+   eth0 IPv4 mongos1 [52:54:00:47:46:52]                   Workstation          local
=   eth1 IPv4 mongod1 [08:00:27:59:68:61]                   Workstation          local
   hostname = [mongod1.local]
   address = [192.168.43.103]
   port = [9]
   txt = []
=   eth1 IPv4 mongos2 [08:00:27:18:1c:be]                   Workstation          local
   hostname = [mongos2.local]
   address = [192.168.43.101]
   port = [9]
   txt = []
=   eth1 IPv4 mongod5 [08:00:27:6d:3d:80]                   Workstation          local
   hostname = [mongod5.local]
   address = [192.168.43.107]
   port = [9]
   txt = []
=   eth1 IPv4 mongod3 [08:00:27:54:02:58]                   Workstation          local
   hostname = [mongod3.local]
   address = [192.168.43.105]
   port = [9]
   txt = []
=   eth1 IPv4 mongos3 [08:00:27:71:66:c9]                   Workstation          local
   hostname = [mongos3.local]
   address = [192.168.43.102]
   port = [9]
   txt = []
=   eth1 IPv4 mongos1 [08:00:27:e5:53:33]                   Workstation          local
   hostname = [mongos1.local]
   address = [192.168.43.100]
   port = [9]
   txt = []
=   eth0 IPv4 mongos1 [52:54:00:47:46:52]                   Workstation          local
   hostname = [mongos1.local]
   address = [10.0.2.15]
   port = [9]
   txt = []
=   eth1 IPv4 mongod6 [08:00:27:5b:4c:a8]                   Workstation          local
   hostname = [mongod6.local]
   address = [192.168.43.108]
   port = [9]
   txt = []
=   eth1 IPv4 mongod4 [08:00:27:1b:60:89]                   Workstation          local
   hostname = [mongod4.local]
   address = [192.168.43.106]
   port = [9]
   txt = []
=   eth1 IPv4 mongod2 [08:00:27:29:9a:bb]                   Workstation          local
   hostname = [mongod2.local]
   address = [192.168.43.104]
   port = [9]
   txt = []


Cassandra 3 Node Cluster Setup Notes

Install on each node

wget http://www-eu.apache.org/dist/cassandra/redhat/30x/cassandra-3.0.13-1.noarch.rpm
yum install jre
rpm -ivh cassandra-3.0.13-1.noarch.rpm
chkconfig cassandra on

Configuration changes on each node

vi /etc/cassandra/conf/cassandra.yaml

Customise the seeds / ip address for your environment

cluster_name: 'cassandra_cluster'
seeds: "192.168.65.120,192.168.65.121,192.168.65.122"
listen_address: 
rpc_address: 


Start the cassandra service on each node

service cassandra start
service cassandra status

If you get the following error;

org.apache.cassandra.exceptions.ConfigurationException: Saved cluster name Test Cluster != configured name cassandra_cluster

Then you need to reset the data folder (note this removes all data so take a backup if you're not sure).


rm -rf /var/lib/cassandra/data/system/*
service cassandra start
View the status of the cluster
nodetool status
Output will be similar to below;
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address         Load       Tokens       Owns (effective)  Host ID                               Rack
UN  192.168.65.120  108.04 KB  256          65.8%             92119740-cbf7-406a-9237-a1f4036e26e9  rack1
UN  192.168.65.121  166.99 KB  256          65.6%             13b5a4f8-6d98-481b-809e-f1a2ffd8ae94  rack1
UN  192.168.65.122  143.53 KB  256          68.6%             fe0068b2-2dca-403a-b5f2-93e827250bc5  rack1

Login with the command-line client

export CQLSH_HOST=$(hostname --ip-address)
cqlsh

Do some stuff;

cqlsh> CREATE KEYSPACE rhys WITH REPLICATION = {'class':'SimpleStrategy','replication_factor':2};
cqlsh> USE rhys;
cqlsh> CREATE TABLE rhys (empid int primary key, emp_first varchar, emp_last varchar, emp_dept varchar);
cqlsh> INSERT INTO rhys (empid, emp_first, emp_last, emp_dept) VALUES (1, 'Rhys', 'Campbell', 'ENT');

Enable authentication on each node

vi /etc/cassandra/conf/cassandra.conf

Change the option in this file on each node;

authenticator: PasswordAuthenticator

Restart each node;

service cassandra restart

Login to one node to update the cassandra admin user;

export CQLSH_HOST=$(hostname --ip-address)
cqlsh -u cassandra -p cassandra

Alter the replication factor for the system_auth namespace;

cqlsh> ALTER KEYSPACE "system_auth" WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'datacenter1': 3 };

Ensure the change is propogated through the system;

nodetool repair system_auth

Restart;

service cassandra restart

Create a new superuser

cqlsh -u cassandra -p cassandra
csqlsh> CREATE ROLE admin WITH PASSWORD = 'BigSecret' AND SUPERUSER = true AND LOGIN = true;
exit

Change the default user;

cqlsh -u ucid_admin -p BigSecret
cqlsh> ALTER ROLE cassandra WITH PASSWORD='xfvasdfvsxv3456456uyhnfdfgu657rt87ytygwe3456' AND SUPERUSER=false;

Change some settings to update the system roles;

vi /etc/cassandra/conf/cassandra.conf

Set to ten minutes refresh 5

roles_validity_in_ms: 600000
roles_update_interval_in_ms: 300000

A Clone of the STRING_SPLIT MSSQL 2016 Function

I have recently been developing some stuff using MSSQL 2016 and used the STRING_SPLIT function. This doesn’t exist in earlier versions and I discovered I would be required to deploy to 2008 or 2012. So here’s a my own version of the STRING_SPLIT function I have developed and tested on MSSQL 2008 (may also work on 2005).

CREATE FUNCTION [dbo].[STRING_SPLIT_2008]
(
	@string VARCHAR(1024),
	@seperator CHAR(1)
)
	RETURNS @table TABLE (
		[Value] VARCHAR(1024)
	)
AS
BEGIN
 
	DECLARE @x XML;
	SELECT @x = CAST('<a>' + REPLACE(@string, @seperator, '</a><a>') + '</a>' AS XML);
 
	INSERT INTO @table
	SELECT t.value('.', 'varchar(1024)') as inVal
	FROM @X.nodes('/A') AS x(t)
 
	RETURN
END

Usage is as follows;

SELECT *
FROM dbo.STRING_SPLIT_2008('mail1.com;mail2.com;mail3.com;mail4.com;mail5.com;mail6.com;mail7.com;mail8.com;mail9.com', ';')

This would return the following resultset;

string_split_2008 resultset


A simple MariaDB deployment with Ansible

Here’s a simple Ansible Playbook to create a basic MariaDB deployment.

The basic steps the playbook will attempt are:

  • Install a few libraries
  • Setup Repos
  • Install MariaDB packages
  • Install Percona software
  • Create MariaDB directories
  • Copy my.cnf to server (note this is a template file and not supplied here)
  • Run mysql_install_db if needed
  • Start MariaDB
  • Set root password
  • Delete anonymous users
  • Create myapp database and user

Note: some steps will only execute if a root password has not been set. These are identifiable by the following line:

when: is_root_password_set.rc == 0

This is the playbook in full:

---
- hosts: database
  become: true
 
  tasks:
 
    - name: Install Utility software
      apt: name={{item}} state=latest update_cache=yes
      with_items:
        - software-properties-common
        - python-mysqldb
 
    - name: Add apt key
      command: apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db
 
    - name: Add MariaDB Repo
      apt_repository:
        filename: MariaDB-10.2
        repo: deb [arch=amd64,i386] http://mirror.rackspeed.de/mariadb.org/repo/10.2/ubuntu trusty main
        state: present
 
    - name: Get Key for Percona Repo
      command: apt-key adv --keyserver keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A
 
    - name: Add Percona Tools Repo
      apt_repository:
        filename: Percona
        repo: deb http://repo.percona.com/apt trusty main
        state: present
 
    - name: Install MariaDB Packages
      apt: name={{item}} state=installed default_release=trusty update_cache=yes
      with_items:
        - mariadb-client
        - mariadb-common
        - mariadb-server
 
    - name: Install Percona Software
      apt: name={{item}} state=latest force=yes
      with_items:
        - percona-toolkit
        - percona-xtrabackup
        - percona-nagios-plugins
 
    - name: Create MariaDB Directories
      file: path=/data/{{item}} state=directory owner=mysql group=mysql recurse=yes
      with_items:
        - db
        - log
 
    - name: Write new configuration file
      template:
        src: /home/vagrant/ansible/templates/mysql/my.cnf
        dest: /etc/mysql/my.cnf
        owner: mysql
        group: mysql
        mode: '0600'
        backup: yes
 
    - name: Count files in /data/db
      find: path=/data/db patterns='*'
      register: db_files
 
    - name: Run mysql_install_db only if /data/db is empty
      command: mysql_install_db --datadir=/data/db
      when: db_files.matched|int == 0
 
    - name: Start MariaDB
      service: name=mysql state=started
 
    - name: Is root password set?
      command: mysql -u root --execute "SELECT NOW()"
      register: is_root_password_set
      ignore_errors: yes
 
    - name: Delete anonymous users
      mysql_user: user="" state="absent"
      when: is_root_password_set.rc == 0
 
    - name: Generate mysql root password
      shell: tr -d -c "a-zA-Z0-9" < /dev/urandom | head -c 10
      register: mysql_root_password
      when: is_root_password_set.rc == 0
 
    - name: Set root password
      mysql_user: user=root password="{{mysql_root_password.stdout}}" host=localhost
      when: is_root_password_set.rc == 0
 
    - name: Set root password for other hosts
      mysql_user: user=root password="{{mysql_root_password.stdout}}" host="{{item}}" login_user="root" login_host="localhost" login_password="{{mysql_root_password.stdout}}"
      when: is_root_password_set.rc == 0
      with_items:
        - "127.0.0.1"
        - "::1"
 
    - name: Inform user of mysql root password
      debug:
        msg: "MariaDB root password was set to {{mysql_root_password.stdout}}"
      when: is_root_password_set.rc == 0
 
    - name: Create myapp database
      mysql_db:
        name: myapp
        login_user: root
        login_password: "{{mysql_root_password.stdout}}"
        login_host: localhost
        state: present
      when: is_root_password_set.rc == 0
 
    - name: Generate myapp_rw password
      shell: tr -d -c "a-zA-Z0-9" < /dev/urandom | head -c 10
      register: mysql_myapp_rw_password
      when: is_root_password_set.rc == 0
 
    - name: Create user for myapp db
      mysql_user:
        name: myapp_rw
        password: "{{mysql_myapp_rw_password}}"
        priv: myapp.*:SELECT,INSERT,UPDATE,DELETE
        login_user: root
        login_password: "{{mysql_root_password.stdout}}"
        state: present
      when: is_root_password_set.rc == 0
PLAY [database] *********************************************************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************************************************
ok: [db01]

TASK [Install Utility software] *****************************************************************************************************************************
changed: [db01] => (item=[u'software-properties-common', u'python-mysqldb'])

TASK [Add apt key] ******************************************************************************************************************************************
changed: [db01]

TASK [Add MariaDB Repo] *************************************************************************************************************************************
changed: [db01]

TASK [Get Key for Percona Repo] *****************************************************************************************************************************
changed: [db01]

TASK [Add Percona Tools Repo] *******************************************************************************************************************************
changed: [db01]

TASK [Install MariaDB Packages] *****************************************************************************************************************************
changed: [db01] => (item=[u'mariadb-client', u'mariadb-common', u'mariadb-server'])

TASK [Install Percona Software] *****************************************************************************************************************************
changed: [db01] => (item=[u'percona-toolkit', u'percona-xtrabackup', u'percona-nagios-plugins'])

TASK [Create MariaDB Directories] ***************************************************************************************************************************
changed: [db01] => (item=db)
changed: [db01] => (item=log)

TASK [Write new configuration file] *************************************************************************************************************************
changed: [db01]

TASK [Count files in /data/db] ******************************************************************************************************************************
ok: [db01]

TASK [Run mysql_install_db only if /data/db is empty] *******************************************************************************************************
changed: [db01]

TASK [Start MariaDB] ****************************************************************************************************************************************
ok: [db01]

TASK [Is root password set?] ********************************************************************************************************************************
changed: [db01]

TASK [Delete anonymous users] *******************************************************************************************************************************
ok: [db01]

TASK [Generate mysql root password] *************************************************************************************************************************
changed: [db01]

TASK [Set root password] ************************************************************************************************************************************
changed: [db01]

TASK [Set root password for other hosts] ********************************************************************************************************************
changed: [db01] => (item=127.0.0.1)
changed: [db01] => (item=::1)

TASK [Inform user of mysql root password] *******************************************************************************************************************
ok: [db01] => {
    "changed": false,
    "msg": "MariaDB root password was set to zr2MuEXUBD"
}

TASK [Create myapp database] ********************************************************************************************************************************
changed: [db01]

TASK [Generate myapp_rw password] ***************************************************************************************************************************
changed: [db01]

TASK [Create user for myapp db] *****************************************************************************************************************************
changed: [db01]

PLAY RECAP **************************************************************************************************************************************************
db01                       : ok=22   changed=17   unreachable=0    failed=0

A dockerized mongod instance with authentication enabled

Here’s just a quick walkthrough showing how to create a dockerized instance of a standalone MongoDB instance.

First, from within a terminal, create a folder to hold the Dockerfile…

mkdir Docker_MongoDB_Image
cd Docker_MongoDB_Image
touch Dockerfile

Edit the Dockerfile…

vi Dockerfile

Enter the following text. You may wish to modify the file slightly. For example; if you need to set any of the proxy values or the MongoDB admin password.

FROM centos
#ENV http_proxy XXXXXXXXXXXXXXXXXX
#ENV https_proxy XXXXXXXXXXXXXXX

MAINTAINER Rhys Campbell no_mail@no_mail.cc

RUN echo $'[mongodb-org-3.4] \n\
name=MongoDB Repository \n\
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/ \n\
gpgcheck=1 \n\
enabled=1 \n\
gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc ' > /etc/yum.repos.d/mongodb-org-3.4.repo

RUN yum clean all && yum install -y mongodb-org-server mongodb-org-shell mongodb-org-tools
RUN mkdir -p /data/db && chown -R mongod:mongod /data/db
RUN /usr/bin/mongod -f /etc/mongod.conf && sleep 5 && mongo admin --eval "db.createUser({user:\"admin\",pwd:\"secret\",roles:[{role:\"root\",db:\"admin\"}]}); db.shutdownServer()"
RUN echo $'security: \n\
  authorization: enabled \n ' >> /etc/mongod.conf
RUN sed -i 's/^  bindIp: 127\.0\.0\.1/  bindIp: \[127\.0\.0\.1,0\.0\.0\.0\]/' /etc/mongod.conf
RUN sed -i 's/^  fork: true/  fork: false/' /etc/mongod.conf
RUN chown mongod:mongod /etc/mongod.conf
RUN cat /etc/mongod.conf

EXPOSE 27017

ENTRYPOINT /usr/bin/mongod -f /etc/mongod.conf

Build the image from within the current dirfectory…

docker build -t mongod-instance . --no-cache

Run the image and map to the 27017 port…

docker run  -p 27017:27017 --name mongod-instance -t mongod-instance

Inspect the mapped port with…

docker ps

The output should look something like this…

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
5e7e4a069f4a        mongod-instance     "/bin/sh -c '/usr/..."   2 hours ago         Up 2 hours          0.0.0.0:27017->27017/tcp   mongod-instance

We can view the docker ip address with this command…

docker-machine ls

Output looks like this…

NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER        ERRORS
default   *        virtualbox   Running   tcp://192.168.99.100:2376           v17.05.0-ce

You can connect to the dockerized mongod instance with this command…

mongo admin -u admin -p --port 27017 --host 192.168.99.100

When you are done with the instance it can be destroyed with…

docker stop mongod-instance
docker rm mongod-instance

Update: I’ve added this to my Docker Hub account so you can grab the image directly from there.