From 67ee60e807b1a85b92867eea69c888f132965cf0 Mon Sep 17 00:00:00 2001 From: Doug Szumski Date: Fri, 7 May 2021 18:49:31 +0100 Subject: [PATCH 1/6] Add role to deploy cASO cASO is an OpenStack accounting extractor. For more detail see: https://github.com/IFCA/caso By default, cASO is configured to output to Fluentd via TCP. The accounting information can then be shipped off to ElasticSearch. (cherry picked from commit d8ab00fa3c6ec3aba440ac50beb32c8e18e42337) --- ansible/group_vars/all.yml | 3 + ansible/inventory/all-in-one | 4 + ansible/inventory/multinode | 4 + ansible/roles/caso/defaults/main.yml | 40 +++++++++ ansible/roles/caso/handlers/main.yml | 24 +++++ ansible/roles/caso/meta/main.yml | 3 + ansible/roles/caso/tasks/check.yml | 1 + ansible/roles/caso/tasks/config.yml | 90 +++++++++++++++++++ ansible/roles/caso/tasks/deploy.yml | 12 +++ ansible/roles/caso/tasks/main.yml | 2 + ansible/roles/caso/tasks/precheck.yml | 1 + ansible/roles/caso/tasks/pull.yml | 11 +++ ansible/roles/caso/tasks/reconfigure.yml | 2 + ansible/roles/caso/tasks/register.yml | 14 +++ ansible/roles/caso/tasks/upgrade.yml | 5 ++ ansible/roles/caso/templates/caso.conf.j2 | 23 +++++ ansible/roles/caso/templates/caso.crontab.j2 | 1 + ansible/roles/caso/templates/caso.json.j2 | 41 +++++++++ ansible/roles/caso/templates/voms.json.j2 | 9 ++ ansible/roles/common/tasks/config.yml | 2 + .../conf/filter/00-record_transformer.conf.j2 | 9 ++ .../templates/conf/input/99-caso.conf.j2 | 8 ++ .../templates/conf/output/01-es.conf.j2 | 16 ++++ .../templates/cron-logrotate-caso.conf.j2 | 3 + ansible/site.yml | 10 +++ etc/kolla/passwords.yml | 5 ++ 26 files changed, 343 insertions(+) create mode 100644 ansible/roles/caso/defaults/main.yml create mode 100644 ansible/roles/caso/handlers/main.yml create mode 100644 ansible/roles/caso/meta/main.yml create mode 100644 ansible/roles/caso/tasks/check.yml create mode 100644 ansible/roles/caso/tasks/config.yml create mode 100644 ansible/roles/caso/tasks/deploy.yml create mode 100644 ansible/roles/caso/tasks/main.yml create mode 100644 ansible/roles/caso/tasks/precheck.yml create mode 100644 ansible/roles/caso/tasks/pull.yml create mode 100644 ansible/roles/caso/tasks/reconfigure.yml create mode 100644 ansible/roles/caso/tasks/register.yml create mode 100644 ansible/roles/caso/tasks/upgrade.yml create mode 100644 ansible/roles/caso/templates/caso.conf.j2 create mode 100644 ansible/roles/caso/templates/caso.crontab.j2 create mode 100644 ansible/roles/caso/templates/caso.json.j2 create mode 100644 ansible/roles/caso/templates/voms.json.j2 create mode 100644 ansible/roles/common/templates/conf/input/99-caso.conf.j2 create mode 100644 ansible/roles/common/templates/cron-logrotate-caso.conf.j2 diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index e33d2d4244..3209873e32 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -285,6 +285,8 @@ barbican_api_listen_port: "{{ barbican_api_port }}" blazar_api_port: "1234" +caso_tcp_output_port: "24224" + cinder_internal_fqdn: "{{ kolla_internal_fqdn }}" cinder_external_fqdn: "{{ kolla_external_fqdn }}" cinder_api_port: "8776" @@ -600,6 +602,7 @@ enable_haproxy_memcached: "no" enable_aodh: "no" enable_barbican: "no" enable_blazar: "no" +enable_caso: "no" enable_ceilometer: "no" enable_ceilometer_ipmi: "no" enable_cells: "no" diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one index f713d1c1ce..98f42f2c1c 100644 --- a/ansible/inventory/all-in-one +++ b/ansible/inventory/all-in-one @@ -18,6 +18,10 @@ localhost ansible_connection=local [deployment] localhost ansible_connection=local +# Caso +[caso:children] +monitoring + # You can explicitly specify which hosts run each project by updating the # groups in the sections below. Common services are grouped together. diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode index c534669d82..6f9ee8e449 100644 --- a/ansible/inventory/multinode +++ b/ansible/inventory/multinode @@ -42,6 +42,10 @@ monitoring [tls-backend:children] control +# Caso +[caso:children] +monitoring + # You can explicitly specify which hosts run each project by updating the # groups in the sections below. Common services are grouped together. diff --git a/ansible/roles/caso/defaults/main.yml b/ansible/roles/caso/defaults/main.yml new file mode 100644 index 0000000000..462c9f5b36 --- /dev/null +++ b/ansible/roles/caso/defaults/main.yml @@ -0,0 +1,40 @@ +--- +caso_services: + caso: + container_name: caso + group: caso + enabled: true + image: "{{ caso_image_full }}" + volumes: + - "{{ node_config_directory }}/caso/:{{ container_config_directory }}/" + - "/etc/localtime:/etc/localtime:ro" + - "caso_spool:/var/lib/caso" + - "caso_ssm_outgoing:/var/spool/apel/outgoing/openstack" + - "kolla_logs:/var/log/kolla/" + dimensions: "{{ caso_dimensions }}" + +#################### +# caso +#################### +caso_site_name: "kolla_caso" +caso_projects: [] +caso_logging_debug: "{{ openstack_logging_debug }}" +caso_log_dir: "/var/log/kolla/caso" +caso_cron_table: "10 * * * *" +caso_messengers: + - caso.messenger.logstash.LogstashMessenger + +#################### +# OpenStack +#################### +caso_openstack_auth: "{{ openstack_auth }}" +caso_keystone_user: "caso" + +#################### +# Docker +#################### +caso_install_type: "{{ kolla_install_type }}" +caso_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ caso_install_type }}-caso" +caso_tag: "{{ openstack_release }}" +caso_image_full: "{{ caso_image }}:{{ caso_tag }}" +caso_dimensions: "{{ default_container_dimensions }}" diff --git a/ansible/roles/caso/handlers/main.yml b/ansible/roles/caso/handlers/main.yml new file mode 100644 index 0000000000..07cd0f24d4 --- /dev/null +++ b/ansible/roles/caso/handlers/main.yml @@ -0,0 +1,24 @@ +--- +- name: Restart caso container + vars: + service_name: "caso" + service: "{{ caso_services[service_name] }}" + config_json: "{{ caso_config_jsons.results|selectattr('item.key', 'equalto', service_name)|first }}" + caso_container: "{{ check_caso_containers.results|selectattr('item.key', 'equalto', service_name)|first }}" + become: true + kolla_docker: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + volumes: "{{ service.volumes }}" + dimensions: "{{ service.dimensions }}" + when: + - kolla_action != "config" + - inventory_hostname in groups[service.group] + - service.enabled | bool + - config_json.changed | bool + or caso_conf.changed | bool + or caso_vom_conf.changed | bool + or caso_crontab.changed | bool + or caso_container.changed | bool diff --git a/ansible/roles/caso/meta/main.yml b/ansible/roles/caso/meta/main.yml new file mode 100644 index 0000000000..6b4fff8fef --- /dev/null +++ b/ansible/roles/caso/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - { role: common } diff --git a/ansible/roles/caso/tasks/check.yml b/ansible/roles/caso/tasks/check.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/caso/tasks/check.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/caso/tasks/config.yml b/ansible/roles/caso/tasks/config.yml new file mode 100644 index 0000000000..7e4d7eec3a --- /dev/null +++ b/ansible/roles/caso/tasks/config.yml @@ -0,0 +1,90 @@ +--- +- name: Ensuring config directories exist + file: + path: "{{ node_config_directory }}/{{ item.key }}" + state: "directory" + owner: "{{ config_owner_user }}" + group: "{{ config_owner_group }}" + mode: "0770" + become: true + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + +- name: Copying over config.json files for services + template: + src: "{{ item.key }}.json.j2" + dest: "{{ node_config_directory }}/{{ item.key }}/config.json" + mode: "0660" + become: true + register: caso_config_jsons + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + notify: + - Restart caso container + +- name: Copying over caso config + merge_configs: + sources: + - "{{ role_path }}/templates/caso.conf.j2" + - "{{ node_custom_config }}//caso/caso.conf" + - "{{ node_custom_config }}/{{ item.key }}/{{ inventory_hostname }}/caso.conf" + dest: "{{ node_config_directory }}/{{ item.key }}/caso.conf" + mode: "0660" + become: true + register: caso_conf + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + notify: + - Restart caso container + +- name: Copying over caso crontab + template: + src: "{{ role_path }}/templates/caso.crontab.j2" + dest: "{{ node_config_directory }}/{{ item.key }}/caso.crontab" + mode: "0660" + become: true + register: caso_crontab + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + notify: + - Restart caso container + +- name: Copying over caso voms file + template: + src: "{{ role_path }}/templates/voms.json.j2" + dest: "{{ node_config_directory }}/{{ item.key }}/voms.json" + mode: "0660" + become: true + register: caso_vom_conf + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + notify: + - Restart caso container + +- name: Check caso containers + become: true + kolla_docker: + action: "compare_container" + common_options: "{{ docker_common_options }}" + name: "{{ item.value.container_name }}" + image: "{{ item.value.image }}" + volumes: "{{ item.value.volumes }}" + dimensions: "{{ item.value.dimensions }}" + register: check_caso_containers + when: + - kolla_action != "config" + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" + notify: + - Restart caso container diff --git a/ansible/roles/caso/tasks/deploy.yml b/ansible/roles/caso/tasks/deploy.yml new file mode 100644 index 0000000000..27c275b7a4 --- /dev/null +++ b/ansible/roles/caso/tasks/deploy.yml @@ -0,0 +1,12 @@ +--- +- include_tasks: register.yml + when: inventory_hostname in groups['caso'] + +- include_tasks: config.yml + when: inventory_hostname in groups['caso'] + +- name: Flush handlers + meta: flush_handlers + +- include_tasks: check.yml + when: inventory_hostname in groups['caso'] diff --git a/ansible/roles/caso/tasks/main.yml b/ansible/roles/caso/tasks/main.yml new file mode 100644 index 0000000000..bc5d1e6257 --- /dev/null +++ b/ansible/roles/caso/tasks/main.yml @@ -0,0 +1,2 @@ +--- +- include_tasks: "{{ kolla_action }}.yml" diff --git a/ansible/roles/caso/tasks/precheck.yml b/ansible/roles/caso/tasks/precheck.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/caso/tasks/precheck.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/caso/tasks/pull.yml b/ansible/roles/caso/tasks/pull.yml new file mode 100644 index 0000000000..5b08cc879a --- /dev/null +++ b/ansible/roles/caso/tasks/pull.yml @@ -0,0 +1,11 @@ +--- +- name: Pulling caso images + become: true + kolla_docker: + action: "pull_image" + common_options: "{{ docker_common_options }}" + image: "{{ item.value.image }}" + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ caso_services }}" diff --git a/ansible/roles/caso/tasks/reconfigure.yml b/ansible/roles/caso/tasks/reconfigure.yml new file mode 100644 index 0000000000..f670a5b78d --- /dev/null +++ b/ansible/roles/caso/tasks/reconfigure.yml @@ -0,0 +1,2 @@ +--- +- include_tasks: deploy.yml diff --git a/ansible/roles/caso/tasks/register.yml b/ansible/roles/caso/tasks/register.yml new file mode 100644 index 0000000000..b0b2049be9 --- /dev/null +++ b/ansible/roles/caso/tasks/register.yml @@ -0,0 +1,14 @@ +--- +- name: Configure cASO user + kolla_toolbox: + module_name: "kolla_keystone_user" + module_args: + project: "{{ item }}" + user: "{{ caso_keystone_user }}" + password: "{{ caso_keystone_password }}" + region_name: "{{ openstack_region_name }}" + role: admin + auth: "{{ caso_openstack_auth }}" + endpoint_type: "{{ openstack_interface }}" + with_items: "{{ caso_projects }}" + run_once: True diff --git a/ansible/roles/caso/tasks/upgrade.yml b/ansible/roles/caso/tasks/upgrade.yml new file mode 100644 index 0000000000..375dcad19b --- /dev/null +++ b/ansible/roles/caso/tasks/upgrade.yml @@ -0,0 +1,5 @@ +--- +- include_tasks: config.yml + +- name: Flush handlers + meta: flush_handlers diff --git a/ansible/roles/caso/templates/caso.conf.j2 b/ansible/roles/caso/templates/caso.conf.j2 new file mode 100644 index 0000000000..81502116df --- /dev/null +++ b/ansible/roles/caso/templates/caso.conf.j2 @@ -0,0 +1,23 @@ +[DEFAULT] +messengers = {{ caso_messengers|join(', ') }} +site_name = {{ caso_site_name }} +projects = {{ caso_projects|join(', ') }} +debug = {{ caso_logging_debug }} +log_file = caso.log +log_dir = {{ caso_log_dir }} +log_rotation_type = none +spooldir = /var/lib/caso + +[keystone_auth] +auth_type = password +auth_url = {{ admin_protocol }}://{{ kolla_internal_fqdn }}:{{ keystone_admin_port }} +project_domain_id = {{ default_project_domain_id }} +username = {{ caso_keystone_user }} +user_domain_id = {{ default_user_domain_id }} +password = {{ caso_keystone_password }} + +[logstash] +port = {{ caso_tcp_output_port }} + +[ssm] +output_path = /var/spool/apel/outgoing/openstack diff --git a/ansible/roles/caso/templates/caso.crontab.j2 b/ansible/roles/caso/templates/caso.crontab.j2 new file mode 100644 index 0000000000..f406d808eb --- /dev/null +++ b/ansible/roles/caso/templates/caso.crontab.j2 @@ -0,0 +1 @@ +{{ caso_cron_table }} caso-extract --config-file /etc/caso/caso.conf diff --git a/ansible/roles/caso/templates/caso.json.j2 b/ansible/roles/caso/templates/caso.json.j2 new file mode 100644 index 0000000000..949c4ca022 --- /dev/null +++ b/ansible/roles/caso/templates/caso.json.j2 @@ -0,0 +1,41 @@ +{% set cron_cmd = 'cron -f' if kolla_base_distro in ['ubuntu', 'debian'] else 'crond -s -n' %} +{ + "command": "{{ cron_cmd }}", + "config_files": [ + { + "source": "{{ container_config_directory }}/caso.crontab", + "dest": "/var/spool/cron/caso", + "owner": "caso", + "perm": "0600" + }, + { + "source": "{{ container_config_directory }}/caso.conf", + "dest": "/etc/caso/caso.conf", + "owner": "caso", + "perm": "0600" + }, + { + "source": "{{ container_config_directory }}/voms.json", + "dest": "/etc/caso/voms.json", + "owner": "caso", + "perm": "0600" + } + ], + "permissions": [ + { + "path": "/var/log/kolla/caso", + "owner": "caso:caso", + "recurse": true + }, + { + "path": "/var/spool/apel/outgoing/openstack", + "owner": "caso:caso", + "recurse": true + }, + { + "path": "/var/lib/caso", + "owner": "caso:caso", + "recurse": true + } + ] +} diff --git a/ansible/roles/caso/templates/voms.json.j2 b/ansible/roles/caso/templates/voms.json.j2 new file mode 100644 index 0000000000..559eccb765 --- /dev/null +++ b/ansible/roles/caso/templates/voms.json.j2 @@ -0,0 +1,9 @@ +{ + "VO FQAN": { + "projects": ["local tenant 1", "local tenant 2"] + }, + "VO NAME": { + "projects": ["local tenant 3"] + } +} + diff --git a/ansible/roles/common/tasks/config.yml b/ansible/roles/common/tasks/config.yml index d8a6fde79f..73651d478d 100644 --- a/ansible/roles/common/tasks/config.yml +++ b/ansible/roles/common/tasks/config.yml @@ -116,6 +116,7 @@ # Inputs fluentd_input_files: "{{ default_input_files | customise_fluentd(customised_input_files) }}" default_input_files: + - "conf/input/99-caso.conf.j2" - "conf/input/00-global.conf.j2" - "conf/input/01-syslog.conf.j2" - "conf/input/02-mariadb.conf.j2" @@ -173,6 +174,7 @@ - { name: "barbican", enabled: "{{ enable_barbican | bool }}" } - { name: "blazar", enabled: "{{ enable_blazar | bool }}" } - { name: "ceilometer", enabled: "{{ enable_ceilometer | bool }}" } + - { name: "caso", enabled: "{{ enable_caso | bool }}" } - { name: "chrony", enabled: "{{ enable_chrony | bool }}" } - { name: "cinder", enabled: "{{ enable_cinder | bool }}" } - { name: "cloudkitty", enabled: "{{ enable_cloudkitty | bool }}" } diff --git a/ansible/roles/common/templates/conf/filter/00-record_transformer.conf.j2 b/ansible/roles/common/templates/conf/filter/00-record_transformer.conf.j2 index 2d5fef5bbd..874c274d14 100644 --- a/ansible/roles/common/templates/conf/filter/00-record_transformer.conf.j2 +++ b/ansible/roles/common/templates/conf/filter/00-record_transformer.conf.j2 @@ -92,3 +92,12 @@ {% endif %} + +{% if enable_caso | bool and inventory_hostname in groups['caso'] %} + + @type parser + format json + key_name Payload + reserve_data true + +{% endif %} diff --git a/ansible/roles/common/templates/conf/input/99-caso.conf.j2 b/ansible/roles/common/templates/conf/input/99-caso.conf.j2 new file mode 100644 index 0000000000..5c577de410 --- /dev/null +++ b/ansible/roles/common/templates/conf/input/99-caso.conf.j2 @@ -0,0 +1,8 @@ + + @type tcp + tag apel.events + port {{ caso_tcp_output_port }} + bind 127.0.0.1 + format /^(?.*)$/ + emit_unmatched_lines true + diff --git a/ansible/roles/common/templates/conf/output/01-es.conf.j2 b/ansible/roles/common/templates/conf/output/01-es.conf.j2 index 956d30156b..f35803efda 100644 --- a/ansible/roles/common/templates/conf/output/01-es.conf.j2 +++ b/ansible/roles/common/templates/conf/output/01-es.conf.j2 @@ -1,3 +1,19 @@ +{% if enable_caso | bool and inventory_hostname in groups['caso'] %} + + @type copy + + @type elasticsearch + host {% raw %}{{ elasticsearch_address }} +{% endraw %} + port {% raw %}{{ elasticsearch_port }} +{% endraw %} + logstash_format true + logstash_prefix apel + flush_interval 15s + + +{% endif %} + @type copy diff --git a/ansible/roles/common/templates/cron-logrotate-caso.conf.j2 b/ansible/roles/common/templates/cron-logrotate-caso.conf.j2 new file mode 100644 index 0000000000..2d4642e4b5 --- /dev/null +++ b/ansible/roles/common/templates/cron-logrotate-caso.conf.j2 @@ -0,0 +1,3 @@ +"/var/log/kolla/caso/*.log" +{ +} diff --git a/ansible/site.yml b/ansible/site.yml index fbeb40db5e..350282f09b 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -1224,3 +1224,13 @@ - { role: masakari, tags: masakari, when: enable_masakari | bool } + +- name: Apply role caso + gather_facts: false + hosts: + - caso + serial: '{{ kolla_serial|default("0") }}' + roles: + - { role: caso, + tags: caso, + when: enable_caso | bool } diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml index d1319f2ae9..68b4ed9336 100644 --- a/etc/kolla/passwords.yml +++ b/etc/kolla/passwords.yml @@ -9,6 +9,11 @@ rbd_secret_uuid: cinder_rbd_secret_uuid: +############ +# cASO +############ +caso_keystone_password: + ################### # Database options #################### From d896b8c0e40b8851c8a5e53de8d546576518c1ac Mon Sep 17 00:00:00 2001 From: Doug Szumski Date: Mon, 10 May 2021 12:49:09 +0100 Subject: [PATCH 2/6] Add support for deploying Prometheus libvirt exporter Add support for deploying the Kolla Prometheus libvirt exporter image to facilitate gathering metrics from the Nova libvirt service. Partially-Implements: blueprint libvirt-exporter (cherry picked from commit edcd41ace5ffc31f760d1722529d506613960bf7) (cherry picked from commit ab84608b21eb634e443abd91be41b1a8f4558f46) Change-Id: Ife0a9ac6e4f03802543d0812d552c30ddb982b9e (cherry picked from commit 1422e0e0436f2e3c5156e0e740ff87172f09f524) --- ansible/group_vars/all.yml | 3 +++ ansible/inventory/all-in-one | 3 +++ ansible/inventory/multinode | 3 +++ ansible/roles/nova-cell/defaults/main.yml | 1 + ansible/roles/prometheus/defaults/main.yml | 17 +++++++++++++++++ ansible/roles/prometheus/handlers/main.yml | 15 +++++++++++++++ ansible/roles/prometheus/tasks/precheck.yml | 15 +++++++++++++++ .../prometheus-libvirt-exporter.json.j2 | 4 ++++ .../prometheus/templates/prometheus.yml.j2 | 11 +++++++++++ etc/kolla/globals.yml | 1 + ...theus-libvirt-exporter-b05a3a9c08db517c.yaml | 12 ++++++++++++ 11 files changed, 85 insertions(+) create mode 100644 ansible/roles/prometheus/templates/prometheus-libvirt-exporter.json.j2 create mode 100644 releasenotes/notes/add-prometheus-libvirt-exporter-b05a3a9c08db517c.yaml diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 3209873e32..ae22b03e72 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -449,6 +449,7 @@ placement_api_port: "8780" placement_api_listen_port: "{{ placement_api_port }}" prometheus_port: "9091" +prometheus_libvirt_exporter_port: "9177" prometheus_node_exporter_port: "9100" prometheus_mysqld_exporter_port: "9104" prometheus_haproxy_exporter_port: "9101" @@ -1115,6 +1116,7 @@ use_common_mariadb_user: "no" # Prometheus ############ enable_prometheus_server: "{{ enable_prometheus | bool }}" +enable_prometheus_libvirt_exporter: "no" enable_prometheus_haproxy_exporter: "{{ enable_haproxy | bool }}" enable_prometheus_mysqld_exporter: "{{ enable_mariadb | bool }}" enable_prometheus_node_exporter: "{{ enable_prometheus | bool }}" @@ -1128,6 +1130,7 @@ enable_prometheus_blackbox_exporter: "{{ enable_prometheus | bool }}" enable_prometheus_rabbitmq_exporter: "{{ enable_prometheus | bool and enable_rabbitmq | bool }}" prometheus_alertmanager_user: "admin" +prometheus_libvirt_exporter_interval: "60s" prometheus_openstack_exporter_interval: "60s" prometheus_openstack_exporter_timeout: "10s" prometheus_elasticsearch_exporter_interval: "60s" diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one index 98f42f2c1c..0bda5b52bb 100644 --- a/ansible/inventory/all-in-one +++ b/ansible/inventory/all-in-one @@ -732,6 +732,9 @@ compute network storage +[prometheus-libvirt-exporter:children] +compute + [prometheus-mysqld-exporter:children] mariadb diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode index 6f9ee8e449..424a3e1f8c 100644 --- a/ansible/inventory/multinode +++ b/ansible/inventory/multinode @@ -750,6 +750,9 @@ compute network storage +[prometheus-libvirt-exporter:children] +compute + [prometheus-mysqld-exporter:children] mariadb diff --git a/ansible/roles/nova-cell/defaults/main.yml b/ansible/roles/nova-cell/defaults/main.yml index 80eba05ed7..64d1b308c3 100644 --- a/ansible/roles/nova-cell/defaults/main.yml +++ b/ansible/roles/nova-cell/defaults/main.yml @@ -328,6 +328,7 @@ nova_libvirt_default_volumes: - "/sys/fs/cgroup:/sys/fs/cgroup" - "kolla_logs:/var/log/kolla/" - "libvirtd:/var/lib/libvirt" + - "{% if enable_prometheus_libvirt_exporter | bool %}libvirtd_run:/var/run/libvirt{% endif %}" - "{{ nova_instance_datadir_volume }}:/var/lib/nova/" - "{% if enable_shared_var_lib_nova_mnt | bool %}/var/lib/nova/mnt:/var/lib/nova/mnt:shared{% endif %}" - "nova_libvirt_qemu:/etc/libvirt/qemu" diff --git a/ansible/roles/prometheus/defaults/main.yml b/ansible/roles/prometheus/defaults/main.yml index 568a774a4d..2760a9b831 100644 --- a/ansible/roles/prometheus/defaults/main.yml +++ b/ansible/roles/prometheus/defaults/main.yml @@ -21,6 +21,13 @@ prometheus_services: pid_mode: "host" volumes: "{{ prometheus_node_exporter_default_volumes + prometheus_node_exporter_extra_volumes }}" dimensions: "{{ prometheus_node_exporter_dimensions }}" + prometheus-libvirt-exporter: + container_name: "prometheus_libvirt_exporter" + group: "prometheus-libvirt-exporter" + enabled: "{{ enable_prometheus_libvirt_exporter | bool }}" + image: "{{ prometheus_libvirt_exporter_image_full }}" + volumes: "{{ prometheus_libvirt_exporter_default_volumes + prometheus_libvirt_exporter_extra_volumes }}" + dimensions: "{{ prometheus_libvirt_exporter_dimensions }}" prometheus-mysqld-exporter: container_name: prometheus_mysqld_exporter group: prometheus-mysqld-exporter @@ -121,6 +128,10 @@ prometheus_server_image: "{{ docker_registry ~ '/' if docker_registry else '' }} prometheus_server_tag: "{{ prometheus_tag }}" prometheus_server_image_full: "{{ prometheus_server_image }}:{{ prometheus_server_tag }}" +prometheus_libvirt_exporter_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ prometheus_install_type }}-prometheus-libvirt-exporter" +prometheus_libvirt_exporter_tag: "{{ prometheus_tag }}" +prometheus_libvirt_exporter_image_full: "{{ prometheus_libvirt_exporter_image }}:{{ prometheus_libvirt_exporter_tag }}" + prometheus_haproxy_exporter_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ prometheus_install_type }}-prometheus-haproxy-exporter" prometheus_haproxy_exporter_tag: "{{ prometheus_tag }}" prometheus_haproxy_exporter_image_full: "{{ prometheus_haproxy_exporter_image }}:{{ prometheus_haproxy_exporter_tag }}" @@ -159,6 +170,7 @@ prometheus_blackbox_exporter_tag: "{{ prometheus_tag }}" prometheus_blackbox_exporter_image_full: "{{ prometheus_blackbox_exporter_image }}:{{ prometheus_blackbox_exporter_tag }}" prometheus_server_dimensions: "{{ default_container_dimensions }}" +prometheus_libvirt_exporter_dimensions: "{{ default_container_dimensions }}" prometheus_haproxy_exporter_dimensions: "{{ default_container_dimensions }}" prometheus_mysqld_exporter_dimensions: "{{ default_container_dimensions }}" prometheus_node_exporter_dimensions: "{{ default_container_dimensions }}" @@ -175,6 +187,10 @@ prometheus_server_default_volumes: - "{{ '/etc/timezone:/etc/timezone:ro' if ansible_facts.os_family == 'Debian' else '' }}" - "prometheus:/var/lib/prometheus" - "kolla_logs:/var/log/kolla/" +prometheus_libvirt_exporter_default_volumes: + - "{{ node_config_directory }}/prometheus-libvirt-exporter/:{{ container_config_directory }}/:ro" + - "/etc/localtime:/etc/localtime:ro" + - "libvirtd_run:/var/run/libvirt:ro" prometheus_haproxy_exporter_default_volumes: - "{{ node_config_directory }}/prometheus-haproxy-exporter/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" @@ -231,6 +247,7 @@ prometheus_blackbox_exporter_default_volumes: - "kolla_logs:/var/log/kolla/" prometheus_extra_volumes: "{{ default_extra_volumes }}" +prometheus_libvirt_exporter_extra_volumes: "{{ prometheus_extra_volumes }}" prometheus_server_extra_volumes: "{{ prometheus_extra_volumes }}" prometheus_haproxy_exporter_extra_volumes: "{{ prometheus_extra_volumes }}" prometheus_mysqld_exporter_extra_volumes: "{{ prometheus_extra_volumes }}" diff --git a/ansible/roles/prometheus/handlers/main.yml b/ansible/roles/prometheus/handlers/main.yml index 42820e6365..d155cf825c 100644 --- a/ansible/roles/prometheus/handlers/main.yml +++ b/ansible/roles/prometheus/handlers/main.yml @@ -149,3 +149,18 @@ dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" + +- name: Restart prometheus-libvirt-exporter container + vars: + service_name: "prometheus-libvirt-exporter" + service: "{{ prometheus_services[service_name] }}" + become: true + kolla_docker: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + volumes: "{{ service.volumes }}" + dimensions: "{{ service.dimensions }}" + when: + - kolla_action != "config" diff --git a/ansible/roles/prometheus/tasks/precheck.yml b/ansible/roles/prometheus/tasks/precheck.yml index c3aa2eadde..ec6fb3b6e9 100644 --- a/ansible/roles/prometheus/tasks/precheck.yml +++ b/ansible/roles/prometheus/tasks/precheck.yml @@ -19,6 +19,7 @@ - prometheus_openstack_exporter - prometheus_elasticsearch_exporter - prometheus_blackbox_exporter + - prometheus_libvirt_exporter register: container_facts - name: Checking free port for Prometheus server @@ -149,3 +150,17 @@ - enable_prometheus_blackbox_exporter | bool with_items: - "{{ prometheus_blackbox_exporter_port }}" + +- name: Checking free ports for Prometheus libvirt-exporter + wait_for: + host: "{{ hostvars[inventory_hostname]['ansible_' + api_interface]['ipv4']['address'] }}" + port: "{{ item }}" + connect_timeout: 1 + timeout: 1 + state: stopped + when: + - container_facts['prometheus_libvirt_exporter'] is not defined + - inventory_hostname in groups['prometheus-libvirt-exporter'] + - enable_prometheus_libvirt_exporter | bool + with_items: + - "{{ prometheus_libvirt_exporter_port }}" diff --git a/ansible/roles/prometheus/templates/prometheus-libvirt-exporter.json.j2 b/ansible/roles/prometheus/templates/prometheus-libvirt-exporter.json.j2 new file mode 100644 index 0000000000..6154beef5f --- /dev/null +++ b/ansible/roles/prometheus/templates/prometheus-libvirt-exporter.json.j2 @@ -0,0 +1,4 @@ +{ + "command": "/opt/libvirt_exporter --web.listen-address={{ api_interface_address }}:{{ prometheus_libvirt_exporter_port }}", + "config_files": [] +} diff --git a/ansible/roles/prometheus/templates/prometheus.yml.j2 b/ansible/roles/prometheus/templates/prometheus.yml.j2 index bc494b3ff6..5f41f7a4f5 100644 --- a/ansible/roles/prometheus/templates/prometheus.yml.j2 +++ b/ansible/roles/prometheus/templates/prometheus.yml.j2 @@ -174,6 +174,17 @@ scrape_configs: replacement: '{{ api_interface_address | put_address_in_context('url') }}:{{ prometheus_blackbox_exporter_port }}' {% endif %} + +{% if enable_prometheus_libvirt_exporter | bool %} + - job_name: libvirt_exporter + scrape_interval: {{ prometheus_libvirt_exporter_interval }} + static_configs: + - targets: +{% for host in groups["prometheus-libvirt-exporter"] %} + - '{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}:{{ hostvars[host]['prometheus_libvirt_exporter_port'] }}' +{% endfor %} +{% endif %} + {% if enable_prometheus_alertmanager | bool %} - job_name: alertmanager static_configs: diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml index 70780ad00f..5d06cc3c59 100644 --- a/etc/kolla/globals.yml +++ b/etc/kolla/globals.yml @@ -674,6 +674,7 @@ # Prometheus ############ #enable_prometheus_server: "{{ enable_prometheus | bool }}" +#enable_prometheus_libvirt_exporter: "no" #enable_prometheus_haproxy_exporter: "{{ enable_haproxy | bool }}" #enable_prometheus_mysqld_exporter: "{{ enable_mariadb | bool }}" #enable_prometheus_node_exporter: "{{ enable_prometheus | bool }}" diff --git a/releasenotes/notes/add-prometheus-libvirt-exporter-b05a3a9c08db517c.yaml b/releasenotes/notes/add-prometheus-libvirt-exporter-b05a3a9c08db517c.yaml new file mode 100644 index 0000000000..39e0174b7f --- /dev/null +++ b/releasenotes/notes/add-prometheus-libvirt-exporter-b05a3a9c08db517c.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Deploys and configures the prometheus-libvirt-exporter image as + (https://github.com/kumina/libvirt_exporter) as part of the + Prometheus monitoring stack. +upgrade: + - | + The libvirt sockets in the Nova libvirt container have been moved to a + docker volume to support monitoring of libvirt. As part of this upgrade + the Nova libvirt containers will be restarted. This will affect users + of instances running in those containers. From ec53eb0ebef4e30d41e6edea1db13f5d9359b3e0 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Mon, 14 Dec 2020 18:29:21 +0000 Subject: [PATCH 3/6] Switch prometheus to active/passive mode This uses the same approach as the mariadb role (and others). Closes-Bug: : 1928193 Co-Authored-By: John Garbutt Change-Id: I79a7a8c80327cfd9ef31d17fe71f450a181a638c (cherry picked from commit 502152290ac5fc2a09176c52a459b9741273b752) (cherry picked from commit 9ec49a3c302a431de55ef1ea167070a406deeac8) --- ansible/roles/prometheus/defaults/main.yml | 6 ++++++ .../prometheus-active-passive-be4fb033493e958f.yaml | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 releasenotes/notes/prometheus-active-passive-be4fb033493e958f.yaml diff --git a/ansible/roles/prometheus/defaults/main.yml b/ansible/roles/prometheus/defaults/main.yml index 2760a9b831..d1845c9596 100644 --- a/ansible/roles/prometheus/defaults/main.yml +++ b/ansible/roles/prometheus/defaults/main.yml @@ -13,6 +13,7 @@ prometheus_services: mode: "http" external: false port: "{{ prometheus_port }}" + custom_member_list: "{{ prometheus_haproxy_members.split(';') }}" prometheus-node-exporter: container_name: prometheus_node_exporter group: prometheus-node-exporter @@ -105,6 +106,11 @@ prometheus_services: #################### prometheus_mysql_exporter_database_user: "{% if use_preconfigured_databases | bool and use_common_mariadb_user | bool %}{{ database_user }}{% else %}prometheus{% endif %}" +#################### +# HAProxy +#################### +prometheus_haproxy_members: "{% for host in groups['prometheus'] %}server {{ hostvars[host]['ansible_hostname'] }} {{ 'api' | kolla_address(host) }}:{{ prometheus_port }} check inter 2000 rise 2 fall 5{% if not loop.first %} backup{% endif %};{% endfor %}" + #################### # Blackbox #################### diff --git a/releasenotes/notes/prometheus-active-passive-be4fb033493e958f.yaml b/releasenotes/notes/prometheus-active-passive-be4fb033493e958f.yaml new file mode 100644 index 0000000000..9af437157f --- /dev/null +++ b/releasenotes/notes/prometheus-active-passive-be4fb033493e958f.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + Prometheus has been switched to active/passive mode. See bug `1928193 + `__. +fixes: + - | + Fixes an issue with misaligned data points in grafana when loadbalancing + over multiple prometheus server instances. See bug `1928193 + `__. From 69176aa666615ab6ade014f4f22fe350dbcabc18 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Tue, 1 Feb 2022 10:14:10 +0000 Subject: [PATCH 4/6] Add support for Ceph RadosGW integration * Register Swift-compatible endpoints in Keystone * Load balance across RadosGW API servers using HAProxy The support is exercised in the cephadm CI jobs, but since RGW is not currently enabled via cephadm, it is not yet tested. https://docs.ceph.com/en/latest/radosgw/keystone/ Implements: blueprint ceph-rgw Change-Id: I891c3ed4ed93512607afe65a42dd99596fd4dbf9 (cherry picked from commit 5b75fefb7d9f50c197f3cd86e18059cb31ca9984) (cherry picked from commit 31f18ded176180e3fff63573299740fd9295b89a) --- ansible/group_vars/all.yml | 6 ++ ansible/roles/ceph-rgw/defaults/main.yml | 78 +++++++++++++++++++ ansible/roles/ceph-rgw/tasks/check.yml | 1 + ansible/roles/ceph-rgw/tasks/config.yml | 1 + .../ceph-rgw/tasks/deploy-containers.yml | 1 + ansible/roles/ceph-rgw/tasks/deploy.yml | 2 + ansible/roles/ceph-rgw/tasks/loadbalancer.yml | 7 ++ ansible/roles/ceph-rgw/tasks/main.yml | 2 + ansible/roles/ceph-rgw/tasks/precheck.yml | 10 +++ ansible/roles/ceph-rgw/tasks/pull.yml | 1 + ansible/roles/ceph-rgw/tasks/reconfigure.yml | 2 + ansible/roles/ceph-rgw/tasks/register.yml | 9 +++ ansible/roles/ceph-rgw/tasks/stop.yml | 1 + ansible/roles/ceph-rgw/tasks/upgrade.yml | 1 + ansible/roles/haproxy/tasks/precheck.yml | 14 ++++ ansible/site.yml | 21 ++++- .../reference/storage/external-ceph-guide.rst | 65 ++++++++++++++++ etc/kolla/globals.yml | 1 + etc/kolla/passwords.yml | 5 ++ .../notes/ceph-rgw-062e0544a004f7b1.yaml | 4 + tests/templates/globals-default.j2 | 5 ++ 21 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 ansible/roles/ceph-rgw/defaults/main.yml create mode 100644 ansible/roles/ceph-rgw/tasks/check.yml create mode 100644 ansible/roles/ceph-rgw/tasks/config.yml create mode 100644 ansible/roles/ceph-rgw/tasks/deploy-containers.yml create mode 100644 ansible/roles/ceph-rgw/tasks/deploy.yml create mode 100644 ansible/roles/ceph-rgw/tasks/loadbalancer.yml create mode 100644 ansible/roles/ceph-rgw/tasks/main.yml create mode 100644 ansible/roles/ceph-rgw/tasks/precheck.yml create mode 100644 ansible/roles/ceph-rgw/tasks/pull.yml create mode 100644 ansible/roles/ceph-rgw/tasks/reconfigure.yml create mode 100644 ansible/roles/ceph-rgw/tasks/register.yml create mode 100644 ansible/roles/ceph-rgw/tasks/stop.yml create mode 100644 ansible/roles/ceph-rgw/tasks/upgrade.yml create mode 100644 releasenotes/notes/ceph-rgw-062e0544a004f7b1.yaml diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index ae22b03e72..d7ca83b968 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -287,6 +287,10 @@ blazar_api_port: "1234" caso_tcp_output_port: "24224" +ceph_rgw_internal_fqdn: "{{ kolla_internal_fqdn }}" +ceph_rgw_external_fqdn: "{{ kolla_external_fqdn }}" +ceph_rgw_port: "6780" + cinder_internal_fqdn: "{{ kolla_internal_fqdn }}" cinder_external_fqdn: "{{ kolla_external_fqdn }}" cinder_api_port: "8776" @@ -608,6 +612,8 @@ enable_ceilometer: "no" enable_ceilometer_ipmi: "no" enable_cells: "no" enable_central_logging: "no" +enable_ceph_rgw: "no" +enable_ceph_rgw_loadbalancer: "{{ enable_ceph_rgw | bool }}" enable_chrony: "yes" enable_cinder: "no" enable_cinder_backup: "yes" diff --git a/ansible/roles/ceph-rgw/defaults/main.yml b/ansible/roles/ceph-rgw/defaults/main.yml new file mode 100644 index 0000000000..f997dd6ae4 --- /dev/null +++ b/ansible/roles/ceph-rgw/defaults/main.yml @@ -0,0 +1,78 @@ +--- +project_name: "ceph-rgw" + +ceph_rgw_services: + # NOTE(mgoddard): There is no container deployment, this is used for load + # balancer configuration. + ceph-rgw: + group: "all" + enabled: "{{ enable_ceph_rgw | bool }}" + haproxy: + radosgw: + enabled: "{{ enable_ceph_rgw_loadbalancer | bool }}" + mode: "http" + external: false + port: "{{ ceph_rgw_port }}" + custom_member_list: "{{ ceph_rgw_haproxy_members }}" + radosgw_external: + enabled: "{{ enable_ceph_rgw_loadbalancer | bool }}" + mode: "http" + external: true + port: "{{ ceph_rgw_port }}" + custom_member_list: "{{ ceph_rgw_haproxy_members }}" + +#################### +# Load balancer +#################### + +# List of Ceph RadosGW hostname:port to use as HAProxy backends. +ceph_rgw_hosts: [] +ceph_rgw_haproxy_members: "{{ ceph_rgw_hosts | map('regex_replace', '(.*)', 'server \\1 \\1 ' + ceph_rgw_haproxy_healthcheck) | list }}" +ceph_rgw_haproxy_healthcheck: "check inter 2000 rise 2 fall 5" + + +#################### +# OpenStack +#################### + +# Whether to register Ceph RadosGW swift-compatible endpoints in Keystone. +enable_ceph_rgw_keystone: "{{ enable_ceph_rgw | bool }}" + +# Enable/disable ceph-rgw compatibility with OpenStack Swift. +ceph_rgw_compatibility: false + +# Enable/disable including the account (project) in the endpoint URL. This +# allows for cross-project and public object access. +ceph_rgw_account_in_url: false + +ceph_rgw_endpoint_path: "{{ '/' if ceph_rgw_compatibility | bool else '/swift/' }}v1{% if ceph_rgw_account_in_url | bool %}/AUTH_%(project_id)s{% endif %}" + +ceph_rgw_admin_endpoint: "{{ admin_protocol }}://{{ ceph_rgw_internal_fqdn | put_address_in_context('url') }}:{{ ceph_rgw_port }}{{ ceph_rgw_endpoint_path }}" +ceph_rgw_internal_endpoint: "{{ internal_protocol }}://{{ ceph_rgw_internal_fqdn | put_address_in_context('url') }}:{{ ceph_rgw_port }}{{ ceph_rgw_endpoint_path }}" +ceph_rgw_public_endpoint: "{{ public_protocol }}://{{ ceph_rgw_external_fqdn | put_address_in_context('url') }}:{{ ceph_rgw_port }}{{ ceph_rgw_endpoint_path }}" + +ceph_rgw_keystone_user: "ceph_rgw" + +openstack_ceph_rgw_auth: "{{ openstack_auth }}" + + +#################### +# Keystone +#################### +ceph_rgw_ks_services: + - name: "swift" + type: "object-store" + description: "Openstack Object Storage" + endpoints: + - {'interface': 'admin', 'url': '{{ ceph_rgw_admin_endpoint }}'} + - {'interface': 'internal', 'url': '{{ ceph_rgw_internal_endpoint }}'} + - {'interface': 'public', 'url': '{{ ceph_rgw_public_endpoint }}'} + +ceph_rgw_ks_users: + - project: "service" + user: "{{ ceph_rgw_keystone_user }}" + password: "{{ ceph_rgw_keystone_password }}" + role: "admin" + +ceph_rgw_ks_roles: + - "ResellerAdmin" diff --git a/ansible/roles/ceph-rgw/tasks/check.yml b/ansible/roles/ceph-rgw/tasks/check.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/check.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/ceph-rgw/tasks/config.yml b/ansible/roles/ceph-rgw/tasks/config.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/config.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/ceph-rgw/tasks/deploy-containers.yml b/ansible/roles/ceph-rgw/tasks/deploy-containers.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/deploy-containers.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/ceph-rgw/tasks/deploy.yml b/ansible/roles/ceph-rgw/tasks/deploy.yml new file mode 100644 index 0000000000..40daddd63b --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/deploy.yml @@ -0,0 +1,2 @@ +--- +- import_tasks: register.yml diff --git a/ansible/roles/ceph-rgw/tasks/loadbalancer.yml b/ansible/roles/ceph-rgw/tasks/loadbalancer.yml new file mode 100644 index 0000000000..d29f3e56d1 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/loadbalancer.yml @@ -0,0 +1,7 @@ +--- +- name: "Configure haproxy for {{ project_name }}" + import_role: + role: haproxy-config + vars: + project_services: "{{ ceph_rgw_services }}" + tags: always diff --git a/ansible/roles/ceph-rgw/tasks/main.yml b/ansible/roles/ceph-rgw/tasks/main.yml new file mode 100644 index 0000000000..bc5d1e6257 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/main.yml @@ -0,0 +1,2 @@ +--- +- include_tasks: "{{ kolla_action }}.yml" diff --git a/ansible/roles/ceph-rgw/tasks/precheck.yml b/ansible/roles/ceph-rgw/tasks/precheck.yml new file mode 100644 index 0000000000..5430f4837a --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/precheck.yml @@ -0,0 +1,10 @@ +--- +- name: Fail if load balancer members not set + fail: + msg: >- + Ceph RadosGW load balancer configuration is enabled + (enable_ceph_rgw_loadbalancer) but no HAProxy members are configured. + Have you set ceph_rgw_hosts? + when: + - enable_ceph_rgw_loadbalancer | bool + - ceph_rgw_haproxy_members | length == 0 diff --git a/ansible/roles/ceph-rgw/tasks/pull.yml b/ansible/roles/ceph-rgw/tasks/pull.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/pull.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/ceph-rgw/tasks/reconfigure.yml b/ansible/roles/ceph-rgw/tasks/reconfigure.yml new file mode 100644 index 0000000000..5b10a7e111 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/reconfigure.yml @@ -0,0 +1,2 @@ +--- +- import_tasks: deploy.yml diff --git a/ansible/roles/ceph-rgw/tasks/register.yml b/ansible/roles/ceph-rgw/tasks/register.yml new file mode 100644 index 0000000000..c33683163c --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/register.yml @@ -0,0 +1,9 @@ +--- +- import_role: + name: service-ks-register + vars: + service_ks_register_auth: "{{ openstack_ceph_rgw_auth }}" + service_ks_register_services: "{{ ceph_rgw_ks_services }}" + service_ks_register_users: "{{ ceph_rgw_ks_users }}" + service_ks_register_roles: "{{ ceph_rgw_ks_roles }}" + when: enable_ceph_rgw_keystone | bool diff --git a/ansible/roles/ceph-rgw/tasks/stop.yml b/ansible/roles/ceph-rgw/tasks/stop.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/stop.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/ceph-rgw/tasks/upgrade.yml b/ansible/roles/ceph-rgw/tasks/upgrade.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/ceph-rgw/tasks/upgrade.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/haproxy/tasks/precheck.yml b/ansible/roles/haproxy/tasks/precheck.yml index 86ca0b79b6..94e0b20edc 100644 --- a/ansible/roles/haproxy/tasks/precheck.yml +++ b/ansible/roles/haproxy/tasks/precheck.yml @@ -202,6 +202,20 @@ - haproxy_stat.find('blazar_api') == -1 - haproxy_vip_prechecks +- name: Checking free port for Ceph RadosGW HAProxy + wait_for: + host: "{{ kolla_internal_vip_address }}" + port: "{{ ceph_rgw_port }}" + connect_timeout: 1 + timeout: 1 + state: stopped + when: + - enable_ceph_rgw | bool + - enable_ceph_rgw_loadbalancer | bool + - inventory_hostname in groups['haproxy'] + - haproxy_stat.find('radosgw') == -1 + - haproxy_vip_prechecks + - name: Checking free port for Cinder API HAProxy wait_for: host: "{{ kolla_internal_vip_address }}" diff --git a/ansible/site.yml b/ansible/site.yml index 350282f09b..f71f8acf4f 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -27,6 +27,7 @@ - enable_barbican_{{ enable_barbican | bool }} - enable_blazar_{{ enable_blazar | bool }} - enable_ceilometer_{{ enable_ceilometer | bool }} + - enable_ceph_rgw_{{ enable_ceph_rgw | bool }} - enable_chrony_{{ enable_chrony | bool }} - enable_cinder_{{ enable_cinder | bool }} - enable_cloudkitty_{{ enable_cloudkitty | bool }} @@ -162,7 +163,12 @@ tags: blazar when: enable_blazar | bool - include_role: - name: cinder + role: ceph-rgw + tasks_from: loadbalancer + tags: ceph-rgw + when: enable_ceph_rgw | bool + - include_role: + role: cinder tasks_from: loadbalancer tags: cinder when: enable_cinder | bool @@ -656,6 +662,19 @@ tags: swift, when: enable_swift | bool } +- name: Apply role ceph-rgw + gather_facts: false + hosts: + # NOTE(mgoddard): This is only used to register Keystone services, and + # could run on any host running kolla-toolbox. + - kolla-toolbox + - '&enable_ceph_rgw_True' + serial: '{{ kolla_serial|default("0") }}' + roles: + - { role: ceph-rgw, + tags: ceph-rgw, + when: enable_ceph_rgw | bool } + - name: Apply role glance gather_facts: false hosts: diff --git a/doc/source/reference/storage/external-ceph-guide.rst b/doc/source/reference/storage/external-ceph-guide.rst index d119716c02..4a787459ea 100644 --- a/doc/source/reference/storage/external-ceph-guide.rst +++ b/doc/source/reference/storage/external-ceph-guide.rst @@ -212,3 +212,68 @@ type ``default_share_type``, please see :doc:`Manila in Kolla `. For more details on the CephFS Native driver, please see :manila-doc:`CephFS Native driver `. + +RadosGW +------- + +As of the Wallaby 12.0.0 release, Kolla Ansible supports integration with Ceph +RadosGW. This includes: + +* Registration of Swift-compatible endpoints in Keystone +* Load balancing across RadosGW API servers using HAProxy + +See the `Ceph documentation +`__ for further information, +including changes that must be applied to the Ceph cluster configuration. + +Enable Ceph RadosGW integration: + +.. code-block:: yaml + + enable_ceph_rgw: true + +Keystone integration +==================== + +A Keystone user and endpoints are registered by default, however this may be +avoided by setting ``enable_ceph_rgw_keystone`` to ``false``. If registration +is enabled, the username is defined via ``ceph_rgw_keystone_user``, and this +defaults to ``ceph_rgw``. The hostnames used by the endpoints default to +``ceph_rgw_external_fqdn`` and ``ceph_rgw_internal_fqdn`` for the public and +internal endpoints respectively. These default to ``kolla_external_fqdn`` and +``kolla_internal_fqdn`` respectively. The port used by the endpoints is defined +via ``ceph_rgw_port``, and defaults to 6780. + +By default RadosGW supports both Swift and S3 API, and it is not completely +compatible with Swift API. The option ``ceph_rgw_compatibility`` can +enable/disable complete RadosGW compatibility with Swift API. After changing +the value, run the ``kolla-ansible deploy`` command to enable. + +By default, the RadosGW endpoint URL does not include the project (account) ID. +This prevents cross-project and public object access. This can be resolved by +setting ``ceph_rgw_account_in_url`` to ``true``. + +Load balancing +============== + +.. note:: + + Users of Ceph RadosGW can generate very high volumes of traffic. It is + advisable to use a separate load balancer for RadosGW for anything other + than small or lightly utilised RadosGW deployments. + +Load balancing is enabled by default, however this may be avoided by setting +``enable_ceph_rgw_loadbalancer`` to ``false``. If using load balancing, the +RadosGW hosts and ports must be configured. For example: + +.. code-block:: yaml + + ceph_rgw_hosts: + - rgw-host-1:6780 + - rgw-host-1:6780 + +If using hostnames, these should be resolvable from the host running HAProxy. +Alternatively IP addresses may be used. + +The HAProxy frontend port is defined via ``ceph_rgw_port``, and defaults to +6780. diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml index 5d06cc3c59..091ebd01df 100644 --- a/etc/kolla/globals.yml +++ b/etc/kolla/globals.yml @@ -289,6 +289,7 @@ #enable_ceilometer_ipmi: "no" #enable_cells: "no" #enable_central_logging: "no" +#enable_ceph_rgw: "no" #enable_chrony: "yes" #enable_cinder: "no" #enable_cinder_backup: "yes" diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml index 68b4ed9336..1abff9444c 100644 --- a/etc/kolla/passwords.yml +++ b/etc/kolla/passwords.yml @@ -261,3 +261,8 @@ redis_master_password: #################### prometheus_mysql_exporter_database_password: prometheus_alertmanager_password: + +#################### +# Ceph RadosGW options +#################### +ceph_rgw_keystone_password: diff --git a/releasenotes/notes/ceph-rgw-062e0544a004f7b1.yaml b/releasenotes/notes/ceph-rgw-062e0544a004f7b1.yaml new file mode 100644 index 0000000000..b17de1205a --- /dev/null +++ b/releasenotes/notes/ceph-rgw-062e0544a004f7b1.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for integration with Ceph RadosGW. diff --git a/tests/templates/globals-default.j2 b/tests/templates/globals-default.j2 index bccee85739..14047689d6 100644 --- a/tests/templates/globals-default.j2 +++ b/tests/templates/globals-default.j2 @@ -124,6 +124,11 @@ enable_cinder: "yes" glance_backend_ceph: "yes" cinder_backend_ceph: "yes" nova_backend_ceph: "yes" +enable_ceph_rgw: {{ not is_upgrade or previous_release != 'victoria' }} +ceph_rgw_hosts: +{% for host in hostvars %} + - {{ hostvars[host]['ansible_host'] }}:6780 +{% endfor %} {% endif %} {% if tls_enabled %} From 6c304fe267098cf71ab459541d2aa1c41a842750 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Date: Mon, 21 Jan 2019 12:25:39 +0100 Subject: [PATCH 5/6] Add support to OpenID Connect Authentication flow This pull request adds support for the OpenID Connect authentication flow in Keystone and enables both ID and access token authentication flows. The ID token configuration is designed to allow users to authenticate via Horizon using an identity federation; whereas the Access token is used to allow users to authenticate in the OpenStack CLI using a federated user. Without this PR, if one wants to configure OpenStack to use identity federation, he/she needs to do a lot of configurations in the keystone, Horizon, and register quite a good number of different parameters using the CLI such as mappings, identity providers, federated protocols, and so on. Therefore, with this PR, we propose a method for operators to introduce/present the IdP's metadata to Kolla-ansible, and based on the presented metadata, Kolla-ansible takes care of all of the configurations to prepare OpenStack to work in a federated environment. Implements: blueprint add-openid-support Co-Authored-By: Jason Anderson Change-Id: I0203a3470d7f8f2a54d5e126d947f540d93b8210 (cherry picked from commit f3fbe837081546c5bfa194f1e8c89f5769b96d5a) --- ansible/group_vars/all.yml | 44 ++++ .../roles/haproxy-config/defaults/main.yml | 2 + ansible/roles/horizon/defaults/main.yml | 8 +- .../roles/horizon/templates/local_settings.j2 | 27 +- ansible/roles/keystone/defaults/main.yml | 22 ++ .../keystone/tasks/config-federation-oidc.yml | 86 +++++++ ansible/roles/keystone/tasks/config.yml | 4 + ansible/roles/keystone/tasks/deploy.yml | 4 + .../tasks/register_identity_providers.yml | 238 ++++++++++++++++++ .../roles/keystone/templates/keystone.conf.j2 | 15 ++ .../roles/keystone/templates/keystone.json.j2 | 27 ++ .../keystone/templates/wsgi-keystone.conf.j2 | 45 ++++ doc/source/contributor/index.rst | 1 + .../contributor/setup-identity-provider.rst | 193 ++++++++++++++ .../shared-services/keystone-guide.rst | 238 ++++++++++++++++++ etc/kolla/passwords.yml | 5 + ...rt-to-openid-connect-859b12492f8347fe.yaml | 5 + 17 files changed, 951 insertions(+), 13 deletions(-) create mode 100644 ansible/roles/keystone/tasks/config-federation-oidc.yml create mode 100644 ansible/roles/keystone/tasks/register_identity_providers.yml create mode 100644 doc/source/contributor/setup-identity-provider.rst create mode 100644 releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index d7ca83b968..9338d7f784 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -591,6 +591,7 @@ enable_glance: "{{ enable_openstack_core | bool }}" enable_haproxy: "yes" enable_keepalived: "{{ enable_haproxy | bool }}" enable_keystone: "{{ enable_openstack_core | bool }}" +enable_keystone_federation: "{{ (keystone_identity_providers | length > 0) and (keystone_identity_mappings | length > 0) }}" enable_mariadb: "yes" enable_memcached: "yes" enable_neutron: "{{ enable_openstack_core | bool }}" @@ -1063,6 +1064,7 @@ enable_nova_horizon_policy_file: "{{ enable_nova }}" horizon_enable_tls_backend: "{{ kolla_enable_tls_backend }}" horizon_internal_endpoint: "{{ internal_protocol }}://{{ kolla_internal_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_internal | bool else horizon_port }}" +horizon_public_endpoint: "{{ public_protocol }}://{{ kolla_external_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_external | bool else horizon_port }}" ################# # Qinling options @@ -1219,3 +1221,45 @@ swift_public_endpoint: "{{ public_protocol }}://{{ swift_external_fqdn | put_add octavia_admin_endpoint: "{{ admin_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" octavia_internal_endpoint: "{{ internal_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" octavia_public_endpoint: "{{ public_protocol }}://{{ octavia_external_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" + +################################### +# Identity federation configuration +################################### +# Here we configure all of the IdPs meta informations that will be required to implement identity federation with OpenStack Keystone. +# We require the administrator to enter the following metadata: +# * name (internal name of the IdP in Keystone); +# * openstack_domain (the domain in Keystone that the IdP belongs to) +# * protocol (the federated protocol used by the IdP; e.g. openid or saml); +# * identifier (the IdP identifier; e.g. https://accounts.google.com); +# * public_name (the public name that will be shown for users in Horizon); +# * attribute_mapping (the attribute mapping to be used for this IdP. This mapping is configured in the "keystone_identity_mappings" configuration); +# * metadata_folder (folder containing all the identity provider metadata as jsons named as the identifier without the protocol +# and with '/' escaped as %2F followed with '.provider' or '.client' or '.conf'; e.g. accounts.google.com.provider; PS, all .conf, +# .provider and .client jsons must be in the folder, even if you dont override any conf in the .conf json, you must leave it as an empty json '{}'); +# * certificate_file (the path to the Identity Provider certificate file, the file must be named as 'certificate-key-id.pem'; +# e.g. LRVweuT51StjMdsna59jKfB3xw0r8Iz1d1J1HeAbmlw.pem; You can find the key-id in the Identity provider '.well-known/openid-configuration' jwks_uri as kid); +# +# The IdPs meta information are to be presented to Kolla-Ansible as the following example: +# keystone_identity_providers: +# - name: "myidp1" +# openstack_domain: "my-domain" +# protocol: "openid" +# identifier: "https://accounts.google.com" +# public_name: "Authenticate via myidp1" +# attribute_mapping: "mappingId1" +# metadata_folder: "path/to/metadata/folder" +# certificate_file: "path/to/certificate/file.pem" +# +# We also need to configure the attribute mapping that is used by IdPs. +# The configuration of attribute mappings is a list of objects, where each +# object must have a 'name' (that mapps to the 'attribute_mapping' to the IdP +# object in the IdPs set), and the 'file' with a full qualified path to a mapping file. +# keystone_identity_mappings: +# - name: "mappingId1" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId1" +# - name: "mappingId2" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId2" +# - name: "mappingId3" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId3" +keystone_identity_providers: [] +keystone_identity_mappings: [] diff --git a/ansible/roles/haproxy-config/defaults/main.yml b/ansible/roles/haproxy-config/defaults/main.yml index 1fc226dac9..e8f6aa1181 100644 --- a/ansible/roles/haproxy-config/defaults/main.yml +++ b/ansible/roles/haproxy-config/defaults/main.yml @@ -13,3 +13,5 @@ haproxy_backend_tcp_extra: [] haproxy_health_check: "check inter 2000 rise 2 fall 5" haproxy_health_check_ssl: "check check-ssl inter 2000 rise 2 fall 5" + +haproxy_enable_federation_openid: "{{ keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}" diff --git a/ansible/roles/horizon/defaults/main.yml b/ansible/roles/horizon/defaults/main.yml index b8bc5a68e7..ea60351747 100644 --- a/ansible/roles/horizon/defaults/main.yml +++ b/ansible/roles/horizon/defaults/main.yml @@ -124,7 +124,7 @@ horizon_extra_volumes: "{{ default_extra_volumes }}" # OpenStack #################### horizon_logging_debug: "{{ openstack_logging_debug }}" -horizon_keystone_url: "{{ keystone_internal_url }}/v3" +horizon_keystone_url: "{{ keystone_public_url if horizon_use_keystone_public_url | bool else keystone_internal_url }}/v3" #################### @@ -145,3 +145,9 @@ horizon_dev_mode: "{{ kolla_dev_mode }}" horizon_murano_dev_mode: "{{ kolla_dev_mode }}" horizon_source_version: "{{ kolla_source_version }}" horizon_murano_source_version: "{{ kolla_source_version }}" + +# This variable was created for administrators to define which one of the Keystone's URLs should be configured in Horizon. +# In some cases, such as when using OIDC, horizon will need to be configured with Keystone's public URL. +# Therefore, instead of overriding the whole "horizon_keystone_url", this change allows an easier integration because +# the Keystone public URL is already defined with variable "keystone_public_url". +horizon_use_keystone_public_url: False diff --git a/ansible/roles/horizon/templates/local_settings.j2 b/ansible/roles/horizon/templates/local_settings.j2 index 136741b8cf..ecaba31d2b 100644 --- a/ansible/roles/horizon/templates/local_settings.j2 +++ b/ansible/roles/horizon/templates/local_settings.j2 @@ -209,8 +209,9 @@ OPENSTACK_HOST = "{{ kolla_internal_fqdn }}" OPENSTACK_KEYSTONE_URL = "{{ horizon_keystone_url }}" OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" +{% if enable_keystone_federation | bool %} # Enables keystone web single-sign-on if set to True. -#WEBSSO_ENABLED = False +WEBSSO_ENABLED = True # Determines which authentication choice to show as default. #WEBSSO_INITIAL_CHOICE = "credentials" @@ -223,13 +224,13 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" # Do not remove the mandatory credentials mechanism. # Note: The last two tuples are sample mapping keys to a identity provider # and federation protocol combination (WEBSSO_IDP_MAPPING). -#WEBSSO_CHOICES = ( -# ("credentials", _("Keystone Credentials")), -# ("oidc", _("OpenID Connect")), -# ("saml2", _("Security Assertion Markup Language")), -# ("acme_oidc", "ACME - OpenID Connect"), -# ("acme_saml2", "ACME - SAML2"), -#) +WEBSSO_KEYSTONE_URL = "{{ keystone_public_url }}/v3" +WEBSSO_CHOICES = ( + ("credentials", _("Keystone Credentials")), + {% for idp in keystone_identity_providers %} + ("{{ idp.name }}_{{ idp.protocol }}", "{{ idp.public_name }}"), + {% endfor %} +) # A dictionary of specific identity provider and federation protocol # combinations. From the selected authentication mechanism, the value @@ -238,10 +239,12 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" # specific WebSSO endpoint in keystone, otherwise it will use the value # as the protocol_id when redirecting to the WebSSO by protocol endpoint. # NOTE: The value is expected to be a tuple formatted as: (, ). -#WEBSSO_IDP_MAPPING = { -# "acme_oidc": ("acme", "oidc"), -# "acme_saml2": ("acme", "saml2"), -#} +WEBSSO_IDP_MAPPING = { +{% for idp in keystone_identity_providers %} + "{{ idp.name }}_{{ idp.protocol }}": ("{{ idp.name }}", "{{ idp.protocol }}"), +{% endfor %} +} +{% endif %} # Disable SSL certificate checks (useful for self-signed certificates): #OPENSTACK_SSL_NO_VERIFY = True diff --git a/ansible/roles/keystone/defaults/main.yml b/ansible/roles/keystone/defaults/main.yml index 75df4f76ab..9eb6a0836b 100644 --- a/ansible/roles/keystone/defaults/main.yml +++ b/ansible/roles/keystone/defaults/main.yml @@ -16,6 +16,7 @@ keystone_services: tls_backend: "{{ keystone_enable_tls_backend }}" port: "{{ keystone_public_port }}" listen_port: "{{ keystone_public_listen_port }}" + backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}" keystone_external: enabled: "{{ enable_keystone }}" mode: "http" @@ -23,6 +24,7 @@ keystone_services: tls_backend: "{{ keystone_enable_tls_backend }}" port: "{{ keystone_public_port }}" listen_port: "{{ keystone_public_listen_port }}" + backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}" keystone_admin: enabled: "{{ enable_keystone }}" mode: "http" @@ -177,3 +179,23 @@ keystone_ks_services: # TLS #################### keystone_enable_tls_backend: "{{ kolla_enable_tls_backend }}" + +############################### +# OpenStack identity federation +############################### +# Default OpenID Connect remote attribute key +keystone_remote_id_attribute_oidc: "HTTP_OIDC_ISS" +keystone_container_federation_oidc_metadata_folder: "{{ '/etc/apache2/metadata' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/metadata' }}" +keystone_container_federation_oidc_idp_certificate_folder: "{{ '/etc/apache2/cert' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/cert' }}" +keystone_container_federation_oidc_attribute_mappings_folder: "{{ container_config_directory }}/federation/oidc/attribute_maps" +keystone_host_federation_oidc_metadata_folder: "{{ node_config_directory }}/keystone/federation/oidc/metadata" +keystone_host_federation_oidc_idp_certificate_folder: "{{ node_config_directory }}/keystone/federation/oidc/cert" +keystone_host_federation_oidc_attribute_mappings_folder: "{{ node_config_directory }}/keystone/federation/oidc/attribute_maps" + +# These variables are used to define multiple trusted Horizon dashboards. +# keystone_trusted_dashboards: ['', '', ''] +keystone_trusted_dashboards: "{{ ['%s://%s/auth/websso/' % (public_protocol, kolla_external_fqdn), '%s/auth/websso/' % (horizon_public_endpoint)] if enable_horizon | bool else [] }}" +keystone_enable_federation_openid: "{{ enable_keystone_federation | bool and keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}" +keystone_should_remove_attribute_mappings: False +keystone_should_remove_identity_providers: False +keystone_federation_oidc_scopes: "openid email profile" diff --git a/ansible/roles/keystone/tasks/config-federation-oidc.yml b/ansible/roles/keystone/tasks/config-federation-oidc.yml new file mode 100644 index 0000000000..4171283273 --- /dev/null +++ b/ansible/roles/keystone/tasks/config-federation-oidc.yml @@ -0,0 +1,86 @@ +--- +- name: Remove OpenID certificate and metadata files + become: true + vars: + keystone: "{{ keystone_services['keystone'] }}" + file: + state: absent + path: "{{ item }}" + when: + - inventory_hostname in groups[keystone.group] + with_items: + - "{{ keystone_host_federation_oidc_metadata_folder }}" + - "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + - "{{ keystone_host_federation_oidc_attribute_mappings_folder }}" + +- name: Create OpenID configuration directories + vars: + keystone: "{{ keystone_services['keystone'] }}" + file: + dest: "{{ item }}" + state: "directory" + mode: "0770" + become: true + with_items: + - "{{ keystone_host_federation_oidc_metadata_folder }}" + - "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + - "{{ keystone_host_federation_oidc_attribute_mappings_folder }}" + when: + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenID Identity Providers metadata + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.metadata_folder }}/" + dest: "{{ keystone_host_federation_oidc_metadata_folder }}" + mode: "0660" + with_items: "{{ keystone_identity_providers }}" + when: + - item.protocol == 'openid' + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenID Identity Providers certificate + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.certificate_file }}" + dest: "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + mode: "0660" + with_items: "{{ keystone_identity_providers }}" + when: + - item.protocol == 'openid' + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenStack Identity Providers attribute mappings + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.file }}" + dest: "{{ keystone_host_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + mode: "0660" + with_items: "{{ keystone_identity_mappings }}" + when: + - inventory_hostname in groups[keystone.group] + +- name: Setting the certificates files variable + become: true + vars: + keystone: "{{ keystone_services['keystone'] }}" + find: + path: "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + pattern: "*.pem" + register: certificates_path + when: + - inventory_hostname in groups[keystone.group] + +- name: Setting the certificates variable + vars: + keystone: "{{ keystone_services['keystone'] }}" + set_fact: + keystone_federation_openid_certificate_key_ids: "{{ certificates_path.files | map(attribute='path') | map('regex_replace', '^.*/(.*)\\.pem$', '\\1#' + keystone_container_federation_oidc_idp_certificate_folder + '/\\1.pem') | list }}" # noqa 204 + when: + - inventory_hostname in groups[keystone.group] diff --git a/ansible/roles/keystone/tasks/config.yml b/ansible/roles/keystone/tasks/config.yml index 06ecea3a7c..bec1350a34 100644 --- a/ansible/roles/keystone/tasks/config.yml +++ b/ansible/roles/keystone/tasks/config.yml @@ -144,6 +144,10 @@ notify: - Restart {{ item.key }} container +- include_tasks: config-federation-oidc.yml + when: + - keystone_enable_federation_openid | bool + - name: Copying over wsgi-keystone.conf vars: keystone: "{{ keystone_services.keystone }}" diff --git a/ansible/roles/keystone/tasks/deploy.yml b/ansible/roles/keystone/tasks/deploy.yml index 656e44e312..a6ff99b0e9 100644 --- a/ansible/roles/keystone/tasks/deploy.yml +++ b/ansible/roles/keystone/tasks/deploy.yml @@ -19,3 +19,7 @@ - import_tasks: register.yml - import_tasks: check.yml + +- include_tasks: register_identity_providers.yml + when: + - enable_keystone_federation | bool diff --git a/ansible/roles/keystone/tasks/register_identity_providers.yml b/ansible/roles/keystone/tasks/register_identity_providers.yml new file mode 100644 index 0000000000..befcf41d3f --- /dev/null +++ b/ansible/roles/keystone/tasks/register_identity_providers.yml @@ -0,0 +1,238 @@ +--- +- name: List configured attribute mappings (that can be used by IdPs) + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping list -c ID --format value + run_once: True + become: True + register: existing_mappings_register + +- name: Register existing mappings + set_fact: + existing_mappings: "{{ existing_mappings_register.stdout_lines | map('trim') | list }}" + +- name: Remove unmanaged attribute mappings + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping delete {{ item }} + run_once: True + become: true + with_items: "{{ existing_mappings }}" + when: + - item not in (keystone_identity_mappings | map(attribute='name') | list) + - keystone_should_remove_attribute_mappings + +- name: Create unexisting domains + become: true + kolla_toolbox: + module_name: "os_keystone_domain" + module_args: + name: "{{ item.openstack_domain }}" + auth: "{{ openstack_auth }}" + endpoint_type: "{{ openstack_interface }}" + cacert: "{{ openstack_cacert }}" + region_name: "{{ openstack_region_name }}" + run_once: True + with_items: "{{ keystone_identity_providers }}" + +- name: Register attribute mappings in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping create + --rules "{{ keystone_container_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + {{ item.name }} + run_once: True + when: + - item.name not in existing_mappings + with_items: "{{ keystone_identity_mappings }}" + +- name: Update existing attribute mappings in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping set + --rules "{{ keystone_container_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + {{ item.name }} + run_once: True + when: + - item.name in existing_mappings + with_items: "{{ keystone_identity_mappings }}" + +- name: List configured IdPs + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider list -c ID --format value + run_once: True + register: existing_idps_register + +- name: Register existing idps + set_fact: + existing_idps: "{{ existing_idps_register.stdout.split('\n') | map('trim') | list }}" + +- name: Remove unmanaged identity providers + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider delete {{ item }} + run_once: True + with_items: "{{ existing_idps }}" + when: + - item not in (keystone_identity_providers | map(attribute='name') | list) + - keystone_should_remove_identity_providers + +- name: Register Identity Providers in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider create + --description "{{ item.public_name }}" + --remote-id "{{ item.identifier }}" + --domain "{{ item.openstack_domain }}" + {{ item.name }} + run_once: True + when: + - item.name not in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Update Identity Providers in OpenStack according to Kolla-Ansible configuraitons + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider set + --description "{{ item.public_name }}" + --remote-id "{{ item.identifier }}" + "{{ item.name }}" + run_once: True + when: + - item.name in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Configure attribute mappings for each Identity Provider. (We expect the mappings to be configured by the operator) + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + federation protocol create + --mapping {{ item.attribute_mapping }} + --identity-provider {{ item.name }} + {{ item.protocol }} + run_once: True + when: + - item.name not in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Update attribute mappings for each Identity Provider. (We expect the mappings to be configured by the operator). + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + federation protocol set + --identity-provider {{ item.name }} + --mapping {{ item.attribute_mapping }} + {{ item.protocol }} + run_once: True + register: result + failed_when: result.rc not in [0, 1] # This command returns RC 1 on success, so we need to add this to avoid fails. + when: + - item.name in existing_idps + with_items: "{{ keystone_identity_providers }}" diff --git a/ansible/roles/keystone/templates/keystone.conf.j2 b/ansible/roles/keystone/templates/keystone.conf.j2 index 730107eaca..f1e787b6f5 100644 --- a/ansible/roles/keystone/templates/keystone.conf.j2 +++ b/ansible/roles/keystone/templates/keystone.conf.j2 @@ -77,3 +77,18 @@ connection_string = {{ osprofiler_backend_connection_string }} [cors] allowed_origin = {{ grafana_public_endpoint }} {% endif %} + +{% if enable_keystone_federation %} +[federation] +{% for dashboard in keystone_trusted_dashboards %} +trusted_dashboard = {{ dashboard }} +{% endfor %} + +sso_callback_template = /etc/keystone/sso_callback_template.html + +[openid] +remote_id_attribute = {{ keystone_remote_id_attribute_oidc }} + +[auth] +methods = password,token,openid,application_credential +{% endif %} diff --git a/ansible/roles/keystone/templates/keystone.json.j2 b/ansible/roles/keystone/templates/keystone.json.j2 index e5c676190b..2dee915eb7 100644 --- a/ansible/roles/keystone/templates/keystone.json.j2 +++ b/ansible/roles/keystone/templates/keystone.json.j2 @@ -1,4 +1,5 @@ {% set keystone_dir = 'apache2/conf-enabled' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd/conf.d' %} +{% set apache_user = 'www-data' if kolla_base_distro in ['ubuntu', 'debian'] else 'apache' %} { "command": "/usr/bin/keystone-startup.sh", "config_files": [ @@ -52,6 +53,22 @@ "owner": "keystone", "perm": "0600" }{% endif %} + {% if keystone_enable_federation_openid %}, + { + "source": "{{ container_config_directory }}/federation/oidc/metadata", + "dest": "{{ keystone_container_federation_oidc_metadata_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0600", + "merge": true + }, + { + "source": "{{ container_config_directory }}/federation/oidc/cert", + "dest": "{{ keystone_container_federation_oidc_idp_certificate_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0600", + "merge": true + } + {% endif %} ], "permissions": [ { @@ -61,7 +78,17 @@ { "path": "/var/log/kolla/keystone/keystone.log", "owner": "keystone:keystone" + },{% if keystone_enable_federation_openid %} + { + "path": "{{ keystone_container_federation_oidc_metadata_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0700" }, + { + "path": "{{ keystone_container_federation_oidc_idp_certificate_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0700" + },{% endif %} { "path": "/etc/keystone/fernet-keys", "owner": "keystone:keystone", diff --git a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 index 1d62274659..83886415b0 100644 --- a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 +++ b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 @@ -51,6 +51,51 @@ LogLevel info SSLCertificateFile /etc/keystone/certs/keystone-cert.pem SSLCertificateKeyFile /etc/keystone/certs/keystone-key.pem {% endif %} + +{% if keystone_enable_federation_openid %} + OIDCClaimPrefix "OIDC-" + OIDCClaimDelimiter ";" + OIDCResponseType "id_token" + OIDCScope "{{ keystone_federation_oidc_scopes }}" + OIDCMetadataDir {{ keystone_container_federation_oidc_metadata_folder }} +{% if keystone_federation_openid_certificate_key_ids | length > 0 %} + OIDCOAuthVerifyCertFiles {{ keystone_federation_openid_certificate_key_ids | join(" ") }} +{% endif %} + OIDCCryptoPassphrase {{ keystone_federation_openid_crypto_password }} + OIDCRedirectURI {{ keystone_public_url }}/redirect_uri + + + Require valid-user + AuthType openid-connect + + + {# WebSSO authentication endpoint -#} + + Require valid-user + AuthType openid-connect + + +{% for idp in keystone_identity_providers %} +{% if idp.protocol == 'openid' %} + + Require valid-user + AuthType openid-connect + +{% endif %} +{% endfor %} + + {# CLI / API authentication endpoint -#} +{% for idp in keystone_identity_providers %} +{% if idp.protocol == 'openid' %} + + Require valid-user + {# Note(jasonanderson): `auth-openidc` is a special auth type that can -#} + {# additionally handle verifying bearer tokens -#} + AuthType auth-openidc + +{% endif %} +{% endfor %} +{% endif %} diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 9681dc966a..fb61fccfd6 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -21,3 +21,4 @@ We welcome everyone to join our project! bug-triage ptl-guide release-management + setup-identity-provider diff --git a/doc/source/contributor/setup-identity-provider.rst b/doc/source/contributor/setup-identity-provider.rst new file mode 100644 index 0000000000..99e5ae7807 --- /dev/null +++ b/doc/source/contributor/setup-identity-provider.rst @@ -0,0 +1,193 @@ +.. _setup-identity-provider: + +============================ +Test Identity Provider setup +============================ + +This guide shows how to create an Identity Provider that handles the OpenID +Connect protocol to authenticate users when +:keystone-doc:`using Federation with OpenStack +` (these configurations must not +be used in a production environment). + +Keycloak +======== + +Keycloak is a Java application that implements an Identity Provider handling +both OpenID Connect and SAML protocols. + +To setup a Keycloak instance for testing is pretty simple with Docker. + +Creating the Docker Keycloak instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run the docker command: + +.. code-block:: console + + docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:latest + +This will create a Keycloak instance that has the admin credentials as +admin/admin and is listening on port 8080. + +After creating the instance, you will need to log in to the Keycloak as +administrator and setup the first Identity Provider. + +Creating an Identity Provider with Keycloak +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following guide assumes that the steps are executed from the same machine +(localhost), but you can change the hostname if you want to run it from +elsewhere. + +In this guide, we will use the 'new_realm' as the realm name in Keycloak, so, +if you want to use any other realm name, you must to change 'new_realm' in the +URIs used in the guide and replace the 'new_realm' with the realm name that you +are using. + +- Access the admin console on http://localhost:8080/auth/ in the Administration Console option. +- Authenticate using the credentials defined in the creation step. +- Create a new realm in the http://localhost:8080/auth/admin/master/console/#/create/realm page. +- After creating a realm, you will need to create a client to be used by Keystone; to do it, just access http://localhost:8080/auth/admin/master/console/#/create/client/new_realm. +- To create a client, you will need to set the client_id (just choose anyone), + the protocol (must be openid-connect) and the Root Url (you can leave it + blank) +- After creating the client, you will need to update some client's attributes + like: + + - Enable the Implicit flow (this one allows you to use the OpenStack CLI with + oidcv3 plugin) + - Set Access Type to confidential + - Add the Horizon and Keystone URIs to the Valid Redirect URIs. Keystone should be within the '/redirect_uri' path, for example: https://horizon.com/ and https://keystone.com/redirect_uri + - Save the changes + - Access the client's Mappers tab to add the user's attributes that will be + shared with the client (Keystone): + + - In this guide, we will need the following attribute mappers in Keycloak: + + ==================================== ============== + name/user attribute/token claim name mapper type + ==================================== ============== + openstack-user-domain user attribute + openstack-default-project user attribute + ==================================== ============== + +- After creating the client, you will need to create a user in that realm to + log in OpenStack via identity federation +- To create a user, access http://localhost:8080/auth/admin/master/console/#/create/user/new_realm and fill the form with the user's data +- After creating the user, you can access the tab "Credentials" to set the + user's password +- Then, in the tab "Attributes", you must set the authorization attributes to + be used by Keystone, these attributes are defined in the :ref:`attribute + mapping ` in Keystone + +After you create the Identity provider, you will need to get some data from the +Identity Provider to configure in Kolla-Ansible + +Configuring Kolla Ansible to use the Identity Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section is about how one can get the data needed in +:ref:`Setup OIDC via Kolla Ansible `. + +- name: The realm name, in this case it will be "new_realm" +- identifier: http://localhost:8080/auth/realms/new_realm/ (again, the "new_realm" is the name of the realm) +- certificate_file: This one can be downloaded from http://localhost:8080/auth/admin/master/console/#/realms/new_realm/keys +- metadata_folder: + + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.client: + + - client_id: Access http://localhost:8080/auth/admin/master/console/#/realms/new_realm/clients , and access the client you created for Keystone, copy the Client ID displayed in the page + - client_secret: In the same page you got the client_id, access the tab + "Credentials" and copy the secret value + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.provider: Copy the json from http://localhost:8080/auth/realms/new_realm/.well-known/openid-configuration (the "new_realm" is the realm name) + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.conf: You can leave this file + as an empty json "{}" + + +After you finished the configuration of the Identity Provider, your main +configuration should look something like the following: + +.. code-block:: + + keystone_identity_providers: + - name: "new_realm" + openstack_domain: "new_domain" + protocol: "openid" + identifier: "http://localhost:8080/auth/realms/new_realm" + public_name: "Authenticate via new_realm" + attribute_mapping: "attribute_mapping_keycloak_new_realm" + metadata_folder: "/root/inDev/meta-idp" + certificate_file: "/root/inDev/certs/LRVweuT51StjMdsna59jKfB3xw0r8Iz1d1J1HeAbmlw.pem" + keystone_identity_mappings: + - name: "attribute_mapping_keycloak_new_realm" + file: "/root/inDev/attr_map/attribute_mapping.json" + +Then, after deploying OpenStack, you should be able to log in Horizon +using the "Authenticate using" -> "Authenticate via new_realm", and writing +"new_realm.com" in the "E-mail or domain name" field. After that, you will be +redirected to a new page to choose the Identity Provider in Keystone. Just click in the link +"localhost:8080/auth/realms/new_realm"; this will redirect you to Keycloak (idP) where +you will need to log in with the user that you created. If the user's +attributes in Keycloak are ok, the user will be created in OpenStack and you will +be able to log in Horizon. + +.. _attribute_mapping: + +Attribute mapping +~~~~~~~~~~~~~~~~~ +This section shows how to create the attribute mapping to map an Identity +Provider user to a Keystone user (ephemeral). + +The 'OIDC-' prefix in the remote types is defined in the 'OIDCClaimPrefix' +configuration in the wsgi-keystone.conf file; this prefix must be in the +attribute mapping as the mod-oidc-wsgi is adding the prefix in the user's +attributes before sending it to Keystone. The attribute 'openstack-user-domain' +will define the user's domain in OpenStack and the attribute +'openstack-default-project' will define the user's project in the OpenStack +(the user will be assigned with the role 'member' in the project) + +.. code-block:: json + + [ + { + "local": [ + { + "user": { + "name": "{0}", + "email": "{1}", + "domain": { + "name": "{2}" + } + }, + "domain": { + "name": "{2}" + }, + "projects": [ + { + "name": "{3}", + "roles": [ + { + "name": "member" + } + ] + } + ] + } + ], + "remote": [ + { + "type": "OIDC-preferred_username" + }, + { + "type": "OIDC-email" + }, + { + "type": "OIDC-openstack-user-domain" + }, + { + "type": "OIDC-openstack-default-project" + } + ] + } + ] diff --git a/doc/source/reference/shared-services/keystone-guide.rst b/doc/source/reference/shared-services/keystone-guide.rst index 2012b868f6..126e53c3d9 100644 --- a/doc/source/reference/shared-services/keystone-guide.rst +++ b/doc/source/reference/shared-services/keystone-guide.rst @@ -40,3 +40,241 @@ be configured in Keystone as necessary. Further infomation on Fernet tokens is available in the :keystone-doc:`Keystone documentation `. + +Federated identity +------------------ + +Keystone allows users to be authenticated via identity federation. This means +integrating OpenStack Keystone with an identity provider. The use of identity +federation allows users to access OpenStack services without the necessity of +an account in the OpenStack environment per se. The authentication is then +off-loaded to the identity provider of the federation. + +To enable identity federation, you will need to execute a set of configurations +in multiple OpenStack systems. Therefore, it is easier to use Kolla Ansible +to execute this process for operators. + +For upstream documentations, please see +:keystone-doc:`Configuring Keystone for Federation +` + +Supported protocols +~~~~~~~~~~~~~~~~~~~ + +OpenStack supports both OpenID Connect and SAML protocols for federated +identity, but for now, kolla Ansible supports only OpenID Connect. +Therefore, if you desire to use SAML in your environment, you will need +to set it up manually or extend Kolla Ansible to also support it. + +.. _setup-oidc-kolla-ansible: + +Setting up OpenID Connect via Kolla Ansible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you will need to register the OpenStack (Keystone) in your Identity +provider as a Service Provider. + +After registering Keystone, you will need to add the Identity Provider +configurations in your kolla-ansible globals configuration as the example +below: + +.. code-block:: yaml + + keystone_identity_providers: + - name: "myidp1" + openstack_domain: "my-domain" + protocol: "openid" + identifier: "https://accounts.google.com" + public_name: "Authenticate via myidp1" + attribute_mapping: "mappingId1" + metadata_folder: "path/to/metadata/folder" + certificate_file: "path/to/certificate/file.pem" + + keystone_identity_mappings: + - name: "mappingId1" + file: "/full/qualified/path/to/mapping/json/file/to/mappingId1" + +Identity providers configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +name +**** + +The internal name of the Identity provider in OpenStack. + +openstack_domain +**************** + +The OpenStack domain that the Identity Provider belongs. + +protocol +******** + +The federated protocol used by the IdP; e.g. openid or saml. We support only +OpenID connect right now. + +identifier +********** + +The Identity provider URL; e.g. https://accounts.google.com . + +public_name +*********** + +The Identity provider public name that will be shown for users in the Horizon +login page. + +attribute_mapping +***************** + +The attribute mapping to be used for the Identity Provider. This mapping is +expected to already exist in OpenStack or be configured in the +`keystone_identity_mappings` property. + +metadata_folder +*************** + +Path to the folder containing all of the identity provider metadata as JSON +files. + +The metadata folder must have all your Identity Providers configurations, +the name of the files will be the name (with path) of the Issuer configuration. +Such as: + +.. code-block:: + + - + - keycloak.example.org%2Fauth%2Frealms%2Fidp.client + | + - keycloak.example.org%2Fauth%2Frealms%2Fidp.conf + | + - keycloak.example.org%2Fauth%2Frealms%2Fidp.provider + +.. note:: + + The name of the file must be URL-encoded if needed. For example, if you have + an Issuer with ``/`` in the URL, then you need to escape it to ``%2F`` by + applying a URL escape in the file name. + +The content of these files must be a JSON + +``client``: + +The ``.client`` file handles the Service Provider credentials in the Issuer. + +During the first step, when you registered the OpenStack as a +Service Provider in the Identity Provider, you submitted a `cliend_id` and +generated a `client_secret`, so these are the values you must use in this +JSON file. + +.. code-block:: json + + { + "client_id":"", + "client_secret":"" + } + +``conf``: + +This file will be a JSON that overrides some of the OpenID Connect options. The +options that can be overridden are listed in the +`OpenID Connect Apache2 plugin documentation`_. +.. _`OpenID Connect Apache2 plugin documentation`: https://github.com/zmartzone/mod_auth_openidc/wiki/Multiple-Providers#opclient-configuration + +If you do not want to override the config values, you can leave this file as +an empty JSON file such as ``{}``. + +``provider``: + +This file will contain all specifications about the IdentityProvider. To +simplify, you can just use the JSON returned in the ``.well-known`` +Identity provider's endpoint: + +.. code-block:: json + + { + "issuer": "https://accounts.google.com", + "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "token_endpoint": "https://oauth2.googleapis.com/token", + "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", + "revocation_endpoint": "https://oauth2.googleapis.com/revoke", + "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", + "response_types_supported": [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "scopes_supported": [ + "openid", + "email", + "profile" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "claims_supported": [ + "aud", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "iss", + "locale", + "name", + "picture", + "sub" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ] + } + +certificate_file +**************** + +Path to the Identity Provider certificate file, the file must be named as +'certificate-key-id.pem'. E.g. + +.. code-block:: + + - fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9.pem + +You can find the key-id in the Identity provider +`.well-known/openid-configuration` `jwks_uri` like in +`https://www.googleapis.com/oauth2/v3/certs` : + +.. code-block:: json + + { + "keys": [ + { + "e": "AQAB", + "use": "sig", + "n": "zK8PHf_6V3G5rU-viUOL1HvAYn7q--dxMoU...", + "kty": "RSA", + "kid": "fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9", + "alg": "RS256" + } + ] + } + +.. note:: + + The public key is different from the certificate, the file in this + configuration must be the Identity provider's certificate and not the + Identity provider's public key. diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml index 1abff9444c..033cd41e50 100644 --- a/etc/kolla/passwords.yml +++ b/etc/kolla/passwords.yml @@ -266,3 +266,8 @@ prometheus_alertmanager_password: # Ceph RadosGW options #################### ceph_rgw_keystone_password: + +############################### +# OpenStack identity federation +############################### +keystone_federation_openid_crypto_password: diff --git a/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml b/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml new file mode 100644 index 0000000000..956c3cb5cc --- /dev/null +++ b/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the OpenID Connect authentication protocol in Keystone and + enables both ID and access token authentication flows. From 67ccb7e2d30920ff274ba3b000c83c14dd75faf3 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 28 May 2021 17:22:31 +0100 Subject: [PATCH 6/6] Add OIDCDiscoverURL mod_oidc option This gets rid of one of the steps in the authentication flow. Closes-Bug: 1930055 Change-Id: I4ed4651b55a912f1d9aec7277bae6bb4776f1e0a (cherry picked from commit 510508e9fa416801af58c1aedcf24e0bf8e88194) --- ansible/roles/keystone/templates/wsgi-keystone.conf.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 index 83886415b0..6c6e96450c 100644 --- a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 +++ b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 @@ -78,6 +78,7 @@ LogLevel info {% for idp in keystone_identity_providers %} {% if idp.protocol == 'openid' %} + OIDCDiscoverURL {{ keystone_public_url }}/redirect_uri?iss={{ item.identifier | urlencode }} Require valid-user AuthType openid-connect