Support authentication in Bifrost
* Switch from python-ironic-inspector-client to openstacksdk in ironic-inspector-rules. This allows us to use clouds.yaml to provide credentials. * Enable authentication in Bifrost. Passwords are auto-generated by Bifrost, and stored files in /root/.config/bifrost/. This change depends on a Kolla Ansible patch that ensures that these credentials are persisted between recreations of the bifrost container. * Copy clouds.yaml and (if present) a CA certificate from the Bifrost container to the seed host, under the Kayobe Ansible user (stack). This allows us to use the credentials to register introspection rules. * This patch is needed by a Kolla Ansible patch that enables TLS in Bifrost, since we need the CA certificate on the host to register introspection rules when TLS is enabled. Depends-On: https://review.opendev.org/c/openstack/kolla-ansible/+/851837 Needed-By: https://review.opendev.org/c/openstack/kolla-ansible/+/851838 Story: 2010206 Task: 45930 Change-Id: I757f1bb72afb01a4f1689bed292f5b71b9048fa0
This commit is contained in:
parent
17d49908af
commit
32a82ea039
@ -143,7 +143,6 @@
|
||||
bash -c '
|
||||
export OS_CLOUD=bifrost &&
|
||||
export BIFROST_INVENTORY_SOURCE=ironic &&
|
||||
export OS_BAREMETAL_API_VERSION=1.34 &&
|
||||
ansible-playbook -vvvv
|
||||
/bifrost/playbooks/deploy-dynamic.yaml
|
||||
--inventory /etc/bifrost/inventory/
|
||||
|
@ -26,8 +26,7 @@ compatible with the `auth` argument of `os_*` Ansible modules.
|
||||
|
||||
`ironic_inspector_cacert` is an optional path to a CA certificate.
|
||||
|
||||
`ironic_inspector_url` is the URL of Ironic Inspector API endpoint,
|
||||
required if no authentication is used.
|
||||
`ironic_inspector_cloud` is the name of a cloud in ``clouds.yaml``.
|
||||
|
||||
`ironic_inspector_rules` is a list of introspection rules which should
|
||||
exist. See the Inspector rules API for details of parameters available
|
||||
|
@ -14,12 +14,12 @@ ironic_inspector_auth: {}
|
||||
# CA certificate path.
|
||||
ironic_inspector_cacert:
|
||||
|
||||
# Name of cloud in clouds.yaml.
|
||||
ironic_inspector_cloud:
|
||||
|
||||
# Interface (public, internal, admin).
|
||||
ironic_inspector_interface:
|
||||
|
||||
# URL of Ironic Inspector API endpoint.
|
||||
ironic_inspector_url:
|
||||
|
||||
# List of rules which should exist. See the Inspector rules API for details of
|
||||
# parameters available for rules.
|
||||
ironic_inspector_rules: []
|
||||
|
@ -21,10 +21,6 @@ from ansible.module_utils.openstack import *
|
||||
|
||||
# Store a list of import errors to report to the user.
|
||||
IMPORT_ERRORS = []
|
||||
try:
|
||||
import ironic_inspector_client
|
||||
except Exception as e:
|
||||
IMPORT_ERRORS.append(e)
|
||||
try:
|
||||
import openstack
|
||||
except Exception as e:
|
||||
@ -78,29 +74,21 @@ os_ironic_inspector_rule:
|
||||
"""
|
||||
|
||||
|
||||
def _build_client(module, cloud):
|
||||
"""Create and return an Ironic inspector client."""
|
||||
# Ensure the requested API version is supported.
|
||||
# API 1.14 is the latest API version available in Rocky.
|
||||
api_version = (1, 14)
|
||||
client = ironic_inspector_client.v1.ClientV1(
|
||||
inspector_url=module.params['inspector_url'],
|
||||
interface=module.params['interface'],
|
||||
session=cloud.session, region_name=module.params['region_name'],
|
||||
api_version=api_version)
|
||||
return client
|
||||
def _get_client(module, cloud):
|
||||
"""Return an Ironic inspector client."""
|
||||
return cloud.baremetal_introspection
|
||||
|
||||
|
||||
def _ensure_rule_present(module, client):
|
||||
"""Ensure that an inspector rule is present."""
|
||||
if module.params['uuid']:
|
||||
try:
|
||||
rule = client.rules.get(module.params['uuid'])
|
||||
except ironic_inspector_client.ClientError as e:
|
||||
if e.response.status_code != 404:
|
||||
response = client.get('/rules/{}'.format(module.params['uuid']))
|
||||
if not response.ok:
|
||||
if response.status_code != 404:
|
||||
module.fail_json(msg="Failed retrieving Inspector rule %s: %s"
|
||||
% (module.params['uuid'], repr(e)))
|
||||
else:
|
||||
rule = response.json()
|
||||
# Check whether the rule differs from the request.
|
||||
keys = ('conditions', 'actions', 'description')
|
||||
for key in keys:
|
||||
@ -121,8 +109,16 @@ def _ensure_rule_present(module, client):
|
||||
# Rule differs - delete it before recreating.
|
||||
_ensure_rule_absent(module, client)
|
||||
|
||||
client.rules.create(module.params['conditions'], module.params['actions'],
|
||||
module.params['uuid'], module.params['description'])
|
||||
rule = {
|
||||
"conditions": module.params['conditions'],
|
||||
"actions": module.params['actions'],
|
||||
"description": module.params['description'],
|
||||
"uuid": module.params['uuid'],
|
||||
}
|
||||
response = client.post("/rules", json=rule)
|
||||
if not response.ok:
|
||||
module.fail_json(msg="Failed creating Inspector rule %s: %s"
|
||||
% (module.params['uuid'], response.text))
|
||||
return True
|
||||
|
||||
|
||||
@ -130,14 +126,13 @@ def _ensure_rule_absent(module, client):
|
||||
"""Ensure that an inspector rule is absent."""
|
||||
if not module.params['uuid']:
|
||||
module.fail_json(msg="UUID is required to ensure rules are absent")
|
||||
try:
|
||||
client.rules.delete(module.params['uuid'])
|
||||
except ironic_inspector_client.ClientError as e:
|
||||
response = client.delete("/rules/{}".format(module.params['uuid']))
|
||||
if not response.ok:
|
||||
# If the rule does not exist, no problem and no change.
|
||||
if e.response.status_code == 404:
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
module.fail_json(msg="Failed retrieving Inspector rule %s: %s"
|
||||
% (module.params['uuid'], repr(e)))
|
||||
% (module.params['uuid'], response.text))
|
||||
return True
|
||||
|
||||
|
||||
@ -149,7 +144,6 @@ def main():
|
||||
uuid=dict(required=False),
|
||||
state=dict(required=False, default='present',
|
||||
choices=['present', 'absent']),
|
||||
inspector_url=dict(required=False),
|
||||
)
|
||||
module_kwargs = openstack_module_kwargs()
|
||||
module = AnsibleModule(argument_spec, **module_kwargs)
|
||||
@ -159,20 +153,9 @@ def main():
|
||||
module.fail_json(msg="Import errors: %s" %
|
||||
", ".join([repr(e) for e in IMPORT_ERRORS]))
|
||||
|
||||
if (module.params['auth_type'] in [None, 'None'] and
|
||||
module.params['inspector_url'] is None):
|
||||
module.fail_json(msg="Authentication appears disabled, please "
|
||||
"define an inspector_url parameter")
|
||||
|
||||
if (module.params['inspector_url'] and
|
||||
module.params['auth_type'] in [None, 'None']):
|
||||
module.params['auth'] = dict(
|
||||
endpoint=module.params['inspector_url']
|
||||
)
|
||||
|
||||
sdk, cloud = openstack_cloud_from_module(module)
|
||||
try:
|
||||
client = _build_client(module, cloud)
|
||||
client = _get_client(module, cloud)
|
||||
if module.params["state"] == "present":
|
||||
changed = _ensure_rule_present(module, client)
|
||||
else:
|
||||
|
@ -1,14 +1,4 @@
|
||||
---
|
||||
- name: Ensure required Python packages are installed
|
||||
pip:
|
||||
name: "{{ item.name }}"
|
||||
version: "{{ item.version | default(omit) }}"
|
||||
state: latest
|
||||
virtualenv: "{{ ironic_inspector_venv }}"
|
||||
extra_args: "{% if ironic_inspector_upper_constraints_file %}-c {{ ironic_inspector_upper_constraints_file }}{% endif %}"
|
||||
with_items:
|
||||
- name: python-ironic-inspector-client
|
||||
|
||||
- name: Ensure introspection rules exist
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ ironic_inspector_venv }}/bin/python"
|
||||
@ -16,11 +6,11 @@
|
||||
auth_type: "{{ ironic_inspector_auth_type }}"
|
||||
auth: "{{ ironic_inspector_auth }}"
|
||||
cacert: "{{ ironic_inspector_cacert | default(omit, true) }}"
|
||||
cloud: "{{ ironic_inspector_cloud | default(omit, true) }}"
|
||||
interface: "{{ ironic_inspector_interface | default(omit, true) }}"
|
||||
conditions: "{{ item.conditions }}"
|
||||
actions: "{{ item.actions }}"
|
||||
description: "{{ item.description | default(omit) }}"
|
||||
uuid: "{{ item.uuid | default(item.description | to_uuid) | default(omit) }}"
|
||||
state: present
|
||||
inspector_url: "{{ ironic_inspector_url }}"
|
||||
with_items: "{{ ironic_inspector_rules }}"
|
||||
|
@ -71,9 +71,6 @@ use_firewalld: "{{ kolla_bifrost_use_firewalld }}"
|
||||
# Firewalld zone used by Bifrost.
|
||||
firewalld_internal_zone: "{{ kolla_bifrost_firewalld_internal_zone }}"
|
||||
|
||||
# Disable authentication for the Ironic and Inspector APIs.
|
||||
noauth_mode: true
|
||||
|
||||
# Enable discovery of nodes in Ironic Inspector.
|
||||
enable_inspector_discovery: true
|
||||
|
||||
|
45
ansible/seed-credentials.yml
Normal file
45
ansible/seed-credentials.yml
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
# Copy the Bifrost clouds.yaml file and CA certificate (if one is in use) to
|
||||
# the host. This allows us to access the Ironic and Inspector APIs outside of
|
||||
# the Bifrost container.
|
||||
- name: Ensure credentials are available on the host
|
||||
hosts: seed
|
||||
tags:
|
||||
- seed-credentials
|
||||
vars:
|
||||
openstack_config_dir: "{{ ansible_facts.env.HOME }}/.config/openstack"
|
||||
tasks:
|
||||
- name: Ensure OpenStack config directory exists
|
||||
file:
|
||||
path: "{{ openstack_config_dir }}"
|
||||
state: directory
|
||||
mode: 0700
|
||||
|
||||
- name: Get clouds.yaml from Bifrost container
|
||||
command:
|
||||
cmd: docker exec bifrost_deploy cat /root/.config/openstack/clouds.yaml
|
||||
changed_when: false
|
||||
register: clouds_yaml
|
||||
no_log: true
|
||||
|
||||
- name: Write clouds.yaml
|
||||
copy:
|
||||
content: |
|
||||
{%- set clouds = clouds_yaml.stdout | from_yaml -%}
|
||||
{%- for cloud in clouds.clouds.keys() | list -%}
|
||||
{%- if 'cacert' in clouds.clouds[cloud] -%}
|
||||
{%- set _ = clouds.clouds[cloud].update({'cacert': openstack_config_dir ~ '/bifrost.crt'}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ clouds | to_nice_yaml }}
|
||||
dest: "{{ openstack_config_dir }}/clouds.yaml"
|
||||
mode: 0600
|
||||
|
||||
- name: Copy CA certificate from Bifrost container
|
||||
vars:
|
||||
clouds: "{{ clouds_yaml.stdout | from_yaml }}"
|
||||
cacerts: "{{ clouds.clouds.values() | selectattr('cacert', 'defined') | map(attribute='cacert') | list }}"
|
||||
command:
|
||||
cmd: docker cp bifrost_deploy:{{ cacerts[0] }} {{ openstack_config_dir }}/bifrost.crt
|
||||
changed_when: false
|
||||
when: cacerts | length > 0
|
@ -9,10 +9,7 @@
|
||||
os_openstacksdk_state: latest
|
||||
ironic_inspector_venv: "{{ virtualenv_path }}/openstacksdk"
|
||||
ironic_inspector_upper_constraints_file: "{{ pip_upper_constraints_file }}"
|
||||
# No auth required for Bifrost.
|
||||
ironic_inspector_auth_type: None
|
||||
ironic_inspector_auth: {}
|
||||
ironic_inspector_url: "http://localhost:5050"
|
||||
ironic_inspector_cloud: bifrost
|
||||
ironic_inspector_rules: "{{ kolla_bifrost_inspector_rules }}"
|
||||
# These variables may be referenced in the introspection rules.
|
||||
inspector_rule_var_ipmi_username: "{{ kolla_bifrost_inspector_ipmi_username }}"
|
||||
|
@ -704,6 +704,7 @@ class SeedServiceDeploy(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
|
||||
|
||||
self.run_kolla_ansible_seed(parsed_args, "deploy-bifrost")
|
||||
playbooks = _build_playbook_list(
|
||||
"seed-credentials",
|
||||
"seed-introspection-rules",
|
||||
"dell-switch-bmp")
|
||||
self.run_kayobe_playbooks(parsed_args, playbooks)
|
||||
@ -738,6 +739,7 @@ class SeedServiceUpgrade(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
|
||||
self.run_kayobe_playbooks(parsed_args, playbooks)
|
||||
self.run_kolla_ansible_seed(parsed_args, "upgrade-bifrost")
|
||||
playbooks = _build_playbook_list(
|
||||
"seed-credentials",
|
||||
"seed-introspection-rules",
|
||||
"dell-switch-bmp")
|
||||
self.run_kayobe_playbooks(parsed_args, playbooks)
|
||||
|
@ -881,6 +881,8 @@ class TestCase(unittest.TestCase):
|
||||
mock.call(
|
||||
mock.ANY,
|
||||
[
|
||||
utils.get_data_files_path(
|
||||
"ansible", "seed-credentials.yml"),
|
||||
utils.get_data_files_path(
|
||||
"ansible", "seed-introspection-rules.yml"),
|
||||
utils.get_data_files_path(
|
||||
@ -936,6 +938,8 @@ class TestCase(unittest.TestCase):
|
||||
mock.call(
|
||||
mock.ANY,
|
||||
[
|
||||
utils.get_data_files_path(
|
||||
"ansible", "seed-credentials.yml"),
|
||||
utils.get_data_files_path(
|
||||
"ansible",
|
||||
"seed-introspection-rules.yml"),
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds support for copying the Bifrost ``clouds.yaml`` file and optionally a
|
||||
TLS CA certificate from the Bifrost container to the seed host. This makes
|
||||
it possible to enable authentication and TLS for Bifrost services.
|
||||
upgrade:
|
||||
- |
|
||||
Enables authentication by default in Bifrost.
|
Loading…
Reference in New Issue
Block a user