Creating Multiple Users/Tenants

Walter Bentley

December 2015

In this article by Walter Bently, author of the book OpenStack Administration with Ansible, has discussed how to create our very first OpenStack administration playbook. The task of creating users and tenants for your OpenStack cloud is literally one of the first steps in setting up your cloud for user consumption. So, let's get started. We will see how one would manually do this first and then transition into creating a playbook with roles to fully automate it. While creating the playbook/role, I will try to highlight any possible concerns and flexible ways in which you can accomplish this using Ansible:

  • Creating users and tenants
  • Automation considerations
  • Coding the playbook and roles
  • The playbook and role review

(For more resources related to this topic, see here.)

Creating users and tenants

Creating new users and tenants seems like a simple task as a cloud operator/administrator, but it does become a burden if asked to create 10, 20, or 50 users and 5, 10, or 20 tenants. First, we create the user (with a corresponding complex secure password), we then create the tenant for the user, and finally, we link that user to that tenant while assigning that user with the appropriate role.

Imagine doing this over and over again. Boring! The first thing you learn as an administrator of anything is figure out what your daily tasks are and then determine how to get them completed as fast/easily as possible. This is exactly what we are going to do here.

Manually creating users and tenants

To further demonstrate the steps outlined earlier, we will walk you through the commands used to create a user and tenant.

For simplicity purposes, we will demonstrate the manual commands using the OpenStack CLI only.

Creating a user

Creating a user within OpenStack involves sending requests to the identity service (keystone). The keystone request can be executed by either first sourcing the OpenRC file, or by passing authentication parameters in line with the command (this is shown in the second example). Next, you would be responsible for providing the required parameter values, such as the username and password with the command. Let's take a look at the following example:

$ source openrc
$ keystone user-create --name=<username> --pass=<password>

Or we can also use this command:

$ keystone --os-username=<OS_USERNAME> --os-password=<OS_PASSWORD> --os-tenant-name=<OS_TENANT_NAME> --os-auth-url=<OS_AUTH_URL> user-create --name=<username> --pass=<password>

The output will look similar to this:

Creating a tenant

As mentioned previously, a tenant (also known as a project in current versions of OpenStack) is a segregated area of your cloud where you can assign users. This user can be restricted to just that tenant or allowed access to multiple tenants. The process of creating a tenant is similar to the user creation process mentioned earlier. You can continue to execute CLI commands once you source the OpenRC file or pass authentication parameters with each command. Imagine that the OpenRC file was already sourced, as shown in the following example:

$ keystone tenant-create --name=<tenant name> --description="<tenant description>"

The output will look similar to this:

Assigning users role and tenant access

Using the keystone service, you can assign a specific role (user permissions) to a designated tenant for the user that you just created. There are default roles that come with a base OpenStack cloud: admin and _member_. You can also create custom roles as well. You would need the name of the role and tenant that you wish to assign to the user. If the OpenRC file is still sourced, refer to the following example:

$ keystone user-role-add --user=<username> --tenant=<tenant name> --role=<role name>

The output will look similar to this:

At this point, you would have created a user and tenant and assigned that user to the tenant with a role; this is all done manually. Let's move forward to review some of the considerations around the thought of automating all the steps, as mentioned earlier.

Automation considerations

The idea of taking a manual task and creating an automation script, no matter the automation tool, requires some base framework decisions to be made. This is to keep consistency within your code and allow the easy adoption of your code by someone else. Ever tried using scripts created by someone else and they had no code standards? It is confusing, and you waste a lot of time attempting to understand their approach.

In our case, we are going to make some framework decisions ahead of time and keep this consistency. My biggest disclaimer before we get started with reviewing the considerations in order to set our framework decisions is as follows:

There are many ways to approach automating tasks for OpenStack with Ansible. The playbooks/roles are intended to be working examples, so you can use as is or adjust/improve for your personal use cases.

Now that this has been said, let's get on with it.

Defining variables globally or per roles

This topic may not seem important enough to be reviewed, but in reality, with Ansible, you have more options than usual. With that thought, you will have to decide how you will define your variables within the roles.

