Usage¶
In this section we’ll cover how to use the storage role, the different operations available, their return values, how to address resources in the operations, and several examples.
One of the biggest differences between the Storage Role and other roles is that in this role it is recommended to include your storage tasks on the consumer nodes, even if part of the tasts are actually executed by the controller.
Instead of creating a task for the controller node to create as many volumes as consumer nodes we have and store the results in variables (or use a naming template), and then on the consumer nodes have a task that attaches one of those volumes to each node, we just have a task on the consumers to create the volume and connect it.
This way there’s no need for variables or naming templates, and the creation and attaching tasks are together. This helps simplify the playbooks and the number of variables we have to move around in our playbooks, resulting in greater readability.
Configuration¶
The role needs to know what type of node we are defining, this is done using the node_type parameter. Acceptable values are controller and consumer. The default being consumer.
Note
When a node acts as controller and consumer we have to define it as two separate role entries. There is no controller-consumer or all node types.
Here’s an example of how to configure a node to be the controller and a consumer.
- hosts: storage_controller
vars:
[ ... ]
roles:
- { role: storage, node_type: controller }
- { role: storage, node_type: consumer }
For a controller node, the role needs to know the backends it’s going to be managing in order to set them up. A single controller node can manage multiple backends, which are configured using the storage_backends variable.
The keys of the storage_backends dictionary define the IDs of the backends and must be preserved between runs to be able to access previously provisioned resources. If we change the backend IDs (key in the dictionary) we will no longer be able to access older resources.
The value part of each entry in the storage_backends dictionary corresponds to another dictionary, this one with the configuration of the specific backend. The key-value pairs in this dictionary will vary from one provider to another. The only shared key between them is the provider key used to select the provider we want to use for this backend.
The default value for the provider key is cinderlib, which is the default provider. When using the default value it is common practice to not include the provider key from the configuration.
We can have backends from different providers configured on the same controller node. For example, we can have one using the default provider and another using the cinderclient provider.
- hosts: storage_controller
vars:
storage_backends:
backend1:
[ ... ]
backend2:
provider: cinderclient
[ ... ]
roles:
- { role: storage, node_type: controller }
A list of available parameters we can pass to each provider can be found in the providers’ section.
Attention
Controller nodes must always be defined and setup in the playbooks before any storage can be used on a consumer node.
Resource addressing¶
In this section we’ll cover the rules that are applied by the role to locate resources for the purposes of idempotency and resource addressing.
The storage role is modestly smart about locating resources, reducing the amount of information required to pass on task.
Volumes, which are the primary resource available at this moment, have the following attributes:
- resource: Type of the resource, must be volume.
- backend: Backend id.
- provider: Provider for the backend.
- host: Who “owns” this backend.
- id: UUID for the resource.
- name: User defined identifier for the volume.
- size: Size of the volume in GBi.
The way providers identify resources is by applying the parameters passed to tasks as if they were filters. If the result of applying the filters returns more than one resource, the provider will return an error.
For single backend controllers there’s no need to pass backend or provider parameters, as they will default to the only configured backend. If we have configured multiple backends and at lest one of them is the default provider, then it will default to the first backend that was added. If there are multiple backends and none of them uses the default provider, then the role won’t be able to determine a default value for these parameters.
Default value for host is the FQDN of the consumer node. Thanks to this, if we create resources as recommended, in a task on the consumer node, we won’t need to create complicated templates to address volumes when performing tasks on multiple consumers.
Now that we know the basics of addressing resources it’s probably best to have a look at examples of how it affects operations. In each one of the Operations we’ll present different addressing situations using the backends defined in the previous Configuration section, where we have 2 backends:
- backend1 using the cinderlib provider.
- backend2 using the cinderclient provider.
Operations¶
Create¶
The most basic, and most common, operation is creating a volume on a backend, which is accomplished by setting the state of a volume resource to present. The default state for a volume is present, so there’s no need to pass it. There are only 2 required attributes that must be passed on a create task: resource and size.
The task provides the following keys in the returned value at the root level:
Key | Contents |
---|---|
type | Type of resource. Now it can only be volume. |
backend | ID of the backend where the volume exists. Matches the key provided in storage_backends. |
host | Who “owns” this backend. |
id | Resource’s ID generated by the provider. Most providers use a UUID. |
name | User defined identifier for the volume. |
size | Size of the volume in GBi. |
Here’s the smallest task that can be used to create a volume:
- storage:
resource: volume
size: 1
We only have 2 backends, and only one of them uses the default provider, so following the addressing rules the volume will be created on backend1. This create task is equivalent to:
- storage:
resource: volume
state: present
size: 1
backend: backend1
provider: cinderlib
If we wanted to create the volume on backend2, we would have to specify the backend or the provider. Passing the provider is also enough as there’s only 1 backend for each provider:
- storage:
resource: volume
size: 1
backend: backend2
The rest of the parameters will use defaults (state: present) or be detected automatically based on provided parameters (provider: cinderclient).
Creating these 2 volumes on the same node doesn’t require any additional parameters as each one is going to different backends:
- storage:
resource: volume
size: 1
- storage:
resource: volume
size: 1
backend: backend2
But if we try to do the same to create 2 volumes of the same size on the same backend like this:
- storage:
resource: volume
size: 1
- storage:
resource: volume
size: 1
We will end with only 1 volume, as the second call will be considered as a repeated call by the controller node. And since these are idempotent operations no new volume will be created.
To create multiple volumes of the same size on the same backend we need to use the name attribute. Providing it just in one of the tasks is enough, but we recommend passing it to both:
- storage:
resource: volume
size: 1
name: first-volume
- storage:
resource: volume
size: 1
name: second-volume
If each one of our volumes has a different size, then we don’t need to provide a name, as one call cannot be mistaken for the other:
- storage:
resource: volume
size: 1
- storage:
resource: volume
size: 2
Delete¶
Deleting a specific volume is accomplished by setting the state of a volume resource to absent. And there are no required parameters for this call, but we can provide as many as we wan to narrow the volume we want to delete to a single one.
The delete task only returns the changed key to reflect whether the volume was present, and therefore was deleted, or if it wasn’t present in the first place.
To reference a volume for deletion we usually use the same parameters that were used on the create task. If we didn’t pass any parameters on create, passing none as well on delete will remove that volume:
- storage:
resource: volume
size: 1
- storage:
resource: volume
state: absent
Warning
There is no confirmation required to delete a volume, and once the volume is deleted it is usually impossible to recover its contents, so we recommend specifying as may parameters as possible on deletion tasks.
We don’t need to provide the same parameters that we used on the create method as long as we provide enough information. We can use the return value from the create task to do the addressing:
- storage:
resource: volume
size: 1
name: my_volume
backend: backend2
register: volume
- storage:
resource: volume
state: absent
id: "{{volume.id}}"
backend: "{{volume.backend}}"
Note
Keep in mind that there is no global database that stores all the resources IDs. So when using multiple backends, even if an ID uniquely identifies a resource in all your backends, the Storage Role has no way of knowing on which backend it is, so the task needs enough parameters to locate it. That’s why in the example above we pass the backend parameter to the delete task.
When describin the create task we saw how we could create 2 volumes without a name because they had different sizes. If we wanted to remove those volumes we would have to provide the sizes on the delete task, otherwise the task would fail because there are 2 volumes that matches the addressing.
- storage:
resource: volume
size: 1
- storage:
resource: volume
size: 2
- storage:
resource: volume
state: absent
size: 1
- storage:
resource: volume
state: absent
size: 2
Extend¶
Extending the size of a specific volume is accomplished by setting the state of a volume resource to extended. There is only one required parameters for this call, size which indicates the new size of the volume.
The task provides the following keys in the returned value at the root level:
Key Contents ================= ============================================================ changed Following standard rules, will be False if the volume was
already connected, and True if it wasn’t but now it is.
- type Describes the type of device that is connected, which at the
- moment can only be block.
path Path to the device that has been added on the system. additional_data (Optional) Provider specific additional information. ================= ============================================================
If we only have 1 volume on the node the addressing for the connect task is minimal.
- storage:
resource: volume
size: 1
- storage:
resource: volume
state: connected
Creating and connecting a volume is usually just the first step in our automation, and following tasks will rely on the path key of the returned value to use the volume on the consumer node.
- storage:
resource: volume
size: 1
register: vol
- storage:
resource: volume
state: connected
register: conn
- debug:
msg: "Volume {{vol.id}} is now attached to {{conn.path}}"
Disconnect¶
Disconnecting a volume from a node is a multi-step process that undoes the steps performed during the connection in reverse. The consumer node detaches the volume from the node, and then the controller unmaps and removes the exported volume. These steps are opaque to the playbooks, where they are seen as a single task.
Disconnecting a specific volume from a node is accomplished by setting the state of a volume resource to disconnected. There are no specific parameters for the disconnect task. All parameters are used for the addressing of the volume. Addressing rules explained before apply here.
The disconnect task only returns the changed key to reflect whether the volume was present, and therefore was disconnected, or if it wasn’t present in the first place.
Note
Disconnecting a volume will properly flush devices before proceeding to detach them. If it’s a multipath device, the multipath will be flushed first and then the individual paths. If flushing is not possible due to connectivity issues the volume won’t be disconnected.
When we using a single volume the disconnect doesn’t need any additional parameters:
- storage:
resource: volume
size: 1
- storage:
resource: volume
state: connected
- storage:
resource: volume
state: disconnected
It’s when we have multiple volumes that we have to provide more parameters, like we do in all the other tasks.
- storage:
resource: volume
size: 1
- storage:
resource: volume
size: 1
backend: backend2
- storage:
resource: volume
backend: backend2
state: connected
- storage:
resource: volume
backend: backend2
state: disconnected
Stats¶
This is the only task that is meant to be executed on the controller node.
Stats gathering is a provider specific task that return arbitrary data. Each provider specifies what information is returned in the providers’ section, but they must all return this data as the value for the result key.
And example for the default provider:
- storage:
resource: backend
backend: lvm
state: stats
register: stats
- debug:
msg: "Backend {{stats.result.volume_backend_name}} from vendor {{stats.result.vendor_name}} uses protocol {{stats.result.storage_protocol}}"