Ansible Delphix

 

 

 

 

 

 

 

The DataOps movement is certainly gaining momentum and, as a key supporting tool,  the Delphix Dynamic Data Platform (DDDP) is quickly growing with it.  More and more large enterprises are deploying tens or even hundreds of Delphix engines throughout their data centres in all parts of the world.  With that comes the inevitable challenge of managing such an estate and specifically with Delphix, as an example, managing system configurations, user accounts and privileges that are locally configured on each engine becomes unwieldy and time consuming.  For instance, the enterprise may have a global support model with a dedicated team of support engineers who look after the day to day running of the estate.  The support team take on a new engineer who needs access to every engine to perform their role and we really don’t want to manually access each engine individually to add a new support user account.

In this post I’m going to look at one way of creating a centralised management solution for such a scenario.  It can then be developed further to achieve much more, such as mass deployment of source and target environment configurations, dSources, VDB’s and any other object management requirements you may have.

Introducing Ansible

I’m going to assume you’ve at least heard of Ansible and know what the tool is capable of but in a nutshell it’s a simple agentless automation engine used for a variety of tasks with configuration management being one of them.  The main reason it is a good fit for managing a large Delphix estate is its ability to perform repetitive tasks on many servers without the need to install agents on the servers, which we can’t on a Delphix engine (an appliance with no underlying o/s access for customers).

If you don’t have Ansible up and running in your shop already then installing and configuring it is a very simple process and a lightweight linux VM is more than adequate as a control host but in fact you can run it from your own laptop if necessary.  It’s also worth mentioning that Ansible is open source and free-to-use so there’s no procurement process to worry about (unless you use Ansible Tower, which is Red Hats licensable add-on GUI and scheduler).

Working With Ansible

There are several parts to Ansible that we need to create, develop and configure so lets break it down and take a look at each one individually before seeing how it all fits together.

Inventory Management

Normally Ansible uses an INI file that lists all of the machines it will manage and here you can group sets of machines together for easier management.  When a task runs against a machine Ansible copies a module (script) to the machine and runs the commands within the module.  However, our machines are Delphix engines where we only have GUI, CLI and API access therefore we must use a different method.

To overcome this we configure Ansible to run modules locally on the control machine (where we have Ansible installed) but then build the modules in a way that they perform API calls to the Delphix engines. We still need an inventory of engines though and a way to do that is to create a simple JSON or YAML file with entries for each and every engine in our estate and we can include login information too.

Here’s an example:

DX_ENV:
  US:
    CA:
      PROD:
        P_VE1:
          host: "virtengine01.example.com"
          ip: "10.0.0.11"
          delphix_admin: 
            id: "delphix_admin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE1.delphix_admin.pw}}"
          sysadmin: 
            id: "sysadmin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE1.sysadmin.pw}}"
        P_VE2:
          host: "virtengine02.example.com"
          ip: "10.0.0.12"
          delphix_admin: 
            id: "delphix_admin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE2.delphix_admin.pw}}"
          sysadmin: 
            id: "sysadmin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE2.sysadmin.pw}}"
        P_VE3:
          host: "virtengine03.example.com"
          ip: "10.0.0.13"
          delphix_admin: 
            id: "delphix_admin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE3.delphix_admin.pw}}"
          sysadmin: 
            id: "sysadmin"
            pw: "{{DX_ENV_VAULT.US.CA.PROD.P_VE3.sysadmin.pw}}"

You will notice the pw: values are references.  This is where we can ensure we are working securely and not exposing passwords in plain text files.  Ansible supports encrypted files using it’s vault functionality.  Again it is a simple text file but we use ansible-vault to encrypt the file so it can only be opened with a password.  Therefore the pw: values reference the actual values in the vault file.

Ansible Modules

Modules are discreet units of code used to perform certain tasks. Ansible has a vast array of modules supporting many technologies and use cases however none (that I’m aware of) that support Delphix engines.  I expect this is partly due to the way in which Ansible has to interact with Delphix appliances, making it an unusual and slightly more complicated process.  Therefore we have to create our own.

