diff --git a/README.md b/README.md index b78503f..6e2b7a1 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,5 @@ This collection includes content for interacting with OpenStack clouds. - os_openstackclient - os_openstacksdk - os_projects +- os_ratings - os_volumes diff --git a/roles/os_ratings/README.md b/roles/os_ratings/README.md new file mode 100644 index 0000000..5307d79 --- /dev/null +++ b/roles/os_ratings/README.md @@ -0,0 +1,88 @@ +OpenStack Cloudkitty Ratings +============================ + +This role can be used to register ratings in OpenStack Cloudkitty. + +Requirements +------------ + +The OpenStack Cloudkitty API should be accessible from the target host. + +Role Variables +-------------- + +`os_ratings_venv` is a path to a directory in which to create a +virtual environment. + +`os_ratings_upper_constraints_file` is a file or URL containing Python +upper constraints. + +`os_ratings_environment` is a dict of environment variables for use with +OpenStack CLI. Default is empty. + +`os_ratings_hashmap_field_mappings` is a list of mappings associated with a +field. Each item is a dict with the following fields: +* `service` +* `name` +* `mappings` +The mappings field is a list, where each item is a dict with the following +fields: +* `value` +* `cost` +* `group` (optional) +* `type` + +`os_ratings_hashmap_service_mappings` is a list of mappings not associated with +a field. Each item is a dict with the following fields: +* `service` +* `cost` +* `group` (optional) +* `type` + +Dependencies +------------ + +This role depends on the `stackhpc.openstack.os_openstackclient` role. + +Example Playbook +---------------- + +The following playbook registers a Cloudkitty flavor field with two mappings +for different Nova flavors. It also registers a service mapping based on the +size of images stored in Glance. + +``` +--- +- name: Ensure Cloudkitty ratings are registered + hosts: os-client + tasks: + - import_role: + name: stackhpc.openstack.os_ratings + vars: + os_ratings_venv: "~/os-ratings-venv" + os_ratings_environment: + OS_AUTH_URL: "{{ lookup('env', 'OS_AUTH_URL') }}" + ... + os_ratings_hashmap_field_mappings: + - service: instance + name: flavor_id + mappings: + - value: small + cost: 1.0 + group: instance_uptime_flavor_id + type: flat + - value: large + cost: 2.0 + group: instance_uptime_flavor_id + type: flat + os_ratings_hashmap_service_mappings: + - service: image.size + cost: 0.1 + group: volume_ceph + type: flat +``` + +Author Information +------------------ + +- Mark Goddard () diff --git a/roles/os_ratings/defaults/main.yml b/roles/os_ratings/defaults/main.yml new file mode 100644 index 0000000..13f862a --- /dev/null +++ b/roles/os_ratings/defaults/main.yml @@ -0,0 +1,43 @@ +--- +# Path to a directory in which to create a virtualenv. +os_ratings_venv: +# Upper constraints file for installation of Python dependencies. +os_ratings_upper_constraints_file: https://releases.openstack.org/constraints/upper/2023.1 + +# Environment variables for use with OpenStack CLI. +os_ratings_environment: {} +# Mappings associated with a field. +# Each item is a dict with the following fields: +# * service +# * name +# * mappings +# The mappings field is a list, where each item is a dict with the following fields: +# * value +# * cost +# * group (optional) +# * type +# For example, for per-instance rating: +# - service: instance +# name: flavor_id +# mappings: +# - value: small +# cost: 1.0 +# group: instance_uptime_flavor_id +# type: flat +# - value: large +# cost: 2.0 +# group: instance_uptime_flavor_id +# type: flat +os_ratings_hashmap_field_mappings: [] +# Mappings not associated with a field. +# Each item is a dict with the following fields: +# * service +# * cost +# * group (optional) +# * type +# For example, for image image storage (MB) +# - service: image.size +# cost: 0.1 +# group: volume_ceph +# type: flat +os_ratings_hashmap_service_mappings: [] diff --git a/roles/os_ratings/meta/main.yml b/roles/os_ratings/meta/main.yml new file mode 100644 index 0000000..12caf8c --- /dev/null +++ b/roles/os_ratings/meta/main.yml @@ -0,0 +1,5 @@ +--- +dependencies: + - role: stackhpc.openstack.os_openstackclient + os_openstackclient_venv: "{{ os_ratings_venv }}" + os_openstackclient_upper_constraints_file: "{{ os_ratings_upper_constraints_file | default(None) }}" diff --git a/roles/os_ratings/tasks/field-mappings.yml b/roles/os_ratings/tasks/field-mappings.yml new file mode 100644 index 0000000..f9eeb64 --- /dev/null +++ b/roles/os_ratings/tasks/field-mappings.yml @@ -0,0 +1,40 @@ +--- +# Task file for a single field and its mappings. + +- name: Create hashmap field + ansible.builtin.command: > + {{ openstack }} rating hashmap field create {{ service_id }} {{ field.name }} + when: field.name not in fields | map(attribute='Name') | list + changed_when: true + +# List again to get ID of created mapping. +- name: List hashmap fields + ansible.builtin.command: > + {{ openstack }} rating hashmap field list -f json {{ service_id }} + register: hashmap_field + changed_when: false + +- name: List hashmap field mappings + vars: + field_id: "{{ (hashmap_field.stdout | from_json | selectattr('Name', 'equalto', field.name) | first)['Field ID'] }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap mapping list -f json --field-id {{ field_id }} + register: hashmap_mappings + changed_when: false + +- name: Create hashmap field mappings + vars: + field_id: "{{ (hashmap_field.stdout | from_json | selectattr('Name', 'equalto', field.name) | first)['Field ID'] }}" + group_id: >- + {{ (hashmap_groups.stdout | from_json | selectattr('Name', 'equalto', item.group) | first)['Group ID'] | default('') if item.group is defined else '' }} + ansible.builtin.command: > + {{ openstack }} rating hashmap mapping create + {{ item.cost }} + --field-id {{ field_id }} + --value {{ item.value }} + {% if group_id | length > 0 %}--group-id {{ group_id }}{% endif %} + --type {{ item.type }} + loop: "{{ field.mappings }}" + # Condition could be better, but should work with current values. + when: item.value not in (hashmap_mappings.stdout | from_json | map(attribute='Value') | list) + changed_when: true diff --git a/roles/os_ratings/tasks/main.yml b/roles/os_ratings/tasks/main.yml new file mode 100644 index 0000000..5166d07 --- /dev/null +++ b/roles/os_ratings/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Ensure Cloudkitty client is installed # noqa package-latest + ansible.builtin.pip: + name: + - python-cloudkittyclient + state: latest + extra_args: "{% if os_ratings_upper_constraints_file %}-c {{ os_ratings_upper_constraints_file }}{% endif %}" + virtualenv: "{{ os_ratings_venv }}" + run_once: true + +- name: Set a fact about the Ansible python interpreter + ansible.builtin.set_fact: + old_ansible_python_interpreter: "{{ ansible_python_interpreter | default('/usr/bin/python3') }}" + +- name: Import ratings.yml + ansible.builtin.import_tasks: ratings.yml + vars: + ansible_python_interpreter: "{{ os_ratings_venv ~ '/bin/python' if os_ratings_venv != None else old_ansible_python_interpreter }}" + openstack: "{{ os_ratings_venv ~ '/bin/' if os_ratings_venv else '' }}openstack" + os_ratings_hashmap_field_mapping_services: "{{ os_ratings_hashmap_field_mappings | map(attribute='service') | list }}" + os_ratings_hashmap_service_mapping_services: "{{ os_ratings_hashmap_service_mappings | map(attribute='service') | list }}" + environment: "{{ os_ratings_environment }}" diff --git a/roles/os_ratings/tasks/ratings.yml b/roles/os_ratings/tasks/ratings.yml new file mode 100644 index 0000000..4e7bc9d --- /dev/null +++ b/roles/os_ratings/tasks/ratings.yml @@ -0,0 +1,110 @@ +--- +- name: List modules + ansible.builtin.command: > + {{ openstack }} rating module list -f json + register: modules + changed_when: false + +- name: Enable hashmap module + ansible.builtin.command: > + {{ openstack }} rating module enable hashmap + when: not (modules.stdout | from_json | selectattr('Module', 'equalto', 'hashmap') | first)['Enabled'] | bool + changed_when: true + +- name: List hashmap services + ansible.builtin.command: > + {{ openstack }} rating hashmap service list -f json + register: hashmap_services + changed_when: false + +- name: Create hashmap services + vars: + existing_services: "{{ hashmap_services.stdout | from_json | map(attribute='Name') | list }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap service create {{ item }} + loop: "{{ (os_ratings_hashmap_field_mapping_services + os_ratings_hashmap_service_mapping_services) | unique | list }}" + when: item not in existing_services + changed_when: true + +- name: List hashmap groups + ansible.builtin.command: > + {{ openstack }} rating hashmap group list -f json + register: hashmap_groups + changed_when: false + +- name: Create hashmap groups + vars: + existing_groups: "{{ hashmap_groups.stdout | from_json | map(attribute='Name') | list }}" + field_mapping_groups: "{{ query('subelements', os_ratings_hashmap_field_mappings, 'mappings') | map(attribute='1.group') | select('defined') | list }}" + service_mapping_groups: "{{ os_ratings_hashmap_service_mappings | map(attribute='group') | select('defined') | list }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap group create {{ item }} + loop: "{{ (field_mapping_groups + service_mapping_groups) | unique | list }}" + when: + - item is not none and item | length > 0 + - item not in existing_groups + changed_when: true + +# List again to get IDs of created services. +- name: List hashmap services + ansible.builtin.command: > + {{ openstack }} rating hashmap service list -f json + register: hashmap_services + changed_when: false + +# List again to get IDs of created groups. +- name: List hashmap groups + ansible.builtin.command: > + {{ openstack }} rating hashmap group list -f json + register: hashmap_groups + changed_when: false + +- name: List hashmap fields + vars: + service_id: "{{ (hashmap_services.stdout | from_json | selectattr('Name', 'equalto', item) | first)['Service ID'] }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap field list {{ service_id }} -f json + loop: "{{ os_ratings_hashmap_field_mapping_services }}" + register: hashmap_fields + changed_when: false + +# Field mappings + +- name: Include field mappings + ansible.builtin.include_tasks: field-mappings.yml + vars: + fields_result: "{{ hashmap_fields.results | selectattr('item', 'equalto', field.service) | first }}" + fields: "{{ fields_result.stdout | from_json }}" + service_id: "{{ (hashmap_services.stdout | from_json | selectattr('Name', 'equalto', field.service) | first)['Service ID'] }}" + loop: "{{ os_ratings_hashmap_field_mappings }}" + loop_control: + loop_var: field + +# Service mappings + +- name: List hashmap service mappings + vars: + service_id: "{{ (hashmap_services.stdout | from_json | selectattr('Name', 'equalto', item) | first)['Service ID'] }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap mapping list -f json --service-id {{ service_id }} + loop: "{{ os_ratings_hashmap_service_mapping_services }}" + register: hashmap_mappings + changed_when: false + +- name: Create hashmap service mappings + vars: + mappings_result: "{{ hashmap_mappings.results | selectattr('item', 'equalto', item.service) | first }}" + mappings: "{{ mappings_result.stdout | from_json }}" + service_id: "{{ (hashmap_services.stdout | from_json | selectattr('Name', 'equalto', item.service) | first)['Service ID'] }}" + group_id: "{{ (hashmap_groups.stdout | from_json | selectattr('Name', 'equalto', item.group) | first)['Group ID'] | default('') if item.group is defined else + '' }}" + ansible.builtin.command: > + {{ openstack }} rating hashmap mapping create + {{ item.cost }} + --service-id {{ service_id }} + {% if group_id | length > 0 %}--group-id {{ group_id }}{% endif %} + --type {{ item.type }} + loop: "{{ os_ratings_hashmap_service_mappings }}" + # Condition could be better, but should work with current values. + when: mappings | length == 0 + changed_when: true