Ansible has a variable definition hierarchy that it follows. You have the option to define the value of a variable placed within your playbook/role globally and assign it to a group of hosts or locals to that specific role only. Defining the value globally would mean that all playbooks/roles can use this value and apply it to a group of hosts. If you set the value locally to that role, only that role would have access to the variable value.

Globally defined variable values will be defined in a file located in the group_vars/ directory of the playbook. The filename will have to match a group name set in the hosts file. The advantage of this approach is that you can set the variable value once and have your playbooks/roles just reuse the value. This simplifies the process of defining variables overall and the tasks of updating the values as needed. The disadvantage of this approach is that if you wish to reuse a variable name and want to provide a different value on a per role basis. This is where the alternative option comes into play.

Defining the variable value locally to the role allows the reuse of a variable name and the capability to define different values for that variable. Through my experience of creating playbooks/roles against an OpenStack cloud, I have found out that defining the variables locally to the role seems to be the best option. My overall approach to creating roles is to create roles that are as simple as possible and to accomplish a single administrative task. Try not to combine multiple administrative tasks into a single role. Keeping the role simple enables the ability to reuse the role and be inline with Ansible's best practices.

So, the first framework decision that we make here is we define variable values locally to the role. Now, we can move on to the next consideration/decision point, which is whether to use the OpenStack API or CLI to execute administrative commands.

OpenStack API or CLI?

Again, this decision may seem unimportant at a high level. Deciding whether to use the OpenStack API or CLI could drastically change the overall structure and approach to creating your playbooks/roles.

One thing that stands out is that the CLI is much easier to use and code than Ansible. Keep in mind that the CLI still executes API commands behind the scenes, dealing with all the token and JSON interpretation stuff for you. This allows zero loss in the functionality.

The second framework decision that we make is we create roles to make calls to your OpenStack cloud using the CLI commands. With this decision, we then need to standardize the way in which we will authorize each CLI command. While I would like to say that I am not a fan of adding extra syntaxes to commands, this by far outweighs the alternative of making API calls in the automation code.

For each CLI command, you will find the following authorization strings with defined variables:

--os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}

The good news is that these variables are the only variables defined as global variables, and they only need to be updated in one place. Now, we need to decide from where do we wish to execute the playbooks.

Where to run Ansible

My next statement may be a bit obvious, but the playbooks need to be executed from a workstation/server with Ansible installed. Now that we have this out of the way, let's explore our options:

  • I would recommend that you do not run the playbooks directly from any of the OpenStack controller nodes. The controller nodes have plenty of work to do in order to just keep OpenStack going, so no need to add an additional burden.
  • The other option is to execute the playbooks from some sort of centralized Ansible server in your environment. While this is a totally viable option, I have one better for you.

Since I am a huge fan and advocate of the OpenStack Ansible Deployment (OSAD) method of deploying OpenStack, the playbooks/roles out of the box will use some of the great features offered by OSAD. My last sentence may seem a bit off topic, but it will make more sense shortly.

One of the greatest features that come with running OSAD is the built-in dynamic inventory script. This feature removes your burden of keeping the inventory of your OpenStack service locations in a hosts file. In order to benefit from this feature, you would need to execute the playbooks/roles from the OSAD deployment server, which in the big picture makes sense to keep all the Ansible playbooks/roles together (deployment and administration scripts).

The other compelling reason why this is the best option is that the OSAD deployment server is already set up to communicate with the LXC containers, where the OpenStack services are located. This point is very important when/if you want to make OpenStack service configuration changes to Ansible.

The last feature of OSAD that I would like to highlight is the fact that it comes with a container designated just to administer your OpenStack cloud called the utility container. This container comes with every OpenStack service CLI package installed, and you're ready to go. Yes, one less thing for you to worry about. This is why I love OSAD.

Our last framework decision that we make is we execute the playbooks from the OSAD deployment server in order to take full advantage of all the features OSAD offers us (it just feels right). Now that we are all armed with tons of good information and coding framework, all we have to do now is create our first playbook and roles.

Coding the playbook and roles

Before we start, we should first refer to the beginning of this article. We outlined the steps to create users and tenants within your OpenStack cloud. Here they are again for a quick reference:

  • Creating the user (with a corresponding complex secure password)
  • Creating the tenant for the user
  • Linking the user to the tenant while assigning that user with the appropriate role