Modules can be created using any language you prefer.  Delphix provide a set of Python packages (called delphixpy), which is a way to call the Delphix API from Python, so by utilising delphixpy we can create our modules using this popular language.

As I explained earlier, when running a playbook, normally Ansible copies the module to the target server and executes it their but we can’t do that so we have to bear that in mind when creating our modules to use against a Delphix engine.  The modules instead will execute on the Ansible control machine so we have to perform API calls from that machine to the Delphix engine.  Therefore we first need to define functions to perform the session creation and login process.  From here we can carry on and perform whatever tasks we need via standard API calls using the delphixpy module.  A key thing to remember is to ensure the module is idempotent.

In a module used to create new user accounts on Delphix engines the main function could look something like this:

...
def main():
  module = AnsibleModule(
        argument_spec=dict(
            engine=dict(required=True, type='str'),
            user=dict(required=True, type='str'),
            domain=dict(required=False, type='str', default='DOMAIN'),
            passwd=dict(required=True, type='str', no_log=True),
            newusername=dict(required=False, type='str'),
            state=dict(required=False, choices=['absent', 'present'], default='present'),
            userdict=dict(required=True, type='dict'),
        ),
    )

  params = module.params
  engine = params['engine']
  engine_user = params['user']
  domain = params['domain']
  passwd = params['passwd']
  newusername = params['newusername']
  state = params['state']
  userdict = params['userdict']

  engine = login(engine, engine_user, passwd, domain)
  userVO = checkifnamedobjectexists(engine, user, userdict['name'])
  if userVO is None:
    if state == 'present':
      createuser(module, engine, userdict)
      module.exit_json(changed=True, msg="User created")
...

 

And here’s the login function called in that block:

def login(engine, user, passwd, domain):
  engine = DelphixEngine(engine, user, passwd, domain)
  return engine

 

And here’s part of the function used to create the user:

def createuser(module, engine, userdict):
  '''
  Function to create a user
  '''
  userVO = User()
  userVO.name = userdict['name']
  userVO.first_name = userdict['first_name']
  userVO.last_name = userdict['last_name']
  userVO.email_address = userdict['email_address']
  userVO.work_phone_number = userdict['work_phone_number']
  userVO.credential = PasswordCredential()
  userVO.credential.password = userdict['passwd']
  if 'user_type' in userdict:
    userVO.user_type = userdict['user_type']
  user.create(engine, userVO)

 

We’re utilising the delphixpy module by importing several packages:

#!/usr/bin/python27
import importlib
from delphixpy.v1_7_0.delphix_engine import DelphixEngine
from delphixpy.v1_7_0.web import user, authorization, role, domain
from delphixpy.v1_7_0.web.vo import User, PasswordCredential, Authorization, Role

 

I’m not showing the whole module here but just the important parts so you get an idea of how we can piece one together.

The Playbook

Playbooks are Ansibles configuration, deployment, and orchestration language and are how we instruct Ansible to manage and configure our Delphix engines.  The Ansible docs use a great analogy:

If Ansible modules are the tools in your workshop, playbooks are your instruction manuals…

Each playbook is composed of one or more ‘plays’ in a list.  To create reusable playbooks we can use Ansible roles where content is defined as a standard set of files including tasks.  Therefore to create a user account on several engines only requires a single play in our playbook but that play calls tasks in a task file, which is part of a role.

---
- name: Create Users on Delphix Engines
  hosts: localhost
  gather_facts: False
     
  roles:
    - { role: create_dx_users}

You will notice hosts: is set to localhost, because we are going to run our modules locally.

Building the Tasks

As just mentioned, playbooks run tasks either defined in the playbook or in a role.  I’ve structured my Ansible setup to use roles and therefore I have a file called main.yml located in the tasks directory of the role.  Remember, I want to create one or more user accounts on several Delphix engines, so how we define the task is very important.  If it were several user accounts on a single engine it is as simple as providing the account credentials for each account and Ansible will run through the list and create each account.  To do the same thing but on many engines we need some way of working through a list of engines as well as the list of accounts and that’s where “nested lists” come in.  The definition of a nested list from the docs is:

