- 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>
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
=====
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
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>`
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
^^^^^^^^^^^
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
^^^^^^^^^^^^^^^^^^^
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
=================================== ========= ====== ========= =====
Command Ansible Rook DeepSea SSH
=================================== ========= ====== ========= =====
- host add â\9aª ⚪ ⚪ ✔️
- host ls â\9aª ⚪ ⚪ ✔️
- host rm â\9aª ⚪ ⚪ ✔️
+ host add â\9c\94ï¸\8f ⚪ ⚪ ✔️
+ host ls â\9c\94ï¸\8f ⚪ ⚪ ✔️
+ host rm â\9c\94ï¸\8f ⚪ ⚪ ✔️
mgr update ⚪ ⚪ ⚪ ✔️
mon update ⚪ ✔️ ⚪ ✔️
- osd create ✔️ ✔️ ⚪ ✔️
+ osd create ✔️ ✔️ ⚪ ✔️
osd device {ident,fault}-{on,off} ⚪ ⚪ ⚪ ⚪
osd rm ✔️ ⚪ ⚪ ⚪
device {ident,fault}-(on,off} ⚪ ⚪ ⚪ ⚪
"""
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):
"""
@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):
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
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
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
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)
"""
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
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
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
# 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,\
# 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):
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,
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,
"""
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):
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
"""
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 "
# 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)
# 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)
# 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)
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)
self.all_completions.append(ansible_operation)
def verify_config(self):
- """ Verify configuration options for the Ansible orchestrator module
- """
- client_msg = ""
+ """Verify mandatory settings for the module and provide help to
+ configure properly the orchestrator
+ """
+
+ 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
- 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 " \
+ # 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
-
- 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 " \
+ self.log.error(self.status_message)
+ return
+
+
+ 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:
#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 ""
completion objects
"""
-# pylint: disable=bad-continuation
-
import json
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
# 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
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
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)
def test_server_not_reachable(self):
with self.assertRaises(AnsibleRunnerServiceError):
- ars_client = Client(SERVER_URL, USER, PASSWORD,
- CERTIFICATE, logger)
-
- def test_server_wrong_USER(self):
+ ars_client = Client(SERVER_URL, verify_server=False, ca_bundle="",
+ client_cert = "DUMMY_PATH", client_key = "DUMMY_PATH",
+ logger = logger)
- 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)
+ status = ars_client.is_operative()
- 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")
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",
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
"""