The first step is to tackle the user creation portion of the process. Creating a user is a simple task in OpenStack, so why not add some administration flares to go along with it. Part of the process of creating a user is to assign that user an appropriate password. We will include this as part of the role that creates the user and tenant that we will assign the user to.

When creating a playbook, I normally start with creating roles to handle the administrative tasks needed. The role will contain all the executable code against OpenStack cloud. The playbook will contain the host to run the role against (in this case, the utility container) the role(s) to be executed and other execution settings. The role to handle this administrative task will be named create-users-env.

The directory structure of our playbook will look like this:

base.yml             # master playbook for user creation
group_vars/
   util_container     # assign variable values for this host group
hosts                 # static host inventory file
roles/
   create-users-env   # user/tenant creation role
     tasks/
         main.yml     # tasks file for this role
     vars/
         main.yml     # variables associated with this role

Since we will start with the role task file assembly, let's create the main.yml file in the create-users-env/tasks directory. The contents at the beginning of this file will look like this:

---

- name: Install random password generator package

apt: name={{item}} state=present

with_items:

   - apg

- name: Random generate passwords

command: apg -n {{ pass_cnt }} -M NCL -q

register: passwdss

- name: Create users

command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}

          user-create --name={{ item.0 }} --pass={{ item.1 }}

with_together:

   - userid

   - passwdss.stdout_lines

Now, we can go through the three tasks that were just added to the role, as mentioned previously, in more detail. The first task sets the groundwork to use the apg package, which generates several random passwords, as shown in the following code

- name: Install random password generator package
apt: name={{item}} state=present
with_items:
   - apg

Since in the second task, we will use the apg package to generate passwords for us, we need to make sure that it is installed on the host that executes the playbook/role. The apt module from Ansible is a very useful tool used to manage Debian/Ubuntu packages. Defining the {{item}} parameter value with the module allows us to loop through multiple packages listed in the with_items statement. In this particular case, it is not needed as we are only installing one package but at the same time, does us no harm. Moving on to the second task:

- name: Random generate passwords
command: apg -n {{ pass_cnt }} -M NCL -q
register: passwdss

The second task will execute the apg package using the command module from Ansible.

The command module is one of the most commonly used modules when working with Ansible. Basically, it can handle the execution of any command/package with the exception of any commands that will utilize shell variables and shell specific operations, such as: <, >, |, and &.

With the command module, we pass the apg command with specific parameters, such as -n {{ pass_cnt }} -M NCL -q. Most of the parameters are standard options for apg, with the exception of the {{ pass_cnt }} variable. Setting this parameter allows us to adjust the number of passwords generated from the variable file set for this role (found in the create-users-env/vars directory). We will examine the variable file shortly. One of the last steps in this task is to register the output of the apg command into a variable named passwdss. This variable will be used later in this role.

The third task that is added to the role will now create the user in your OpenStack cloud. As shown in the following code, using the command module, we will execute the keystone command to create a user with authentication parameters:

- name: Create users
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
           user-create --name={{ item.0 }} --pass={{ item.1 }}
with_together:
   - userid
   - passwdss.stdout_lines

In the user-create command, we will also define a few variables that can be used:

{{ item.0 }}   # variable placeholder used to set the usernames from the list defined in the userid variable

{{ item.1 }}   # variable placeholder used to read in the output from the apg command found within the passwdss variable registered earlier

Placing variables in your commands sets you up for creating roles with core code that you will not have to update every time it is used. A much simpler process is to just update variable files instead of continuously altering role tasks.

The other special part of this task is the use of the with_together Ansible loop command. This command allows us to loop through separate sets of variable values, pairing them together in the defined order. Since the passwords are random, we need not care about what user gets which password.

So, now that we have our user creation code in the role, the next step is to create the user's tenant. The following two tasks are as follows:

- name: Create user environments
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
           tenant-create --name={{ item }} --description="{{ item }}"
with_items: tenantid
- name: Assign user to specified role in designated environment
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
          user-role-add --user={{ item.0 }} --tenant={{ item.1 }} --role={{ urole }}
with_together:
   - userid
   - tenantid

The first preceding task will create the tenant with the tenant-create keystone command. The tenant name and description will come from the tenantid variable. The next task, as mentioned previously, will then assign the user that we created earlier to this newly created tenant with a role value set by the urole variable.