Takes the input lists and returns a list with elements that are lists composed of the elements of the input lists

I had to read that several times for it to sink in!  Essentially we’re creating a list from a list and working through each element.  Here’s what the task looks like:

- name: create users on multiple engines
  org_dx_user:
    engine="{{ item[0].ip }}"
    user="{{ item[0].delphix_admin.id }}"
    passwd="{{ item[0].delphix_admin.pw }}"
    state=present
    userdict={{item[1]}}
  with_nested:
  - "{{dx_config.engine_ref}}"
  - "{{dx_config.users}}"
  tags:
    users

This looks slightly more complicated because we’re using references (more on that in a moment) but you can see we have two parts to the task:

  1. Engine information and login details (item [0])
  2. New user credentials (item [1])

dx_config.engine_ref is a list of engines we want to work with.  The list uses references to the defined set of engines I showed at the beginning.  dx_config.users is the list of user accounts with credentials we want to configure.

Input File

Ansible uses variables defined using JSON or YAML to pass the information needed as input to perform the task.  We create a file containing all the variables and that is loaded by Ansible on execution – it’s kinda like an input file in our case, containing the information the Delphix API needs to login and create the user.  Using YAML, our var file will look something like this:

dx_config:

  engine_ref:
  - '{{DX_ENV.US.CA.PROD.P_VE1}}'
  - '{{DX_ENV.US.CA.PROD.P_VE2}}'
  - '{{DX_ENV.US.CA.PROD.P_VE3}}'


  users:
  - user_type: 'DOMAIN'
    DelphixAdmin: 'True'
    email_address: [email protected]
    first_name: Mick
    last_name: Fanning
    name: mfanning
    passwd: '{{USERS.VAULT.ADMINS.pw}}'
    work_phone_number: '123456789'
  - user_type: 'DOMAIN'
    DelphixAdmin: 'True'
    email_address: [email protected]
    first_name: John John
    last_name: Florence
    name: jjflorence
    passwd: '{{USERS.VAULT.ADMINS.pw}}'
    work_phone_number: '123456789'
  - user_type: 'DOMAIN'
    DelphixAdmin: 'True'
    email_address: [email protected]
    first_name: Kelly
    last_name: Slater
    name: kslater
    passwd: '{{USERS.VAULT.ADMINS.pw}}'
    work_phone_number: '123456789'

engine_ref is a list of engines to create the accounts on (as called in the task file) but these are references to the engine list (our inventory). Once again, passwd: is another reference from a vault file.

Bring It All Together

I’ve showed lots of different elements we need for Ansible to accomplish this use case and it probably feels a little disjointed right now so lets run through it again and see how it all comes together.

Ansible Flow

 

  • The playbook calls our role.
  • There are two files in group_vars, loaded at execution time.  One is the details of all the Delphix engines in our estate and the other is a vault file containing the engine passwords.
  • Within our role we have three sets of files:
    • The Python module in the library, which makes the API calls
    • A task file with the nested list
    • Vars contains two files, again loaded at execution time.  One comprises of references to the engines for this specific role as well as users we want to create, the other is another vault file with the passwords for the new user accounts.

When we run our playbook, Ansible will first load all the variables (from group_vars and vars) and then run the tasks in the task file iterating through every engine and running the module each time.  Simple (sort of!).

As is usually the case with Ansible, there can be many subtly different ways to structure the implementation of the Ansible process to achieve the same outcome.  There is often no single right way. I’ve outlined one such structure but it can be achieved in a variety of different ways and be as simple or complex as you make it.  The message I’ve hopefully gotten across here is that although Delphix engines are separate appliances and must be administered individually, using automation tools like Ansible and hooking into the Delphix API can greatly simplify the process.

Leave a Reply