From: Juan Miguel Olmo Martínez Date: Thu, 11 Apr 2019 08:51:50 +0000 (+0200) Subject: mgr/ansible: TLS Mutual Authentication X-Git-Tag: v15.1.0~2519^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=4c6a1c6c6852cdd49bf88e9342c399175e79bb8b;p=ceph.git 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 --- diff --git a/doc/mgr/ansible.rst b/doc/mgr/ansible.rst index e81e67baf30..c992e65a1a8 100644 --- a/doc/mgr/ansible.rst +++ b/doc/mgr/ansible.rst @@ -5,14 +5,14 @@ Ansible Orchestrator ==================== -This module is a :ref:`Ceph orchestrator ` module that uses `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 ` module that uses `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 + # ceph ansible set-ssl-certificate-key -i + + +:: + + If the client certificate provided is for an especific manager server use: + # ceph ansible set-ssl-certificate -i + # ceph ansible set-ssl-certificate-key -i + + + +After setting the client certificate and key files, finish the configuration as follows: + +:: + + # ceph config set mgr mgr/ansible/server_location : + # ceph config set mgr mgr/ansible/verify_server + # ceph config set mgr mgr/ansible/ca_bundle - # ceph config set mgr mgr/ansible/server_addr - # ceph config set mgr mgr/ansible/server_port - # ceph config set mgr mgr/ansible/username - # ceph config set mgr mgr/ansible/password - # ceph config set mgr mgr/ansible/verify_server Where: * : Is the ip address/hostname of the server where the Ansible Runner Service is available. * : The port number where the Ansible Runner Service is listening - * : The username of one authorized user in the Ansible Runner Service - * : The password of the authorized user. - * : 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``. + * : boolean, it controls whether the Ansible Runner Service server's TLS certificate is verified. Defaults to ``True``. + * : 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 ` diff --git a/doc/mgr/orchestrator_cli.rst b/doc/mgr/orchestrator_cli.rst index d93da8672d5..54c96a46e6e 100644 --- a/doc/mgr/orchestrator_cli.rst +++ b/doc/mgr/orchestrator_cli.rst @@ -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} ⚪ ⚪ ⚪ ⚪ diff --git a/src/pybind/mgr/ansible/ansible_runner_svc.py b/src/pybind/mgr/ansible/ansible_runner_svc.py index 636e20b0fdc..45ca1081265 100644 --- a/src/pybind/mgr/ansible/ansible_runner_svc.py +++ b/src/pybind/mgr/ansible/ansible_runner_svc.py @@ -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>: 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>: 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 diff --git a/src/pybind/mgr/ansible/module.py b/src/pybind/mgr/ansible/module.py index 81e1c40a104..6b87b921f34 100644 --- a/src/pybind/mgr/ansible/module.py +++ b/src/pybind/mgr/ansible/module.py @@ -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 group: %s", ex) - operations = [HttpOperation(url_group, "post")] + self.log.exception("Error checking 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 - """ - 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 " + 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 :." \ - "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 "\ + ":."\ + "Try 'ceph config set mgr mgr/{0}/server_location "\ ":'".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 " \ - "'".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 " \ - "'".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 " \ - "' 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 '"\ + "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 "" diff --git a/src/pybind/mgr/ansible/output_wizards.py b/src/pybind/mgr/ansible/output_wizards.py index d924bf04cc5..7ba8b7f7500 100644 --- a/src/pybind/mgr/ansible/output_wizards.py +++ b/src/pybind/mgr/ansible/output_wizards.py @@ -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 diff --git a/src/pybind/mgr/ansible/tests/test_client_playbooks.py b/src/pybind/mgr/ansible/tests/test_client_playbooks.py index 98dfd3dd58f..23cbbe4555b 100644 --- a/src/pybind/mgr/ansible/tests/test_client_playbooks.py +++ b/src/pybind/mgr/ansible/tests/test_client_playbooks.py @@ -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="api", - 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) - - 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="api", + 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 """