mgr/ansible: TLS Mutual Authentication

- Changes needed to allow Ansible Orchestrator to use the new authentication strategy used in Ansible Runner Service
- Changes to propagate Ansible playbook errors to the completion result

Addressed changes suggested by the team

- Certificate and key are stored now in the mon KV store
- Option server_url is now server_location
- Using manager Options to have a better mgmt of MODULE_OPTIONS
- Added verbosity to status command to show problems connecting with external orchestrator
- lint problems fixed

Addressed changes suggested by @sebastian-philipp

- Improved messages and documentation

Fix error in documentation

- Fix error in ansible documentation
- Added examples in orchestrator-cli documentation

Signed-off-by: Juan Miguel Olmo Martínez <jolmomar@redhat.com>
This commit is contained in:
Juan Miguel Olmo Martínez 2019-04-11 10:51:50 +02:00
parent ff5f4a57eb
commit 4c6a1c6c68
No known key found for this signature in database
GPG Key ID: F38428F191BEBAB1
6 changed files with 395 additions and 229 deletions

View File

@ -5,14 +5,14 @@
Ansible Orchestrator
====================
This module is a :ref:`Ceph orchestrator <orchestrator-modules>` module that uses `Ansible Runner Service <https://github.com/pcuzner/ansible-runner-service>`_ (a RESTful API server) to execute Ansible playbooks in order to satisfy the different operations supported.
This module is a :ref:`Ceph orchestrator <orchestrator-modules>` module that uses `Ansible Runner Service <https://github.com/ansible/ansible-runner-service>`_ (a RESTful API server) to execute Ansible playbooks in order to satisfy the different operations supported.
These operations basically (and for the moment) are:
- Get an inventory of the Ceph cluster nodes and all the storage devices present in each node
- Hosts management
- Create/remove OSD's
- ...
- ...
Usage
=====
@ -41,25 +41,58 @@ Enable the Ansible orchestrator module and use it with the :ref:`CLI <orchestrat
Configuration
=============
The external Ansible Runner Service uses TLS mutual authentication to allow clients to use the API.
A client certificate and a key files should be provided by the Administrator of the Ansible Runner Service for each manager node.
This files should be copied in each of the manager nodes with read access for the ceph user.
The destination folder for this files and the name of the files must be the same always in all the manager nodes,
althought the certificate/key content of this files logically will be different in each node.
Configuration must be set once the module is enabled by first time.
This can be done in one monitor node via the configuration key facility on a
cluster-wide level (so they apply to all manager instances) as follows::
cluster-wide level (so they apply to all manager instances) as follows:
In first place, configure the Ansible Runner Service client certificate and key:
::
If the provided client certificate is usable for all servers, apply it using:
# ceph ansible set-ssl-certificate -i <location_of_the_crt_file>
# ceph ansible set-ssl-certificate-key -i <location_of_the_key_file>
::
If the client certificate provided is for an especific manager server use:
# ceph ansible set-ssl-certificate <server> -i <location_of_the_crt_file>
# ceph ansible set-ssl-certificate-key <server> -i <location_of_the_key_file>
After setting the client certificate and key files, finish the configuration as follows:
::
# ceph config set mgr mgr/ansible/server_location <ip_address/server_name>:<port>
# ceph config set mgr mgr/ansible/verify_server <False|True>
# ceph config set mgr mgr/ansible/ca_bundle <path_to_ca_bundle_file>
# ceph config set mgr mgr/ansible/server_addr <ip_address/server_name>
# ceph config set mgr mgr/ansible/server_port <port>
# ceph config set mgr mgr/ansible/username <username>
# ceph config set mgr mgr/ansible/password <password>
# ceph config set mgr mgr/ansible/verify_server <verify_server_value>
Where:
* <ip_address/server_name>: Is the ip address/hostname of the server where the Ansible Runner Service is available.
* <port>: The port number where the Ansible Runner Service is listening
* <username>: The username of one authorized user in the Ansible Runner Service
* <password>: The password of the authorized user.
* <verify_server_value>: Either a boolean, in which case it controls whether the server's TLS certificate is verified, or a string, in which case it must be a path to a CA bundle to use in the verification. Defaults to ``True``.
* <verify_server_value>: boolean, it controls whether the Ansible Runner Service server's TLS certificate is verified. Defaults to ``True``.
* <path_to_ca_bundle_file>: Path to a CA bundle to use in the verification.
In order to check that everything is OK, use the "status" orchestrator command.
# ceph orchestrator status
Backend: ansible
Available: True
Any kind of problem connecting with the external Ansible Runner Service will be reported using this command.
Debugging
@ -95,27 +128,5 @@ And use the "active" manager node: ( "ceph -s" command in one monitor give you t
Operations
==========
**Inventory:**
Get the list of storage devices installed in all the cluster nodes. The output format is::
[host:
device_name (type_of_device , size_in_bytes)]
Example::
[root@mon0 ~]# ceph orchestrator device ls
192.168.121.160:
vda (hdd, 44023414784b)
sda (hdd, 53687091200b)
sdb (hdd, 53687091200b)
sdc (hdd, 53687091200b)
192.168.121.36:
vda (hdd, 44023414784b)
192.168.121.201:
vda (hdd, 44023414784b)
192.168.121.70:
vda (hdd, 44023414784b)
sda (hdd, 53687091200b)
sdb (hdd, 53687091200b)
sdc (hdd, 53687091200b)
To see the complete list of operations, use:
:ref:`CLI <orchestrator-cli-module>`

View File

@ -113,6 +113,22 @@ filtered to a particular node:
ceph orchestrator device ls [--host=...] [--refresh]
Example::
# ceph orchestrator device ls
Host 192.168.121.206:
Device Path Type Size Rotates Available Model
/dev/sdb hdd 50.0G True True ATA/QEMU HARDDISK
/dev/sda hdd 50.0G True False ATA/QEMU HARDDISK
Host 192.168.121.181:
Device Path Type Size Rotates Available Model
/dev/sdb hdd 50.0G True True ATA/QEMU HARDDISK
/dev/sda hdd 50.0G True False ATA/QEMU HARDDISK
.. note::
Output form Ansible orchestrator
Create OSDs
^^^^^^^^^^^
@ -126,6 +142,13 @@ The output of ``osd create`` is not specified and may vary between orchestrator
Where ``drive.group.json`` is a JSON file containing the fields defined in :class:`orchestrator.DriveGroupSpec`
Example::
# ceph orchestrator osd create 192.168.121.206:/dev/sdc
{"status": "OK", "msg": "", "data": {"event": "playbook_on_stats", "uuid": "7082f3ba-f5b7-4b7c-9477-e74ca918afcb", "stdout": "\r\nPLAY RECAP *********************************************************************\r\n192.168.121.206 : ok=96 changed=3 unreachable=0 failed=0 \r\n", "counter": 932, "pid": 10294, "created": "2019-05-28T22:22:58.527821", "end_line": 1170, "runner_ident": "083cad3c-8197-11e9-b07a-2016b900e38f", "start_line": 1166, "event_data": {"ignored": 0, "skipped": {"192.168.121.206": 186}, "ok": {"192.168.121.206": 96}, "artifact_data": {}, "rescued": 0, "changed": {"192.168.121.206": 3}, "pid": 10294, "dark": {}, "playbook_uuid": "409364a6-9d49-4e44-8b7b-c28e5b3adf89", "playbook": "add-osd.yml", "failures": {}, "processed": {"192.168.121.206": 1}}, "parent_uuid": "409364a6-9d49-4e44-8b7b-c28e5b3adf89"}}
.. note::
Output form Ansible orchestrator
Decommission an OSD
^^^^^^^^^^^^^^^^^^^
@ -136,6 +159,13 @@ Decommission an OSD
Removes one or more OSDs from the cluster and the host, if the OSDs are marked as
``destroyed``.
Example::
# ceph orchestrator osd rm 4
{"status": "OK", "msg": "", "data": {"event": "playbook_on_stats", "uuid": "1a16e631-906d-48e0-9e24-fa7eb593cc0a", "stdout": "\r\nPLAY RECAP *********************************************************************\r\n192.168.121.158 : ok=2 changed=0 unreachable=0 failed=0 \r\n192.168.121.181 : ok=2 changed=0 unreachable=0 failed=0 \r\n192.168.121.206 : ok=2 changed=0 unreachable=0 failed=0 \r\nlocalhost : ok=31 changed=8 unreachable=0 failed=0 \r\n", "counter": 240, "pid": 10948, "created": "2019-05-28T22:26:09.264012", "end_line": 308, "runner_ident": "8c093db0-8197-11e9-b07a-2016b900e38f", "start_line": 301, "event_data": {"ignored": 0, "skipped": {"localhost": 37}, "ok": {"192.168.121.181": 2, "192.168.121.158": 2, "192.168.121.206": 2, "localhost": 31}, "artifact_data": {}, "rescued": 0, "changed": {"localhost": 8}, "pid": 10948, "dark": {}, "playbook_uuid": "a12ec40e-bce9-4bc9-b09e-2d8f76a5be02", "playbook": "shrink-osd.yml", "failures": {}, "processed": {"192.168.121.181": 1, "192.168.121.158": 1, "192.168.121.206": 1, "localhost": 1}}, "parent_uuid": "a12ec40e-bce9-4bc9-b09e-2d8f76a5be02"}}
.. note::
Output form Ansible orchestrator
..
Blink Device Lights
@ -245,12 +275,12 @@ This is an overview of the current implementation status of the orchestrators.
=================================== ========= ====== ========= =====
Command Ansible Rook DeepSea SSH
=================================== ========= ====== ========= =====
host add ⚪ ⚪ ✔️
host ls ⚪ ⚪ ✔️
host rm ⚪ ⚪ ✔️
host add ✔️ ⚪ ⚪ ✔️
host ls ✔️ ⚪ ⚪ ✔️
host rm ✔️ ⚪ ⚪ ✔️
mgr update ⚪ ⚪ ⚪ ✔️
mon update ⚪ ✔️ ⚪ ✔️
osd create ✔️ ✔️ ⚪ ✔️
osd create ✔️ ✔️ ⚪ ✔️
osd device {ident,fault}-{on,off} ⚪ ⚪ ⚪ ⚪
osd rm ✔️ ⚪ ⚪ ⚪
device {ident,fault}-(on,off} ⚪ ⚪ ⚪ ⚪

View File

@ -1,19 +1,21 @@
"""
Client module to interact with the Ansible Runner Service
"""
import requests
import json
import re
from functools import wraps
import requests
# Ansible Runner service API endpoints
API_URL = "api"
LOGIN_URL = "api/v1/login"
PLAYBOOK_EXEC_URL = "api/v1/playbooks"
PLAYBOOK_EVENTS = "api/v1/jobs/%s/events"
EVENT_DATA_URL = "api/v1/jobs/%s/events/%s"
class AnsibleRunnerServiceError(Exception):
"""Generic Ansible Runner Service Exception"""
pass
def handle_requests_exceptions(func):
@ -21,10 +23,11 @@ def handle_requests_exceptions(func):
"""
@wraps(func)
def inner(*args, **kwargs):
"""Generic error mgmt decorator"""
try:
return func(*args, **kwargs)
except requests.exceptions.RequestException as ex:
raise AnsibleRunnerServiceError(str(ex))
except (requests.exceptions.RequestException, IOError) as ex:
raise AnsibleRunnerServiceError(str(ex))
return inner
class ExecutionStatusCode(object):
@ -73,8 +76,8 @@ class PlayBookExecution(object):
try:
response = self.rest_client.http_post(endpoint,
self.params,
self.querystr_dict)
self.params,
self.querystr_dict)
except AnsibleRunnerServiceError:
self.log.exception("Error launching playbook <%s>", self.playbook)
raise
@ -128,10 +131,11 @@ class PlayBookExecution(object):
self.log.info("Requested playbook execution status is: %s", status_value)
return status_value
def get_result(self, event_filter=""):
def get_result(self, event_filter):
"""Get the data of the events filtered by a task pattern and
a event filter
@event_filter: list of 0..N event names items
@returns: the events that matches with the patterns provided
"""
response = None
@ -148,16 +152,20 @@ class PlayBookExecution(object):
else:
events = json.loads(response.text)["data"]["events"]
# Filter by task
if self.result_task_pattern:
result_events = {event:data for event,data in events.items()
if "task" in data and
re.match(self.result_task_pattern, data["task"])}
result_events = {event:data for event, data in events.items()
if "task" in data and
re.match(self.result_task_pattern, data["task"])}
else:
result_events = events
# Filter by event
if event_filter:
result_events = {event:data for event,data in result_events.items()
if re.match(event_filter, data['event'])}
type_of_events = "|".join(event_filter)
result_events = {event:data for event, data in result_events.items()
if re.match(type_of_events, data['event'])}
self.log.info("Requested playbook result is: %s", json.dumps(result_events))
return result_events
@ -167,68 +175,41 @@ class Client(object):
and execute easily playbooks
"""
def __init__(self, server_url, user, password, verify_server, logger):
def __init__(self, server_url, verify_server, ca_bundle, client_cert,
client_key, logger):
"""Provide an https client to make easy interact with the Ansible
Runner Service"
:param servers_url: The base URL >server>:<port> of the Ansible Runner Service
:param user: Username of the authorized user
:param password: Password of the authorized user
:param verify_server: Either a boolean, in which case it controls whether we verify
the server's TLS certificate, or a string, in which case it must be a path
to a CA bundle to use. Defaults to ``True``.
:param server_url: The base URL >server>:<port> of the Ansible Runner
Service
:param verify_server: A boolean to specify if server authentity should
be checked or not. (True by default)
:param ca_bundle: If provided, an alternative Cert. Auth. bundle file
will be used as source for checking the authentity of
the Ansible Runner Service
:param client_cert: Path to Ansible Runner Service client certificate
file
:param client_key: Path to Ansible Runner Service client certificate key
file
:param logger: Log file
"""
self.server_url = server_url
self.user = user
self.password = password
self.log = logger
self.auth = (self.user, self.password)
if not verify_server:
self.verify_server = True
elif verify_server.lower().strip() == 'false':
self.verify_server = False
else:
self.verify_server = verify_server
self.client_cert = (client_cert, client_key)
# Once authenticated this token will be used in all the requests
self.token = ""
# used to provide the "verify" parameter in requests
# a boolean that sometimes contains a string :-(
self.verify_server = verify_server
if ca_bundle: # This intentionallly overwrites
self.verify_server = ca_bundle
self.server_url = "https://{0}".format(self.server_url)
# Log in the server and get a token
self.login()
@handle_requests_exceptions
def login(self):
""" Login with user credentials to obtain a valid token
"""
the_url = "%s/%s" % (self.server_url, LOGIN_URL)
response = requests.get(the_url,
auth = self.auth,
verify = self.verify_server)
if response.status_code != requests.codes.ok:
self.log.error("login error <<%s>> (%s):%s",
the_url, response.status_code, response.text)
else:
self.log.info("login succesful <<%s>> (%s):%s",
the_url, response.status_code, response.text)
if response:
self.token = json.loads(response.text)["data"]["token"]
self.log.info("Connection with Ansible Runner Service is operative")
@handle_requests_exceptions
def is_operative(self):
"""Indicates if the connection with the Ansible runner Server is ok
"""
# No Token... this means we haven't used yet the service.
if not self.token:
return False
# Check the service
response = self.http_get(API_URL)
@ -247,17 +228,19 @@ class Client(object):
"""
the_url = "%s/%s" % (self.server_url, endpoint)
response = requests.get(the_url,
verify = self.verify_server,
headers = {"Authorization": self.token})
verify=self.verify_server,
cert=self.client_cert,
headers={})
if response.status_code != requests.codes.ok:
self.log.error("http GET %s <--> (%s - %s)\n%s",
the_url, response.status_code, response.reason,
response.text)
the_url, response.status_code, response.reason,
response.text)
else:
self.log.info("http GET %s <--> (%s - %s)",
the_url, response.status_code, response.text)
the_url, response.status_code, response.text)
return response
@ -274,19 +257,19 @@ class Client(object):
the_url = "%s/%s" % (self.server_url, endpoint)
response = requests.post(the_url,
verify = self.verify_server,
headers = {"Authorization": self.token,
"Content-type": "application/json"},
json = payload,
params = params_dict)
verify=self.verify_server,
cert=self.client_cert,
headers={"Content-type": "application/json"},
json=payload,
params=params_dict)
if response.status_code != requests.codes.ok:
self.log.error("http POST %s [%s] <--> (%s - %s:%s)\n",
the_url, payload, response.status_code,
response.reason, response.text)
the_url, payload, response.status_code,
response.reason, response.text)
else:
self.log.info("http POST %s <--> (%s - %s)",
the_url, response.status_code, response.text)
the_url, response.status_code, response.text)
return response
@ -301,16 +284,17 @@ class Client(object):
the_url = "%s/%s" % (self.server_url, endpoint)
response = requests.delete(the_url,
verify = self.verify_server,
headers = {"Authorization": self.token})
verify=self.verify_server,
cert=self.client_cert,
headers={})
if response.status_code != requests.codes.ok:
self.log.error("http DELETE %s <--> (%s - %s)\n%s",
the_url, response.status_code, response.reason,
response.text)
the_url, response.status_code, response.reason,
response.text)
else:
self.log.info("http DELETE %s <--> (%s - %s)",
the_url, response.status_code, response.text)
the_url, response.status_code, response.text)
return response

View File

@ -7,9 +7,13 @@ The external Orchestrator is the Ansible runner service (RESTful https service)
# pylint: disable=abstract-method, no-member, bad-continuation
import json
import os
import errno
import tempfile
import requests
from OpenSSL import crypto, SSL
from mgr_module import MgrModule
from mgr_module import MgrModule, Option, CLIWriteCommand
import orchestrator
from .ansible_runner_svc import Client, PlayBookExecution, ExecutionStatusCode,\
@ -77,8 +81,8 @@ class AnsibleReadOperation(orchestrator.ReadCompletion):
# Logger
self.log = logger
# OutputWizard object used to process the result
self.output_wizard = None
def __str__(self):
return "Playbook {playbook_name}".format(playbook_name=self.playbook)
@property
def is_complete(self):
@ -184,7 +188,7 @@ class PlaybookOperation(AnsibleReadOperation):
self.playbook = playbook
# An aditional filter of result events based in the event
self.event_filter = ""
self.event_filter_list = [""]
# Playbook execution object
self.pb_execution = PlayBookExecution(client,
@ -237,8 +241,15 @@ class PlaybookOperation(AnsibleReadOperation):
processed_result = []
if self._is_complete:
raw_result = self.pb_execution.get_result(self.event_filter)
if self._is_errored:
processed_result = self.pb_execution.get_result(["runner_on_failed",
"runner_on_unreachable",
"runner_on_no_hosts",
"runner_on_async_failed",
"runner_item_on_failed"])
elif self._is_complete:
raw_result = self.pb_execution.get_result(self.event_filter_list)
if self.output_wizard:
processed_result = self.output_wizard.process(self.pb_execution.play_uuid,
@ -392,10 +403,12 @@ class Module(MgrModule, orchestrator.Orchestrator):
"""
MODULE_OPTIONS = [
{'name': 'server_url'},
{'name': 'username'},
{'name': 'password'},
{'name': 'verify_server'} # Check server identity (Boolean/path to CA bundle)
# url:port of the Ansible Runner Service
Option(name="server_location", type="str", default=""),
# Check server identity (True by default)
Option(name="verify_server", type="bool", default=True),
# Path to an alternative CA bundle
Option(name="ca_bundle", type="str", default="")
]
def __init__(self, *args, **kwargs):
@ -407,11 +420,38 @@ class Module(MgrModule, orchestrator.Orchestrator):
self.ar_client = None
# TLS certificate and key file names used to connect with the external
# Ansible Runner Service
self.client_cert_fname = ""
self.client_key_fname = ""
# used to provide more verbose explanation of errors in status method
self.status_message = ""
def available(self):
""" Check if Ansible Runner service is working
"""
# TODO
return (True, "Everything ready")
available = False
msg = ""
try:
if self.ar_client:
available = self.ar_client.is_operative()
if not available:
msg = "No response from Ansible Runner Service"
else:
msg = "Not possible to initialize connection with Ansible "\
"Runner service."
except AnsibleRunnerServiceError as ex:
available = False
msg = str(ex)
# Add more details to the detected problem
if self.status_message:
msg = "{}:\n{}".format(msg, self.status_message)
return (available, msg)
def wait(self, completions):
"""Given a list of Completion instances, progress any which are
@ -438,16 +478,19 @@ class Module(MgrModule, orchestrator.Orchestrator):
"""
self.log.info("Starting Ansible Orchestrator module ...")
# Verify config options (Just that settings are available)
self.verify_config()
# Ansible runner service client
try:
self.ar_client = Client(server_url=self.get_module_option('server_url', ''),
user=self.get_module_option('username', ''),
password=self.get_module_option('password', ''),
verify_server=self.get_module_option('verify_server', True),
logger=self.log)
# Verify config options and client certificates
self.verify_config()
# Ansible runner service client
self.ar_client = Client(
server_url=self.get_module_option('server_location', ''),
verify_server=self.get_module_option('verify_server', True),
ca_bundle=self.get_module_option('ca_bundle', ''),
client_cert=self.client_cert_fname,
client_key=self.client_key_fname,
logger=self.log)
except AnsibleRunnerServiceError:
self.log.exception("Ansible Runner Service not available. "
"Check external server status/TLS identity or "
@ -482,7 +525,7 @@ class Module(MgrModule, orchestrator.Orchestrator):
# Assign the process_output function
playbook_operation.output_wizard = ProcessInventory(self.ar_client,
self.log)
playbook_operation.event_filter = "runner_on_ok"
playbook_operation.event_filter_list = ["runner_on_ok"]
# Execute the playbook to obtain data
self._launch_operation(playbook_operation)
@ -514,7 +557,7 @@ class Module(MgrModule, orchestrator.Orchestrator):
# Filter to get the result
playbook_operation.output_wizard = ProcessPlaybookResult(self.ar_client,
self.log)
playbook_operation.event_filter = "playbook_on_stats"
playbook_operation.event_filter_list = ["playbook_on_stats"]
# Execute the playbook
self._launch_operation(playbook_operation)
@ -540,7 +583,8 @@ class Module(MgrModule, orchestrator.Orchestrator):
# Filter to get the result
playbook_operation.output_wizard = ProcessPlaybookResult(self.ar_client,
self.log)
playbook_operation.event_filter = "playbook_on_stats"
playbook_operation.event_filter_list = ["playbook_on_stats"]
# Execute the playbook
self._launch_operation(playbook_operation)
@ -579,13 +623,14 @@ class Module(MgrModule, orchestrator.Orchestrator):
add_url = URL_ADD_RM_HOSTS.format(host_name=host,
inventory_group=ORCHESTRATOR_GROUP)
operations = [HttpOperation(add_url, "post")]
operations = [HttpOperation(add_url, "post", "", None)]
except AnsibleRunnerServiceError as ex:
# Problems with the external orchestrator.
# Prepare the operation to return the error in a Completion object.
self.log.exception("Error checking <orchestrator> group: %s", ex)
operations = [HttpOperation(url_group, "post")]
self.log.exception("Error checking <orchestrator> group: %s",
str(ex))
operations = [HttpOperation(url_group, "post", "", None)]
return ARSChangeOperation(self.ar_client, self.log, operations)
@ -642,49 +687,106 @@ class Module(MgrModule, orchestrator.Orchestrator):
self.all_completions.append(ansible_operation)
def verify_config(self):
""" Verify configuration options for the Ansible orchestrator module
"""Verify mandatory settings for the module and provide help to
configure properly the orchestrator
"""
client_msg = ""
if not self.get_module_option('server_url', ''):
msg = "No Ansible Runner Service base URL <server_name>:<port>." \
"Try 'ceph config set mgr mgr/{0}/server_url " \
the_crt = None
the_key = None
# Retrieve TLS content to use and check them
# First try to get certiticate and key content for this manager instance
# ex: mgr/ansible/mgr0/[crt/key]
self.log.info("Tying to use configured specific certificate and key"
"files for this server")
the_crt = self.get_store("{}/{}".format(self.get_mgr_id(), "crt"))
the_key = self.get_store("{}/{}".format(self.get_mgr_id(), "key"))
if the_crt is None or the_key is None:
# If not possible... try to get generic certificates and key content
# ex: mgr/ansible/[crt/key]
self.log.warning("Specific tls files for this manager not "\
"configured, trying to use generic files")
the_crt = self.get_store("crt")
the_key = self.get_store("key")
if the_crt is None or the_key is None:
self.status_message = "No client certificate configured. Please "\
"set Ansible Runner Service client "\
"certificate and key:\n"\
"ceph ansible set-ssl-certificate-"\
"{key,certificate} -i <file>"
self.log.error(self.status_message)
return
# generate certificate temp files
self.client_cert_fname = generate_temp_file("crt", the_crt)
self.client_key_fname = generate_temp_file("key", the_key)
self.status_message = verify_tls_files(self.client_cert_fname,
self.client_key_fname)
if self.status_message:
self.log.error(self.status_message)
return
# Check module options
if not self.get_module_option("server_location", ""):
self.status_message = "No Ansible Runner Service base URL "\
"<server_name>:<port>."\
"Try 'ceph config set mgr mgr/{0}/server_location "\
"<server name/ip>:<port>'".format(self.module_name)
self.log.error(msg)
client_msg += msg
self.log.error(self.status_message)
return
if not self.get_module_option('username', ''):
msg = "No Ansible Runner Service user. " \
"Try 'ceph config set mgr mgr/{0}/username " \
"<string value>'".format(self.module_name)
self.log.error(msg)
client_msg += msg
if not self.get_module_option('password', ''):
msg = "No Ansible Runner Service User password. " \
"Try 'ceph config set mgr mgr/{0}/password " \
"<string value>'".format(self.module_name)
self.log.error(msg)
client_msg += msg
if not self.get_module_option('verify_server', ''):
msg = "TLS server identity verification is enabled by default." \
"Use 'ceph config set mgr mgr/{0}/verify_server False' " \
"to disable it. Use 'ceph config set mgr mgr/{0}/verify_server " \
"<path>' to point the CA bundle path used for " \
if self.get_module_option("verify_server", True):
self.status_message = "TLS server identity verification is enabled"\
" by default.Use 'ceph config set mgr mgr/{0}/verify_server False'"\
"to disable it.Use 'ceph config set mgr mgr/{0}/ca_bundle <path>'"\
"to point an alternative CA bundle path used for TLS server "\
"verification".format(self.module_name)
self.log.error(msg)
client_msg += msg
self.log.error(self.status_message)
return
if client_msg:
# Raise error
# TODO: Use OrchestratorValidationError
raise Exception(client_msg)
# Everything ok
self.status_message = ""
#---------------------------------------------------------------------------
# Ansible Orchestrator self-owned commands
#---------------------------------------------------------------------------
@CLIWriteCommand("ansible set-ssl-certificate",
"name=mgr_id,type=CephString,req=false")
def set_tls_certificate(self, mgr_id=None, inbuf=None):
"""Load tls certificate in mon k-v store
"""
if inbuf is None:
return -errno.EINVAL, \
'Please specify the certificate file with "-i" option', ''
if mgr_id is not None:
self.set_store("{}/crt".format(mgr_id), inbuf)
else:
self.set_store("crt", inbuf)
return 0, "SSL certificate updated", ""
@CLIWriteCommand("ansible set-ssl-certificate-key",
"name=mgr_id,type=CephString,req=false")
def set_tls_certificate_key(self, mgr_id=None, inbuf=None):
"""Load tls certificate key in mon k-v store
"""
if inbuf is None:
return -errno.EINVAL, \
'Please specify the certificate key file with "-i" option', \
''
if mgr_id is not None:
self.set_store("{}/key".format(mgr_id), inbuf)
else:
self.set_store("key", inbuf)
return 0, "SSL certificate key updated", ""
# Auxiliary functions
#==============================================================================
def dg_2_ansible(drive_group):
""" Transform a drive group especification into:
@ -727,3 +829,76 @@ def dg_2_ansible(drive_group):
#osd_spec["osd_objectstore"] = drive_group.objectstore
return host, osd_spec
def generate_temp_file(key, content):
""" Generates a temporal file with the content passed as parameter
:param key : used to build the temp file name
:param content: the content that will be dumped to file
:returns : the name of the generated file
"""
fname = ""
if content is not None:
fname = "{}/{}.tmp".format(tempfile.gettempdir(), key)
try:
if os.path.exists(fname):
os.remove(fname)
with open(fname, "w") as text_file:
text_file.write(content)
except IOError as ex:
raise AnsibleRunnerServiceError("Cannot store TLS certificate/key"
" content: {}".format(str(ex)))
return fname
def verify_tls_files(crt_file, key_file):
"""Basic checks for TLS certificate and key files
:crt_file : Name of the certificate file
:key_file : name of the certificate public key file
:returns : String with error description
"""
# Check we have files
if not crt_file or not key_file:
return "no certificate/key configured"
if not os.path.isfile(crt_file):
return "certificate {} does not exist".format(crt_file)
if not os.path.isfile(key_file):
return "Public key {} does not exist".format(key_file)
# Do some validations to the private key and certificate:
# - Check the type and format
# - Check the certificate expiration date
# - Check the consistency of the private key
# - Check that the private key and certificate match up
try:
with open(crt_file) as fcrt:
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, fcrt.read())
if x509.has_expired():
return "Certificate {} has been expired".format(crt_file)
except (ValueError, crypto.Error) as ex:
return "Invalid certificate {}: {}".format(crt_file, str(ex))
try:
with open(key_file) as fkey:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, fkey.read())
pkey.check()
except (ValueError, crypto.Error) as ex:
return "Invalid private key {}: {}".format(key_file, str(ex))
try:
context = SSL.Context(SSL.TLSv1_METHOD)
context.use_certificate_file(crt_file, crypto.FILETYPE_PEM)
context.use_privatekey_file(key_file, crypto.FILETYPE_PEM)
context.check_privatekey()
except crypto.Error as ex:
return "Private key {} and certificate {} do not match up: {}".format(
key_file, crt_file, str(ex))
# Everything OK
return ""

View File

@ -5,8 +5,6 @@ Output wizards are used to process results in different ways in
completion objects
"""
# pylint: disable=bad-continuation
import json
@ -66,7 +64,7 @@ class ProcessInventory(OutputWizard):
for event_key, dummy_data in inventory_events.items():
event_response = self.ar_client.http_get(EVENT_DATA_URL %
(operation_id, event_key))
(operation_id, event_key))
# self.pb_execution.play_uuid
@ -105,7 +103,7 @@ class ProcessPlaybookResult(OutputWizard):
# Loop over the result events and request the data
for event_key, dummy_data in inventory_events.items():
event_response = self.ar_client.http_get(EVENT_DATA_URL %
(operation_id, event_key))
(operation_id, event_key))
result += event_response.text

View File

@ -8,13 +8,11 @@ import requests_mock
from requests.exceptions import ConnectionError
from ..ansible_runner_svc import Client, PlayBookExecution, ExecutionStatusCode, \
LOGIN_URL, API_URL, PLAYBOOK_EXEC_URL, \
API_URL, PLAYBOOK_EXEC_URL, \
PLAYBOOK_EVENTS, AnsibleRunnerServiceError
SERVER_URL = "ars:5001"
USER = "admin"
PASSWORD = "admin"
CERTIFICATE = ""
# Playbook attributes
@ -32,30 +30,11 @@ formatter = logging.Formatter("%(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
def mock_login(mock_server):
the_login_url = "https://%s/%s" % (SERVER_URL,LOGIN_URL)
mock_server.register_uri("GET",
the_login_url,
json={"status": "OK",
"msg": "Token returned",
"data": {"token": "dummy_token"}},
status_code=200)
the_api_url = "https://%s/%s" % (SERVER_URL,API_URL)
mock_server.register_uri("GET",
the_api_url,
text="<!DOCTYPE html>api</html>",
status_code=200)
def mock_get_pb(mock_server, playbook_name, return_code):
mock_login(mock_server)
ars_client = Client(SERVER_URL, USER, PASSWORD,
CERTIFICATE, logger)
ars_client = Client(SERVER_URL, verify_server=False, ca_bundle="",
client_cert = "DUMMY_PATH", client_key = "DUMMY_PATH",
logger = logger)
the_pb_url = "https://%s/%s/%s" % (SERVER_URL, PLAYBOOK_EXEC_URL, playbook_name)
@ -82,34 +61,26 @@ class ARSclientTest(unittest.TestCase):
def test_server_not_reachable(self):
with self.assertRaises(AnsibleRunnerServiceError):
ars_client = Client(SERVER_URL, USER, PASSWORD,
CERTIFICATE, logger)
ars_client = Client(SERVER_URL, verify_server=False, ca_bundle="",
client_cert = "DUMMY_PATH", client_key = "DUMMY_PATH",
logger = logger)
def test_server_wrong_USER(self):
status = ars_client.is_operative()
with requests_mock.Mocker() as mock_server:
the_login_url = "https://%s/%s" % (SERVER_URL,LOGIN_URL)
mock_server.get(the_login_url,
json={"status": "NOAUTH",
"msg": "Access denied invalid login: unknown USER",
"data": {}},
status_code=401)
ars_client = Client(SERVER_URL, USER, PASSWORD,
CERTIFICATE, logger)
self.assertFalse(ars_client.is_operative(),
"Operative attribute expected to be False")
def test_server_connection_ok(self):
with requests_mock.Mocker() as mock_server:
mock_login(mock_server)
ars_client = Client(SERVER_URL, verify_server=False, ca_bundle="",
client_cert = "DUMMY_PATH", client_key = "DUMMY_PATH",
logger = logger)
ars_client = Client(SERVER_URL, USER, PASSWORD,
CERTIFICATE, logger)
the_api_url = "https://%s/%s" % (SERVER_URL,API_URL)
mock_server.register_uri("GET",
the_api_url,
text="<!DOCTYPE html>api</html>",
status_code=200)
self.assertTrue(ars_client.is_operative(),
"Operative attribute expected to be True")
@ -118,10 +89,9 @@ class ARSclientTest(unittest.TestCase):
with requests_mock.Mocker() as mock_server:
mock_login(mock_server)
ars_client = Client(SERVER_URL, USER, PASSWORD,
CERTIFICATE, logger)
ars_client = Client(SERVER_URL, verify_server=False, ca_bundle="",
client_cert = "DUMMY_PATH", client_key = "DUMMY_PATH",
logger = logger)
url = "https://%s/test" % (SERVER_URL)
mock_server.register_uri("DELETE",
@ -149,8 +119,6 @@ class PlayBookExecutionTests(unittest.TestCase):
self.assertEqual(test_pb.play_uuid, PB_UUID,
"Found Unexpected playbook uuid")
def test_playbook_execution_error(self):
"""Check playbook id is not set when the playbook is not present
"""