You will notice that these tasks are very similar to the previous tasks that were used to create the user and also use similar Ansible parameters. Again, we will use the command module that makes keystone commands using variable placeholders. As you can see, it will begin to form a repeated pattern. This really helps the code creation.

The last task of the role will simply provide an output of the users created and their corresponding passwords. This step will give you (as the Cloud operator) a very simple output with all the information that you would need to save and/or pass on to the Cloud consumer. While this step is not required to complete the overall administrative task, it is a nice to have it. See the task following task:

- name: User password assignment
debug: msg="User {{ item.0 }} was added to {{ item.2 }} tenant, with the assigned password of {{ item.1 }}"
with_together:
   - userid
   - passwdss.stdout_lines
   - tenantid

In this task, we will use the debug module to show the output of the variable that we set either manually or dynamically using the register Ansible command. The output will look something like this:

Believe it or not, you have just created your first OpenStack administration role. To support this role, we now need to create the variable file that will go along with it. The main.yml variable filename, located in the create-users-env/vars directory, is very similar to the task file in structure.

Keep in mind that the values defined in the variable file are intended to be changed before each execution for normal every day use.

The values shown in the following example are just working examples. Let's take a look at them:

---
pass_cnt: 10
userid: [ 'mrkt-dev01', 'mrkt-dev02', 'mrkt-dev03', 'mrkt-dev04', 'mrkt-dev05', 'mrkt-dev06', 'mrkt-dev07', 'mrkt-dev08', 'mrkt-dev09', 'mrkt-dev10' ]
tenantid: [ 'MRKT-Proj01', 'MRKT-Proj02', 'MRKT-Proj03', 'MRKT-Proj04', 'MRKT-Proj05', 'MRKT-Proj06', 'MRKT-Proj07', 'MRKT-Proj08', 'MRKT-Proj09', 'MRKT-Proj10' ]
urole: _member_

Let's take a moment to break down each variable. The summary will be as follows:

pass_cnt   # with the value of 10, we would be creating 10 random passwords with apg
userid     # the value is a comma delimited list of users to loop thru when executing the user-create Keystone command
tenanted   # the value is a comma delimited list of tenant names to loop thru when executing the tenant-create Keystone command
urole     # with the value of _member_, the user would be assigned the member role to the tenant created

This pretty much concludes what is involved when creating a variable file. We can now move on to the base of this playbook and create the master playbook file named base.yml, which is located in the root of the playbook directory. The contents of the base.yml file will be as follows:

---
# This playbook used to demo OpenStack Juno user, role and tenant features.
- hosts: util_container
user: root
remote_user: root
sudo: yes
roles:
- create-users-env

The summary of this file is as follows:

hosts       # the host or host group to execute the playbook against

user         # the user to use when executing the playbook locally

remote_user # the user to use when executing the playbook on the remote host(s)

sudo         # will tell Ansible to sudo into the above user on the remote host(s)

roles       # provide a list of roles to execute as part of this playbook

The last two areas that we need to focus on before we complete the playbook and make it ready for execution are to create the host inventory file and the global variable file. As we are using the static inventory file, we will have to discover the name and/or IP address of the utility container.

This can be accomplished by running the following command on any of the controller nodes:

$ lxc-ls –fancy

Then, we look for something similar to the highlighted item in the output:

Then, add the utility container's IP address to the hosts file, as follows:

[localhost]
localhost ansible_connection=local
[util_container]
172.29.236.199

Last but not least, you will create the global variable file inside the group_vars/ directory. Remember that the file must be named to match the host or host group defined in the master playbook. Since we called the util_container host group, we must then name the variable file with the exact same name. The contents of the util_container global variable file will be as follows:

# Here are variables related globally to the util_container host group

OS_USERNAME: ansible
OS_PASSWORD: passwd
OS_TENANT_NAME: admin
OS_AUTH_URL: http://172.29.236.7:35357/v2.0

ProTip

Always create/use an automation service account when executing commands against a system remotely. Never use the built-in admin and/or your personal account for that system. The use of service accounts is useful for simple troubleshooting and system audits.

You will recall that, earlier in this article, we spoke about the authorization string needed to use the CLI commands. Here they are again as a reminder:

--os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}

The variable placeholder that we set in the roles task file relates back to this global variable file. This is where the values for the variables that we set are stored.

A word of caution

Due to the contents of this file, it should be stored as a secure file in whatever code repository you may use to store your Ansible playbooks/roles. Gaining access to this information can compromise on your OpenStack cloud security.

Guess what, you made it! We just completely finished our first OpenStack administration playbook and role. Let's finish this article with a quick overview of the playbook and role that we just created.

The playbook and role review

To get started, we can start from the top with the role we created called create-users-env. The completed role and file named main.yml is located in the create-users-env/tasks directory, which looks like this:

---
- name: Install random password generator package
apt: name={{item}} state=present
with_items:
   - apg
- name: Random generate passwords
command: apg -n {{ pass_cnt }} -M NCL -q
register: passwdss
- name: Create users
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
           user-create --name={{ item.0 }} --pass={{ item.1 }}
with_together:
   - userid
   - passwdss.stdout_lines
- name: Create user environments
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
           tenant-create --name={{ item }} --description="{{ item }}"
with_items: tenantid
- name: Assign user to specified role in designated environment
command: keystone --os-username={{ OS_USERNAME }} --os-password={{ OS_PASSWORD }} --os-tenant-name={{ OS_TENANT_NAME }} --os-auth-url={{ OS_AUTH_URL }}
          user-role-add --user={{ item.0 }} --tenant={{ item.1 }} --role={{ urole }}
with_together:
   - userid
   - tenantid
- name: User password assignment
debug: msg="User {{ item.0 }} was added to {{ item.2 }} tenant, with the assigned password of {{ item.1 }}"
with_together:
   - userid
   - passwdss.stdout_lines
   - tenantid

The corresponding variable file named main.yml, located in the create-users-env/vars directory, for this role will look like this:

---

pass_cnt: 10

userid: [ 'mrkt-dev01', 'mrkt-dev02', 'mrkt-dev03', 'mrkt-dev04', 'mrkt-dev05', 'mrkt-dev06', 'mrkt-dev07', 'mrkt-dev08', 'mrkt-dev09', 'mrkt-dev10' ]

tenantid: [ 'MRKT-Proj01', 'MRKT-Proj02', 'MRKT-Proj03', 'MRKT-Proj04', 'MRKT-Proj05', 'MRKT-Proj06', 'MRKT-Proj07', 'MRKT-Proj08', 'MRKT-Proj09', 'MRKT-Proj10' ]

urole: _member_

Next, the master playbook file named base.yml, located in the root of the playbook directory, will look like this:

---
# This playbook used to demo OpenStack Juno user, role and tenant features.
- hosts: util_container
user: root
remote_user: root
sudo: yes
roles:
- create-users-env

Following this, we created the hosts file, which is also located in the root of the playbook directory:

[localhost]
localhost ansible_connection=local
[util_container]
172.29.236.199

Lastly, we wrapped this playbook by creating the global variable file named util_container and saving it to the group_vars/ directory of the playbook:

# Here are variables related globally to the util_container host group

OS_USERNAME: ansible

OS_PASSWORD: passwd

OS_TENANT_NAME: admin

OS_AUTH_URL: http://172.29.236.7:35357/v2.0

As promised earlier, I felt it was very important to provide fully working Ansible playbooks and roles for your consumption. You can use them as is and/or as a springboard to create new/improved Ansible code. The code can be found in the GitHub repository available at https://github.com/os-admin-with-ansible/os-admin-with-ansible.

Now, of course, we have to test our work. Assuming that you have cloned the preceding GitHub repository, the command to test the playbook from the deployment node will be as follows:

$ cd os-admin-with-ansible
$ ansible-playbook –i hosts base.yml

Summary

In this article we discussed how Ansible really does a great job in streamlining the efforts involved in automating OpenStack administrative tasks. You can now reuse that role over and over again, reducing the amount of time required to create users and tenants to single digit minutes. The time investment is well worth the benefits.

We covered how to create users and tenants in OpenStack via the API and CLI, gathered an understanding of basic automation considerations, and developed the Ansible playbook and role to automate the user and tenant creation.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

OpenStack Administration with Ansible

Explore Title