Since I’ve started experimenting with Solaris in my home lab, I’ve really wanted to try managing systems with some sort of configuration management software. I originally thought about trying Rex, a Perl configuration management tool, but I’ve yet to take the time to learn it. I do, however, know Ansible, and it came as a welcome surprise to me that I can install Python, which is needed by Ansible, on Solaris without having to go through the hassle of compiling it from source. This is because Python can be installed using the pkgutil tool from OpenCSW. In addition, the community.general collection in Ansible includes a pkgutil module that allows Yum/Apt-like package management. One day I decided to see if Ansible would work on Solaris.
Note: your results may vary in following these directions; I can’t guarantee that they will work for you.
Prepare Solaris 9 for Ansible
I performed the below steps on SPARC and x86 Solaris 9.
- On Solaris 9 x86, I installed the Solaris 9 x86 Recommended Patch Cluster (9_x86_Recommended.zip), found here: http://ftp.lanet.lv/ftp/unix/sun-info/sun-patches/. I also had to install the libm patch 111728-03 found on the same site.
- To install the patch cluster, SCP 9_x86_Recommended.zip to /tmp on your Solaris 9 system and unzip it. Drop to single user mode with init s. cd to /tmp/9_x86_Recommended and run ./install_cluster. Reboot the system
- To install patch 111728-03, SCP 111728-03.zip to tmp on your Solaris 9 system and unzip it. Install it with patchadd -d /tmp/111728-03
- On Solaris 9 SPARC, running on my SunBlade 100, I installed the Solaris 9 Recommended Patch Cluster (9_Recommended.zip), found here: http://ftp.lanet.lv/ftp/unix/sun-info/sun-patches/.
- Just like for the x86 platform, SCP 9_Recommended.zip to /tmp on your Solaris 9 system and unzip it. Drop to single user mode with init s. cd to /tmp/9_Recommended and run ./install_cluster. This took hours on my SB-100 with a spinning disk. Reboot the system after completion.
While not all of the below are necessary, I decided to write a script that took care of the following:
- Install and configure pkgutil.
- Install sudo with pkgutil (you can use the root account, but sudo is the preferred method).
- Install python with pkgutil (I chose to install the basic Python 2.6, but there are also packages for 2.7 and 3.1. Later on I try both of these).
- Create a local Ansible user and group with a home directory in /export/home.
- Create a sudo entry for the ansible user.
- Add an SSH key to .ssh/authorized_hosts
I ended up writing both a Bash script and a Perl script for this purpose. I prefer and recommend the Bash version, as it is much simpler and cleaner, but I have included the Perl one as well in case you are curious and want to look at some ugly Perl code.
#!/bin/bash
PUBKEY='YOUR_SSH_PUBLIC_KEY'
# Install pkgutil from /tmp/pkgutil.pkg
if [[ ! -f "/opt/csw/bin/pkgutil" ]] ; then
if [[ ! -f "/tmp/pkgutil.pkg" ]] ; then
echo "You must copy pkgutil.pkg to /tmp"
exit 1
else
echo "Installing pkgutil."
pkgadd -d /tmp/pkgutil.pkg
fi
fi
# Setting the mirror in /etc/opt/csw/pkgutil.conf
perl -pi -e 's/#?mirror=\S+/mirror=http:\/\/your_mirror_ip\/solaris\/opencsw\/bratislava/g' /etc/opt/csw/pkgutil.conf
# Check if packages need to be installed.
if [[ ! -f "/opt/csw/bin/python" ]] ; then
echo "Installing python."
/opt/csw/bin/pkgutil -y -i python
fi
if [[ ! -e "/opt/csw/bin/sudo" ]] ; then
echo "Installing sudo."
/opt/csw/bin/pkgutil -y -i sudo
fi
if [[ ! -f "/opt/csw/bin/openssl" ]] ; then
echo "Installing openssl."
/opt/csw/bin/pkgutil -y -i openssl_utils
fi
# Create ansible user if needed
umask 077
if ! grep ansible /etc/passwd > /dev/null ; then
echo "Creating ansible user and group."
groupadd -g 1010 ansible
useradd -u 1010 -g ansible -c "Ansible User" -s /bin/bash -m -d /export/home/ansible ansible
echo "Setting a random password for the ansible user."
PW_HASH=`/opt/csw/bin/openssl rand -hex 4 | /opt/csw/bin/openssl passwd -crypt -stdin`
SHADOW_TIME=`perl -e 'print int(time()/60/60/24);'`
echo "Backing up /etc/shadow to /etc/shadow.backup"
cp /etc/shadow /etc/shadow.backup
grep -v ansible /etc/shadow > /etc/shadow.new
echo "ansible:${PW_HASH}:${SHADOW_TIME}::::::" >> /etc/shadow.new
mv /etc/shadow.new /etc/shadow
fi
# Set up sudoers if needed
if [[ ! -f "/etc/opt/csw/sudoers.d/ansible" ]] ; then
echo "Adding an entry for the ansible user in /etc/opt/csw/sudoers.d/ansible"
echo "ansible ALL=(ALL) NOPASSWD: ALL" > /etc/opt/csw/sudoers.d/ansible
fi
# Create additional directories in the home directory and
# ensure the public key is correct
if [[ ! -d "/export/home/ansible/.ansible/tmp" ]] ; then
mkdir -p /export/home/ansible/.ansible/tmp
fi
if [[ ! -d "/export/home/ansible/.ssh" ]] ; then
mkdir /export/home/ansible/.ssh
fi
echo $PUBKEY > /export/home/ansible/.ssh/authorized_keys
chown -R ansible:ansible /export/home/ansible
echo "ansible user configuration script has completed."
#!/usr/bin/perl -w
use strict;
my @chars = ('.', '/', 'A'..'Z', 'a'..'z', 0..9);
my $csw_mirror = 'your-mirror-url';
my $csw_release = 'bratislava';
my $pubkey = <<'PUBKEY';
YOUR_PUBLIC_SSH_KEY
PUBKEY
# You can set a password here, but I choose to set a random one.
my $ansible_pw = join('', @chars[ map { rand @chars } ( 1 .. 8 )]);
if (-f '/opt/csw/bin/pkgutil') {
print "pkgutil is already installed. Skipping this step.\n";
} else {
print "Installing pkgutil.\n";
unless (-f '/tmp/pkgutil.pkg') {
die "You must copy pkgutil.pkg to /tmp before proceeding.\n";
}
# Create answer file to avoid being prompted to install.
open(ADMIN, '>', '/tmp/adminfile') || die "Unable to open /tmp/adminfile: $!\n";
print ADMIN "action=nocheck\n";
close(ADMIN);
system('yes all | pkgadd -a /tmp/adminfile -d /tmp/pkgutil.pkg');
unless (-f '/opt/csw/bin/pkgutil') {
die "pkgutil did not install successfully. Exiting.\n";
}
}
# Update /etc/opt/csw/pkgutil.conf with the correct mirror
print "Updating /etc/opt/csw/pkgutil.conf with the correct mirror\n";
open(PKGUTIL_IN, '<', '/etc/opt/csw/pkgutil.conf')
||die "Unable to open /etc/opt/csw/pkgutil.conf: $!\n";
open(PKGUTIL_OUT, '>', '/tmp/pkgutil.conf') || die "Unable to open /tmp/pkgutil.conf: $!\n";
while (my $line = <PKGUTIL_IN>) {
if ($line =~ /^#?mirror\=/) {
print PKGUTIL_OUT "mirror=${csw_mirror}/${csw_release}\n";
} else {
print PKGUTIL_OUT $line;
}
}
close(PKGUTIL_IN);
close(PKGUTIL_OUT);
system('mv /tmp/pkgutil.conf /etc/opt/csw/pkgutil.conf');
my @packages;
unless (-f '/opt/csw/bin/sudo') {
push(@packages, 'sudo');
}
unless (-f '/opt/csw/bin/python') {
push(@packages, 'python');
}
if (scalar(@packages) > 0) {
my $install = '/opt/csw/bin/pkgutil -y -i ' . join(' ', @packages);
print 'Installing ' . join(', ', @packages) . "\n";
system($install);
} else {
print "Python and sudo are already installed.\n"
}
print "Checking if the ansible user already exists.\n";
my $user_created = 0;
open(PASSWD, '<', '/etc/passwd') || die "Unable to open /etc/passwd: $!\n";
while (my $line = <PASSWD>) {
if ($line =~/^ansible\:/) {
$user_created = 1;
}
}
close(PASSWD);
if ($user_created == 0) {
print "Creating ansible user and group.\n";
system('groupadd -g 1010 ansible');
system('useradd -u 1010 -g ansible -c "Ansible User" -s /bin/bash -m -d /export/home/ansible ansible');
# Setting the password for the user.
print "Backing up /etc/shadow to /etc/shadow.backup. You should check over /etc/shadow before rebooting.\n";
system('cp /etc/shadow /etc/shadow.backup');
my $shadow_time = int(time()/60/60/24);
my $salt = join('', @chars[ map { rand @chars } ( 1 .. 8 )]);
my $pw_hash = crypt($ansible_pw, $salt);
open(SHADOW_IN, '<', '/etc/shadow') || die "Unable to open /etc/shadow: $!\n";
open(SHADOW_OUT, '>', '/etc/shadow.new') || die "Unable to open /etc/shadow.new: $!\n";
while (my $line = <SHADOW_IN>) {
if ($line =~ /^ansible\:/) {
print SHADOW_OUT "ansible:${pw_hash}:${shadow_time}::::::\n";
} else {
print SHADOW_OUT $line;
}
}
close(SHADOW_IN);
close(SHADOW_OUT);
system('mv /etc/shadow.new /etc/shadow');
} else {
print "User already created.\n";
}
umask 077;
print "Adding sudoers entry if needed.\n";
unless (-f '/etc/opt/csw/sudoers.d/ansible') {
open(SUDOERS_OUT, '>', '/etc/opt/csw/sudoers.d/ansible') ||
die "Unable to open /etc/opt/csw/sudoers.d/ansible: $!\n";
print SUDOERS_OUT "ansible ALL=(ALL) NOPASSWD: ALL\n";
close(SUDOERS_OUT);
}
print "Creating /export/home/ansible/.ansible/tmp if needed.\n";
unless (-d '/export/home/ansible/.ansible/tmp') {
system('mkdir -p /export/home/ansible/.ansible/tmp');
}
print "Creating /export/home/ansible/.ssh if needed.\n";
unless (-d '/export/home/ansible/.ssh') {
system('mkdir -p /export/home/ansible/.ssh');
}
# This will run every time because I am lazy
print "Setting up public key.\n";
open(PUBKEY, '>', '/export/home/ansible/.ssh/authorized_keys') ||
die "Unable to open /export/home/ansible/.ssh/authorized_keys: $!\n";
print PUBKEY $pubkey;
close(PUBKEY);
print "Ensuring that the home directory is owned by the ansible user.\n";
system('chown -R ansible:ansible /export/home/ansible');
I SCP’d the scripts and pkgutil.pkg to /tmp on my Solaris 9 systems, and tested each one successfully.
Configure Ansible on the control node
After configuring the Ansible remote user, I configured an ansible.cfg file in the work directory on the Linux control node.
[defaults] remote_user = ansible deprecation_warnings = false host_key_checking = false interpreter_python = /opt/csw/bin/python inventory = inventory remote_tmp = $HOME/.ansible/tmp private_key_file = ~/.ssh/id_rsa [privilege_escalation] become_exe = /opt/csw/bin/sudo become = true
I will explain a few of the necessary options. interpreter_python of course tells Ansible where to find the Python interpreter on the system. On most Linux systems Ansible is able to discover this in locations such as /usr/bin/python, /usr/bin/python3, etc., but it won’t know to look in /opt/csw/bin. Similarly, Ansible won’t know to look in /opt/csw/bin for the sudo command. Below shows the results of attempting to run the setup module without those options.
remote_tmp = $HOME/.ansible/tmp is a solution to this problem here. Finally, I have only tested this with Ansible 2.10.8 running on Linux Mint 21.1, both out-of-date at this point. Versions of Ansible newer than 2.12 have removed support for Python 2.6.
So does it work? Yes, surprisingly. After going through the usual period of troubleshooting, I was finally able to run the setup module against my Solaris 9 VM:
As well as against my SB-100, which is of course runs considerably slower:
Writing a simple playbook to configure NTP
For my first playbook, I decided to write a basic playbook that installs NTP from the OpenCSW repository and configures ntp.conf. While the SPARC version of Solaris 9 has a built-in NTP server, the x86 version I am running does not. For simplicity’s sake, I decided to install the same version on both SPARC and x86. Below is the playbook:
---
- hosts: solaris_9
vars:
ntp_servers:
- '0.pool.ntp.org'
- '1.pool.ntp.org'
tasks:
- name: Install the ntp OpenCSW package
community.general.pkgutil:
name: CSWntp
state: present
- name: Deploy /etc/opt/csw/ntp.conf
ansible.builtin.template:
src: ntp.conf.j2
dest: /etc/opt/csw/ntp.conf
notify: restart ntp
handlers:
- name: restart ntp
ansible.builtin.command: /etc/init.d/cswntp restart
And the Jinja2 Template:
driftfile /var/ntp/ntp.drift
{% for server in ntp_servers %}
pool {{ server }} iburst
{% endfor %}
Below shows it running successfully:
The Ansible lineinfile module works as well:
---
- hosts: solaris_9
tasks:
- name: disable root SSH login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
backup: true
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
notify: restart sshd
handlers:
- name: restart sshd
ansible.builtin.command: /etc/init.d/sshd restart
These are simple examples that demonstrate that Ansible can be used to configure Solaris 9, which came out in 2002! Next, I will try something more complicated: configuring the LDAP client from my previous post.
Configuring LDAP authentication with Ansible
The LDAP client in Solaris is initialized using the ldapclient command. When creating a playbook for configuring LDAP authentication, I would prefer that this command only run when changes to the configuration are made. My solution ended up being creating a shell script that contains the ldapclient command with all of configuration options and is called as a handler from Ansible whenever changes are made. The Ansible playbook pushes this out to /opt/local/bin (supposedly the preferred location in Solaris for custom scripts).
#!/bin/bash
if [[ -f '/var/ldap/ldap_client_cred' ]] ; then
ACTION='mod'
else
ACTION='manual'
fi
ldapclient $ACTION -a credentialLevel=proxy \
-a authenticationMethod=simple \
-a "proxyPassword={{ bind_pw }}" \
-a "proxyDN={{ bind_dn }}" \
-a defaultSearchBase={{ ldap_base }} \
-a serviceSearchDescriptor=passwd:ou=people,{{ ldap_base }} \
-a serviceSearchDescriptor=group:ou=groups,{{ ldap_base }} \
-a serviceAuthenticationMethod=pam_ldap:simple \
-a "preferredServerList={{ ldap_servers | join(' ') }}"
cp /etc/nsswitch.ldap /etc/nsswitch.conf
Now for the playbook itself. For this post, I wrote a simple one with the variables in the playbook. This is not the preferred method of doing this; it is recommended to create a group_vars/ file with all of the variables. Furthermore, I strongly recommend encrypting your LDAP bind password with Ansible Vault. This will ensure that it doesn’t get accidentally committed to GitHub or some other public place. There are many guides out there on how to do this and it is beyond the scope of this post.
---
- hosts: solaris_9
gather_facts: false
vars:
automount: 'YOUR_NFS_HOME_SHARE'
ldap_servers:
- 'LDAP_SERVER_IP_1'
- 'LDAP_SERVER_IP_2'
ldap_base: 'dc=example,dc=com'
bind_dn: 'uid=authagent,ou=backend,dc=example,dc=com'
bind_pw: 'YOUR_BIND_PW_THAT_SHOULD_GO_IN_ANSIBLE_VAULT'
tasks:
- name: Set DNS in /etc/nsswitch.ldap
ansible.builtin.lineinfile:
path: /etc/nsswitch.ldap
regexp: '^hosts:'
line: 'hosts: dns files'
- name: Create /opt/local/bin
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: 0755
loop:
- /opt/local
- /opt/local/bin
- name: Create /opt/local/bin/configure_ldap_client.sh
ansible.builtin.template:
src: configure_ldap_client.sh.j2
dest: /opt/local/bin/configure_ldap_client.sh
mode: 0700
notify: Configure LDAP Client
no_log: true
# You may want to set backup to true
- name: Configure /etc/pam.conf
ansible.builtin.copy:
src: pam.conf_sol9
dest: /etc/pam.conf
#backup: true
- name: Configure /etc/auto_home
ansible.builtin.lineinfile:
path: /etc/auto_home
regexp: '^\*'
line: "* -fstype=nfs {{ automount }}/&amp;"
notify: Restart autofs
handlers:
- name: Configure LDAP Client
ansible.builtin.command: '/opt/local/bin/configure_ldap_client.sh'
- name: Restart autofs
ansible.builtin.sysvinit:
name: autofs
state: restarted
The below collapsed text shows the output from a successful execution of this playbook (it was too long to put in a screenshot).
ansible-playbook -D sol_ldap_client.yml
PLAY [solaris_9] ***************************************************************
TASK [Set DNS in /etc/nsswitch.ldap] *******************************************
--- before: /etc/nsswitch.ldap (content)
+++ after: /etc/nsswitch.ldap (content)
@@ -12,7 +12,7 @@
group: files ldap
# consult /etc "files" only if ldap is down.
-hosts: ldap [NOTFOUND=return] files
+hosts: files dns
ipnodes: files
# Uncomment the following line and comment out the above to resolve
# both IPv4 and IPv6 addresses from the ipnodes databases. Note that
changed: [sol9vm1]
--- before: /etc/nsswitch.ldap (content)
+++ after: /etc/nsswitch.ldap (content)
@@ -12,7 +12,7 @@
group: files ldap
# consult /etc "files" only if ldap is down.
-hosts: ldap [NOTFOUND=return] files
+hosts: files dns
ipnodes: files
# Uncomment the following line and comment out the above to resolve
# both IPv4 and IPv6 addresses from the ipnodes databases. Note that
changed: [sb100]
TASK [Create /opt/local/bin] ***************************************************
--- before
+++ after
@@ -1,4 +1,4 @@
{
"path": "/opt/local",
- "state": "absent"
+ "state": "directory"
}
changed: [sol9vm1] =&gt; (item=/opt/local)
--- before
+++ after
@@ -1,4 +1,4 @@
{
"path": "/opt/local/bin",
- "state": "absent"
+ "state": "directory"
}
changed: [sol9vm1] =&gt; (item=/opt/local/bin)
--- before
+++ after
@@ -1,4 +1,4 @@
{
"path": "/opt/local",
- "state": "absent"
+ "state": "directory"
}
changed: [sb100] =&gt; (item=/opt/local)
--- before
+++ after
@@ -1,4 +1,4 @@
{
"path": "/opt/local/bin",
- "state": "absent"
+ "state": "directory"
}
changed: [sb100] =&gt; (item=/opt/local/bin)
TASK [Create /opt/local/bin/configure_ldap_client.sh] **************************
changed: [sol9vm1]
changed: [sb100]
TASK [Configure /etc/pam.conf] *************************************************
--- before: /etc/pam.conf
+++ after: /solhome/matthew/pam.conf_sol9
@@ -19,39 +19,45 @@
#
login auth requisite pam_authtok_get.so.1
login auth required pam_dhkeys.so.1
-login auth required pam_unix_auth.so.1
login auth required pam_dial_auth.so.1
+login auth binding pam_unix_auth.so.1 server_policy
+login auth required pam_ldap.so.1
#
# rlogin service (explicit because of pam_rhost_auth)
#
rlogin auth sufficient pam_rhosts_auth.so.1
rlogin auth requisite pam_authtok_get.so.1
rlogin auth required pam_dhkeys.so.1
-rlogin auth required pam_unix_auth.so.1
+rlogin auth binding pam_unix_auth.so.1 server_policy
+rlogin auth required pam_ldap.so.1
#
# rsh service (explicit because of pam_rhost_auth,
# and pam_unix_auth for meaningful pam_setcred)
#
rsh auth sufficient pam_rhosts_auth.so.1
-rsh auth required pam_unix_auth.so.1
+rsh auth binding pam_unix_auth.so.1 server_policy
+rsh auth required pam_ldap.so.1
#
# PPP service (explicit because of pam_dial_auth)
#
ppp auth requisite pam_authtok_get.so.1
ppp auth required pam_dhkeys.so.1
-ppp auth required pam_unix_auth.so.1
ppp auth required pam_dial_auth.so.1
+ppp auth binding pam_unix_auth.so.1 server_policy
+ppp auth required pam_ldap.so.1
#
# Default definitions for Authentication management
# Used when service name is not explicitly mentioned for authenctication
#
other auth requisite pam_authtok_get.so.1
other auth required pam_dhkeys.so.1
-other auth required pam_unix_auth.so.1
+other auth binding pam_unix_auth.so.1 server_policy
+other auth required pam_ldap.so.1
#
# passwd command (explicit because of a different authentication module)
#
-passwd auth required pam_passwd_auth.so.1
+passwd auth binding pam_passwd_auth.so.1 server_policy
+passwd auth required pam_ldap.so.1
#
# cron service (explicit because of non-usage of pam_roles.so.1)
#
@@ -63,7 +69,8 @@
#
other account requisite pam_roles.so.1
other account required pam_projects.so.1
-other account required pam_unix_account.so.1
+other account binding pam_unix_account.so.1 server_policy
+other account required pam_ldap.so.1
#
# Default definition for Session management
# Used when service name is not explicitly mentioned for session management
@@ -76,7 +83,7 @@
other password required pam_dhkeys.so.1
other password requisite pam_authtok_get.so.1
other password requisite pam_authtok_check.so.1
-other password required pam_authtok_store.so.1
+other password required pam_authtok_store.so.1 server_policy
#
# Support for Kerberos V5 authentication (uncomment to use Kerberos)
#
changed: [sol9vm1]
--- before: /etc/pam.conf
+++ after: /solhome/matthew/pam.conf_sol9
@@ -19,39 +19,45 @@
#
login auth requisite pam_authtok_get.so.1
login auth required pam_dhkeys.so.1
-login auth required pam_unix_auth.so.1
login auth required pam_dial_auth.so.1
+login auth binding pam_unix_auth.so.1 server_policy
+login auth required pam_ldap.so.1
#
# rlogin service (explicit because of pam_rhost_auth)
#
rlogin auth sufficient pam_rhosts_auth.so.1
rlogin auth requisite pam_authtok_get.so.1
rlogin auth required pam_dhkeys.so.1
-rlogin auth required pam_unix_auth.so.1
+rlogin auth binding pam_unix_auth.so.1 server_policy
+rlogin auth required pam_ldap.so.1
#
# rsh service (explicit because of pam_rhost_auth,
# and pam_unix_auth for meaningful pam_setcred)
#
rsh auth sufficient pam_rhosts_auth.so.1
-rsh auth required pam_unix_auth.so.1
+rsh auth binding pam_unix_auth.so.1 server_policy
+rsh auth required pam_ldap.so.1
#
# PPP service (explicit because of pam_dial_auth)
#
ppp auth requisite pam_authtok_get.so.1
ppp auth required pam_dhkeys.so.1
-ppp auth required pam_unix_auth.so.1
ppp auth required pam_dial_auth.so.1
+ppp auth binding pam_unix_auth.so.1 server_policy
+ppp auth required pam_ldap.so.1
#
# Default definitions for Authentication management
# Used when service name is not explicitly mentioned for authenctication
#
other auth requisite pam_authtok_get.so.1
other auth required pam_dhkeys.so.1
-other auth required pam_unix_auth.so.1
+other auth binding pam_unix_auth.so.1 server_policy
+other auth required pam_ldap.so.1
#
# passwd command (explicit because of a different authentication module)
#
-passwd auth required pam_passwd_auth.so.1
+passwd auth binding pam_passwd_auth.so.1 server_policy
+passwd auth required pam_ldap.so.1
#
# cron service (explicit because of non-usage of pam_roles.so.1)
#
@@ -63,7 +69,8 @@
#
other account requisite pam_roles.so.1
other account required pam_projects.so.1
-other account required pam_unix_account.so.1
+other account binding pam_unix_account.so.1 server_policy
+other account required pam_ldap.so.1
#
# Default definition for Session management
# Used when service name is not explicitly mentioned for session management
@@ -76,7 +83,7 @@
other password required pam_dhkeys.so.1
other password requisite pam_authtok_get.so.1
other password requisite pam_authtok_check.so.1
-other password required pam_authtok_store.so.1
+other password required pam_authtok_store.so.1 server_policy
#
# Support for Kerberos V5 authentication (uncomment to use Kerberos)
#
changed: [sb100]
TASK [Configure /etc/auto_home] ************************************************
--- before: /etc/auto_home (content)
+++ after: /etc/auto_home (content)
@@ -1,3 +1,4 @@
# Home directory map for automounter
#
+auto_home
+* -fstype=nfs 192.168.87.75:/solhome/&amp;
changed: [sol9vm1]
--- before: /etc/auto_home (content)
+++ after: /etc/auto_home (content)
@@ -1,3 +1,4 @@
# Home directory map for automounter
#
+auto_home
+* -fstype=nfs 192.168.87.75:/solhome/&amp;
changed: [sb100]
RUNNING HANDLER [Configure LDAP Client] ****************************************
changed: [sol9vm1]
changed: [sb100]
RUNNING HANDLER [Restart autofs] ***********************************************
changed: [sol9vm1]
changed: [sb100]
PLAY RECAP *********************************************************************
sb100 : ok=7 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
sol9vm1 : ok=7 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Using the sysvinit module
To restart services in the previous examples, I used the command module to call scripts in /etc/init.d, as the Ansible service module would fail.
Only later on did I discover that Ansible includes a sysvinit module that can restart sysvinit scripts. I tested this with the autofs service, which does not have a restart command:
The below output shows using the sysinitv module in the NTP playbook and calling it as a handler successfully after adding an additional NTP server:
matthew@death-star:/solhome/matthew$ tail -5 ntp.yml
handlers:
- name: restart ntp
ansible.builtin.sysvinit:
name: cswntp
state: restarted
matthew@death-star:/solhome/matthew$ ansible-playbook -D ntp.yml
[WARNING]: Collection community.general does not support Ansible version 2.10.8
PLAY [solaris_9] ***************************************************************
TASK [Gathering Facts] *********************************************************
ok: [sol9vm1]
ok: [sb100]
TASK [Install the ntp OpenCSW package] *****************************************
ok: [sol9vm1]
ok: [sb100]
TASK [Deploy /etc/opt/csw/ntp.conf] ********************************************
--- before: /etc/opt/csw/ntp.conf
+++ after: /home2/matthew/.ansible/tmp/ansible-local-8441qe7y1m4f/tmph1273un4/ntp.conf.j2
@@ -1,3 +1,4 @@
driftfile /var/ntp/ntp.drift
pool 0.pool.ntp.org iburst
pool 1.pool.ntp.org iburst
+pool 2.pool.ntp.org iburst
changed: [sol9vm1]
--- before: /etc/opt/csw/ntp.conf
+++ after: /home2/matthew/.ansible/tmp/ansible-local-8441qe7y1m4f/tmp41fvp7tl/ntp.conf.j2
@@ -1,3 +1,4 @@
driftfile /var/ntp/ntp.drift
pool 0.pool.ntp.org iburst
pool 1.pool.ntp.org iburst
+pool 2.pool.ntp.org iburst
changed: [sb100]
RUNNING HANDLER [restart ntp] **************************************************
changed: [sol9vm1]
changed: [sb100]
PLAY RECAP *********************************************************************
sb100 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
sol9vm1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Python 2.7 and 3.1 support
My verdict is, Python 2.7 works, but 3.1 does not. This makes little difference anyway, as you will still need to use an older version of ansible-core, such as 2.10.
Bonus section: does this work on Solaris 8?
The short answer: for the most part, with modifications.
- SSH will need to be configured. A previous post of mine describes how to do this.
- You will need to disable fact gathering with gather_facts: false in your playbook or gathering = explicit in your ansible.cfg. Otherwise, you will get the below error that is of little help:
-
- You will need to use the dublin release for OpenCSW instead of bratislava.
- You will need to put the sudo entry for the ansible user in /opt/csw/etc/sudoers instead of /etc/opt/csw/sudoers.d/ansible. I prefer putting sudo entries in sudoers.d, but this is not available in the sudo version in Solaris 8. I have included the Ansible prepare shell script with these modifications.
#!/bin/bash
PUBKEY='YOUR_SSH_PUBLIC_KEY'
# Install pkgutil from /tmp/pkgutil.pkg
if [[ ! -f "/opt/csw/bin/pkgutil" ]] ; then
if [[ ! -f "/tmp/pkgutil.pkg" ]] ; then
echo "You must copy pkgutil.pkg to /tmp"
exit 1
else
echo "Installing pkgutil."
pkgadd -d /tmp/pkgutil.pkg
fi
fi
# Setting the mirror in /etc/opt/csw/pkgutil.conf
perl -pi -e 's/#?mirror=\S+/mirror=http:\/\/your_mirror_ip\/solaris\/opencsw\/dublin/g' /etc/opt/csw/pkgutil.conf
# Check if packages need to be installed.
if [[ ! -f "/opt/csw/bin/python" ]] ; then
echo "Installing python."
/opt/csw/bin/pkgutil -y -i python
fi
if [[ ! -e "/opt/csw/bin/sudo" ]] ; then
echo "Installing sudo."
/opt/csw/bin/pkgutil -y -i sudo
fi
if [[ ! -f "/opt/csw/bin/openssl" ]] ; then
echo "Installing openssl."
/opt/csw/bin/pkgutil -y -i openssl_utils
fi
# Create ansible user if needed
umask 077
if ! grep ansible /etc/passwd > /dev/null ; then
echo "Creating ansible user and group."
groupadd -g 1010 ansible
useradd -u 1010 -g ansible -c "Ansible User" -s /bin/bash -m -d /export/home/ansible ansible
echo "Setting a random password for the ansible user."
PW_HASH=`/opt/csw/bin/openssl rand -hex 4 | /opt/csw/bin/openssl passwd -crypt -stdin`
SHADOW_TIME=`perl -e 'print int(time()/60/60/24);'`
echo "Backing up /etc/shadow to /etc/shadow.backup"
cp /etc/shadow /etc/shadow.backup
grep -v ansible /etc/shadow > /etc/shadow.new
echo "ansible:${PW_HASH}:${SHADOW_TIME}::::::" >> /etc/shadow.new
mv /etc/shadow.new /etc/shadow
fi
# Setup sudoers if needed
if ! grep ansible /opt/csw/etc/sudoers > /dev/null ; then
echo "Adding an entry for the ansible user in /opt/csw/etc/sudoers"
echo '# Entry for the ansible user' >> /opt/csw/etc/sudoers
echo 'ansible ALL=(ALL) NOPASSWD: ALL' >> /opt/csw/etc/sudoers
fi
# Create additional directories in the home directory and
# ensure the public key is correct
if [[ ! -d "/export/home/ansible/.ansible/tmp" ]] ; then
mkdir -p /export/home/ansible/.ansible/tmp
fi
if [[ ! -d "/export/home/ansible/.ssh" ]] ; then
mkdir /export/home/ansible/.ssh
fi
echo $PUBKEY > /export/home/ansible/.ssh/authorized_keys
chown -R ansible:ansible /export/home/ansible
echo "ansible user configuration script has completed."
- The sshd_config for Solaris 8 is located at /opt/csw/etc/ssh/sshd_config and the service name is cswopenssh. I have updated the disable_root_ssh.yml to include this:
---
- hosts: solaris_8
gather_facts: false
tasks:
- name: disable root SSH login
ansible.builtin.lineinfile:
path: /opt/csw/etc/ssh/sshd_config
backup: true
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
notify: restart cswopenssh
handlers:
- name: restart cswopenssh
ansible.builtin.sysvinit:
name: cswopenssh
state: restarted
- Finally, for the LDAP client playbook. The pam.conf file and ldapclient command are different for Solaris 8, but everything else is the same. If fact-gathering worked, I could have used the same playbook and told it to use different templates and files based upon the OS release, but that was not possible in this case.
---
- hosts: solaris_8
gather_facts: false
vars:
automount: 'YOUR_NFS_HOME_SHARE'
ldap_servers:
- 'LDAP_SERVER_IP_1'
- 'LDAP_SERVER_IP_2'
ldap_base: 'dc=example,dc=com'
bind_dn: 'uid=authagent,ou=backend,dc=example,dc=com'
bind_pw: 'YOUR_BIND_PW_THAT_SHOULD_GO_IN_ANSIBLE_VAULT'
tasks:
- name: Set DNS in /etc/nsswitch.ldap
ansible.builtin.lineinfile:
path: /etc/nsswitch.ldap
regexp: '^hosts:'
line: 'hosts: files dns'
- name: Create /opt/local/bin
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: 0755
loop:
- /opt/local
- /opt/local/bin
- name: Create /opt/local/bin/configure_ldap_client.sh
ansible.builtin.template:
src: configure_ldap_client_8.sh.j2
dest: /opt/local/bin/configure_ldap_client.sh
mode: 0700
notify: Configure LDAP Client
no_log: true
# You may want to set backup to true
- name: Configure /etc/pam.conf
ansible.builtin.copy:
src: pam.conf_sol8
dest: /etc/pam.conf
#backup: true
- name: Configure /etc/auto_home
ansible.builtin.lineinfile:
path: /etc/auto_home
regexp: '^\*'
line: "* -fstype=nfs {{ automount }}/&"
notify: Restart autofs
handlers:
- name: Configure LDAP Client
ansible.builtin.command: '/opt/local/bin/configure_ldap_client.sh'
- name: Restart autofs
ansible.builtin.sysvinit:
name: autofs
state: restarted
#!/bin/bash
if [[ -f '/var/ldap/ldap_client_cred' ]] ; then
ACTION='mod'
else
ACTION='manual'
fi
ldapclient $ACTION -a credentialLevel=proxy \
-a authenticationMethod=simple \
-a "proxyPassword={{ bind_pw }}" \
-a "proxyDN={{ bind_dn }}" \
-a defaultSearchBase={{ ldap_base }} \
-a serviceSearchDescriptor=passwd:ou=people,{{ ldap_base }} \
-a serviceSearchDescriptor=group:ou=groups,{{ ldap_base }} \
-a serviceAuthenticationMethod=pam_ldap:simple \
-a "preferredServerList={{ ldap_servers | join(' ') }}"
cp /etc/nsswitch.ldap /etc/nsswitch.conf
Final thoughts
In sum, I was really surprised that Ansible was able to do most tasks on these really old systems. I believe it is a viable solution for managing Solaris, especially if you are already familiar with Ansible. In the future I post any additional findings I have from running Ansible on Solaris 10.
As always, thank you for reading.










