This blog post is not an intro to what is Ansible and how to use it, but it's rather an example of how a simple task can be automated using Ansible in a way that's scalable, flexible and easily reusable, and also provides the ability for other tasks to pick up the new passwords from a secure password store.
- hosts: ora-serv01
remote_user: oracle
tasks:
- name: change password for SYS
shell: |
. TEST1.env && \
sqlplus / as sysdba @change_pasword.sql SYS \
\"\"I chose to do the implementation using Ansible 2.3, because it introduces the passwordstore lookup, which enables interaction with the pass utility (read more about it in Passwordstore.org). pass is very cool. It store passwords in gpg-encrypted files, and it can also be configured to automatically update the changes to a git repository, which relieves us of the headache of password distribution. The password can be retrieved from git on the servers that need the access to the new passwords. Ansible 2.3 runs on python 2.6, unfortunately, the passwordstore lookup requires Python 2.7, which can be an issue if the control host for Ansible runs on Oracle Linux 6 or RHEL 6, as they don't provide Python 2.7 in the official yum repositories. Still, there are ways of getting it done, and I'll write another blog post about it. So, what we'll need is:
pip install jmespath)pip install -U jinja2 in few cases)pass utilityThis is the whole list of files that are included in the playbook: ./chpwd.yml ./inventory/hosts ./inventory/orcl1-vagrant-private_key ./inventory/orcl2-vagrant-private_key ./roles/db_users/files/change_password.sql ./roles/db_users/files/exists_user.sql ./roles/db_users/defaults/main.yml ./roles/db_users/tasks/main.yml [/code]
Let's take a quick look at all of them:
$ cat ./chpwd.yml --- - name: password change automation hosts: all roles: - db_users $ cat ./inventory/hosts [orahosts] orcl1 ansible_host=127.0.0.1 ansible_port=2201 ansible_ssh_private_key_file=inventory/orcl1-vagrant-private_key ansible_user=vagrant orcl2 ansible_host=127.0.0.1 ansible_port=2202 ansible_ssh_private_key_file=inventory/orcl2-vagrant-private_key ansible_user=vagrant $ cat ./roles/db_users/files/change_password.sql set ver off pages 0 alter user &1 identified by "&2"; exit;$ cat ./roles/db_users/files/exists_user.sql set ver off pages 0 select 'User exists.' from all_users where username=upper('&1') union all select 'User '||upper('&1')||' does not exist.' from (select upper('&1') from dual minus select username from all_users); exit;$ cat ./roles/db_users/defaults/main.yml --- db_users: - name: TEST1 host: orcl1 env: ". ~/.bash_profile && . ~/TEST1.env > /dev/null" pwdstore: "orcl1/TEST1/" os_user: oracle become_os_user: yes users: - dbsnmp - system - name: TEST2 host: orcl2 env: ". ~/.bash_profile && . ~/TEST2.env > /dev/null" pwdstore: "orcl2/TEST2/" os_user: oracle become_os_user: yes users: - sys - system - ctxsys - name: TEST3 host: orcl2 env: ". ~/.bash_profile && . ~/TEST3.env > /dev/null" pwdstore: "orcl2/TEST3/" os_user: oracle become_os_user: yes users: - dbsnmp--- - name: populate host_db_users set_fact: host_db_users="[]"with_items: "", but in that case, the outputs become cluttered as the attributes are displayed during the execution.- name: create directory for target on db hosts file: path: "ansible" state: directory become_user: "{{item.os_user}}" become: "{{item.become_os_user}}" with_items: "[]"- name: copy sql scripts to db_hosts copy: src="" dest=ansible/ mode=0644 become_user: "" become: "" with_nested: - "[]" - ['files/change_password.sql','files/exists_user.sql']- name: verify user existence shell: | && \ sqlplus -S / as sysdba \ @ansible/exists_user.sql register: exists_output become_user: "" become: "" with_subelements: - "" - users failed_when: false changed_when: false- name: User existence results fail: msg="{{item}}" with_items: "[]" #failed_when: false- name: generate and change the user passwords shell: | && \ sqlplus -S / as sysdba \ @ansible/change_password.sql \ \"\" register: change_output become_user: "" become: "" with_subelements: - "" - users failed_when: false changed_when: "'User altered.' in change_output.stdout"# fail if the password change failed. - name: Password change errors fail: msg="{{item}}" with_items: "[]"Now, when you know how it's built - it's time to show how it works! Please pay attention to the following:
pass command$ pass Password Store $ time ansible-playbook -i inventory/hosts chpwd.yml PLAY [pasword change automation] ******************************************************* TASK [Gathering Facts] ***************************************************************** ok: [orcl1] ok: [orcl2] TASK [db_users : populate host_db_users] *********************************************** ok: [orcl1] ok: [orcl2] TASK [db_users : create directory for target on db hosts] ****************************** changed: [orcl1] => (item={'become_os_user': True, 'os_user': u'oracle'}) changed: [orcl2] => (item={'become_os_user': True, 'os_user': u'oracle'}) TASK [db_users : copy sql scripts to db_hosts] ***************************************** changed: [orcl1] => (item=[{'become_os_user': True, 'os_user': u'oracle'}, u'files/change_password.sql']) changed: [orcl2] => (item=[{'become_os_user': True, 'os_user': u'oracle'}, u'files/change_password.sql']) changed: [orcl1] => (item=[{'become_os_user': True, 'os_user': u'oracle'}, u'files/exists_user.sql']) changed: [orcl2] => (item=[{'become_os_user': True, 'os_user': u'oracle'}, u'files/exists_user.sql']) TASK [db_users : verify user existance] ************************************************ ok: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'sys')) ok: [orcl1] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST1.env > /dev/null'}, u'dbsnmp')) ok: [orcl1] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST1.env > /dev/null'}, u'system')) ok: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'system')) ok: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'ctxsys')) ok: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'env': u'. ~/.bash_profile && . ~/TEST3.env > /dev/null'}, u'dbsnmp')) TASK [db_users : User existance results] *********************************************** TASK [db_users : generate and change the user passwords] ******************************* changed: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl2/TEST2/', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'sys')) changed: [orcl1] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl1/TEST1/', 'env': u'. ~/.bash_profile && . ~/TEST1.env > /dev/null'}, u'dbsnmp')) changed: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl2/TEST2/', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'system')) changed: [orcl1] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl1/TEST1/', 'env': u'. ~/.bash_profile && . ~/TEST1.env > /dev/null'}, u'system')) changed: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl2/TEST2/', 'env': u'. ~/.bash_profile && . ~/TEST2.env > /dev/null'}, u'ctxsys')) changed: [orcl2] => (item=({'become_os_user': True, 'os_user': u'oracle', 'pwdstore': u'orcl2/TEST3/', 'env': u'. ~/.bash_profile && . ~/TEST3.env > /dev/null'}, u'dbsnmp')) TASK [db_users : Password change errors] *********************************************** PLAY RECAP ***************************************************************************** orcl1 : ok=6 changed=3 unreachable=0 failed=0 orcl2 : ok=6 changed=3 unreachable=0 failed=0 real 0m12.418s user 0m8.590s sys 0m3.900s $ pass Password Store |-- orcl1 | |-- TEST1 | |-- dbsnmp | |-- system |-- orcl2 |-- TEST2 | |-- ctxsys | |-- sys | |-- system |-- TEST3 |-- dbsnmp $ pass orcl1/TEST1/system HDecEbjc6xoO lookup_pass: First generated by ansible on 26/05/2017 14:28:50
For past 2 months I've been learning Ansible and trying it for various DBA tasks. It hasn't always been a smooth ride, as I had to learn quite a lot, because I wasn't exposed much to beasts like jinja2, json_query, YAML, python (very handy for troubleshooting) and Ansible itself before. I feel that my former PL/SQL coder's experience had created some expectations from Ansible, that turned out not to be true. The biggest challenges to me were getting used to the linear execution of the playbook (while with PL/SQL I can call packages, functions, etc. to process the data "outside" the main linear code line), and the lack of execution feedback, because one has to learn creating Ansible tasks in a way that they either succeed or fail (no middle states like 'this is a special case - process it differently'), as well as the amount of visual output is close to none - which does make sense to some degree, it's "automation" after all, right? Nobody should be watching :)
A separate struggle for me was working with the complex data structure that I created for storing the host/database/user information. It's a mix of yaml "dictionary" and "list", and it turned out to be difficult to process it in a way I wanted - this is why I used the json_query at times (although not in a very complex way in this case). There are probably simpler ways I didn't know of (didn't manage finding), and I'd be glad if you'd let me know of possible improvements or even other approaches to such tasks that you have worked on and implemented. Despite all the complaining above, I think it's really worth investing time in automating tasks like this, it really works and once done it doesn't require much attention.
Happy Automating!
Ready to optimize your Oracle Database for the future?