From 025a21d9f367e2c92836d828d06b09b4d3687a48 Mon Sep 17 00:00:00 2001 From: Hitomi Koba Date: Thu, 20 Feb 2025 15:07:40 +0900 Subject: [PATCH] Enhancement of the Ansible Driver Enhancement of the Ansible Driver To improve flexibility, we will add: 1. Ansible version selection in the VNF-Package 2. Environment variable config for ansible-playbook in tacker.conf Feature 1 allows tenants to specify the Ansible version for their validated playbooks. This prevents issues caused by using a single Ansible version in Tacker. Feature 2 enables administrators to configure ansible-playbook options (e.g., log storage locations, callback plugins) via tacker.conf, removing the need for source code modifications. Implements: blueprint enhance-ansible-driver-2024oct Change-Id: Iafd64e6647d8b1868244fb52ef1ae900c066ad61 --- ..._driver_for_ansible_driver_usage_guide.rst | 58 ++++++++- .../vm_app_config/ansible_playbook_exec.py | 48 ++++++-- .../config_actions/vm_app_config/executor.py | 116 ++++++++++++++++-- .../ansible/config_validator_schema.py | 2 + 4 files changed, 203 insertions(+), 21 deletions(-) diff --git a/doc/source/user/mgmt_driver_for_ansible_driver_usage_guide.rst b/doc/source/user/mgmt_driver_for_ansible_driver_usage_guide.rst index ba140dcd6..e8238ce17 100644 --- a/doc/source/user/mgmt_driver_for_ansible_driver_usage_guide.rst +++ b/doc/source/user/mgmt_driver_for_ansible_driver_usage_guide.rst @@ -347,6 +347,8 @@ of the tacker. vnflcm_noop = tacker.vnfm.mgmt_drivers.vnflcm_noop:VnflcmMgmtNoop ansible_driver = tacker.vnfm.mgmt_drivers.ansible.ansible:DeviceMgmtAnsible +.. _set_tacker_conf: + 2. Set the tacker.conf ^^^^^^^^^^^^^^^^^^^^^^ @@ -363,6 +365,55 @@ and separate by commas. vnflcm_mgmt_driver = vnflcm_noop,ansible_driver ... + +This is example of advanced configuration (optional). + +To allow users to select an Ansible version other than the default, +define mapping of virtual environment paths and identifier in ``venv_path``. + +To enforce specific Ansible execution options, +define the environment variables and values in ``env_vars``. +Here, you can use markers in the values to replace them with specific values. + +.. code-block:: console + + $ vi /etc/tacker/tacker.conf + ... + [ansible] + venv_path = ansible-2.9:/opt/my-envs/2.9 + venv_path = ansible-2.10:/opt/my-envs/2.10 + venv_path = ansible-2.11:/opt/my-envs/2.11 + env_vars = ANSIBLE_CALLBACK_PLUGINS:/opt/callback_plugins + env_vars = ANSIBLE_STDOUT_CALLBACK:custom_callback_plugin_for_tacker + env_vars = ANSIBLE_LOG_PATH:/var/log/tacker/ansible_driver/{tenant_id}_{vnflcm_id}_{lcm_name}_{vdu_name}.log + ... + +.. list-table:: Markers Available in ``env_vars`` + :widths: 20 40 40 + :header-rows: 1 + + * - Marker + - Meaning + - Example Conversion + * - ``{tenant_id}`` + - Tenant ID invoking the playbook + - ``0000000000000000000000000000000`` + * - ``{vnflcm_id}`` + - VNFLCM ID invoking the playbook + - ``00000000-0000-0000-0000-000000000000`` + * - ``{lcm_name}`` + - LCM operation name + - ``instantiate``, ``termination``, ``healing``, ``scale-in``, ``scale-out`` + * - ``{timestamp}`` + - Playbook execution time (Unix timestamp) + - ``1721128301`` + * - ``{date}`` + - Playbook execution date (yyyy-MM-dd) + - ``2024-06-22`` + * - ``{vdu_name}`` + - Virtual Deployment Unit name + - ``VDU_1`` + 3. Update the tacker.egg-info ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -486,13 +537,18 @@ by the set value. params: ansible_password: _VAR_password order: 0 + ansible_version: ansible-2.11 # Optional The example above shows that we have a key ``_VAR_password`` with a value ``password``. It will replace the ``_VAR_password`` used in the ``vm_app_config``. -.. note:: +To specify an Ansible version other than the default, set ``ansible_version``. +Note that to use this option, you must first install multiple Ansible versions +and define a mapping of virtual environment paths and identifiers +in ``tacker.conf`` (see :ref:`set_tacker_conf`). +.. note:: The ``_VAR_vnf_package_path/`` variable is mandatory for the path of the Ansible Playbook. This value is replaced by the actual vnf package path during runtime. diff --git a/samples/mgmt_driver/ansible/config_actions/vm_app_config/ansible_playbook_exec.py b/samples/mgmt_driver/ansible/config_actions/vm_app_config/ansible_playbook_exec.py index a96724960..8229460c4 100644 --- a/samples/mgmt_driver/ansible/config_actions/vm_app_config/ansible_playbook_exec.py +++ b/samples/mgmt_driver/ansible/config_actions/vm_app_config/ansible_playbook_exec.py @@ -14,8 +14,8 @@ import json import os from oslo_log import log as logging + from tacker.vnfm.mgmt_drivers.ansible import event_handler -from tacker.vnfm.mgmt_drivers.ansible import exceptions from tacker.vnfm.mgmt_drivers.ansible.config_actions.\ vm_app_config import executor @@ -82,22 +82,43 @@ class AnsiblePlaybookExecutor(executor.Executor): params += "{}={} ".format(key, str_value) return params + def _get_venv_activate_cmd(self, playbook_cmd): + ansible_version = playbook_cmd.get("ansible_version", "") + + if not ansible_version: + return "" + + if ansible_version not in self._venv_paths: + LOG.warning( + "ansible_version=%s env does not exist, " + "so run in default env.", + ansible_version, + ) + LOG.warning("venv_paths=%s", self._venv_paths) + return "" + + venv_path = self._venv_paths[ansible_version] + activate_script_path = "{}/bin/activate".format(venv_path.rstrip("/")) + + if not os.path.exists(activate_script_path): + LOG.warning( + "Environment for ansible_version=%s cannot be activated, " + "so run in default env. " + "Please check if the environment is properly set up.", + ansible_version, + ) + return "" + + venv_activate_cmd = ". {} ;".format(activate_script_path) + return venv_activate_cmd + def _convert_mgmt_url_to_extra_vars(self, mgmt_url): return json.dumps(mgmt_url) - def _get_playbook_path(self, playbook_cmd): - path = playbook_cmd.get("path", "") - if not path: - raise exceptions.ConfigValidationError( - vdu=self._vdu, - details="Playbook {} did not specify path".format(playbook_cmd) - ) - return path - def _get_final_command(self, playbook_cmd): - init_cmd = ("cd {} ; ansible-playbook -i {} -vvv {} " + venv_activate_cmd = (self._get_venv_activate_cmd(playbook_cmd)) + init_cmd = ("ansible-playbook -i {} {} " "--extra-vars \"host={} node_pair_ip={}".format( - os.path.dirname(self._get_playbook_path(playbook_cmd)), self._get_playbook_target_hosts(playbook_cmd), self._get_playbook_path(playbook_cmd), self._mgmt_ip_address, @@ -140,7 +161,8 @@ class AnsiblePlaybookExecutor(executor.Executor): mgmt_url_vars = " --extra-vars '{}'".format( self._convert_mgmt_url_to_extra_vars(self._mgmt_url)) - cmd_raw = "{} {} {} {} {}".format( + cmd_raw = "{} {} {} {} {} {}".format( + venv_activate_cmd, init_cmd, ssh_args, self._get_params(playbook_cmd), diff --git a/samples/mgmt_driver/ansible/config_actions/vm_app_config/executor.py b/samples/mgmt_driver/ansible/config_actions/vm_app_config/executor.py index e08574508..635f1b778 100644 --- a/samples/mgmt_driver/ansible/config_actions/vm_app_config/executor.py +++ b/samples/mgmt_driver/ansible/config_actions/vm_app_config/executor.py @@ -9,8 +9,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime from oslo_config import cfg +from oslo_config import types from oslo_log import log as logging from tacker.vnfm.mgmt_drivers.ansible import event_handler @@ -20,6 +22,7 @@ from tacker.vnfm.mgmt_drivers.ansible import utils from tacker.vnfm.mgmt_drivers.ansible.config_actions.\ vm_app_config import config_walker +import os import subprocess LOG = logging.getLogger(__name__) @@ -40,6 +43,57 @@ OPTS = [ help="time in seconds before ssh timeout"), cfg.IntOpt("command_execution_wait_timeout", default=3600, help="maximum time allocated to a command to return result"), + cfg.BoolOpt("write_output_to_log", default=True, + help="logs playbook stdout and stderr at info-level if True"), + cfg.MultiOpt("venv_path", item_type=types.Dict(), default="", + help=''' +Mapping ansible-version-id and python venv path + +Example: +venv_path=ansible-2.9:/opt/my-envs/2.9 +venv_path=ansible-2.10:/opt/my-envs/2.10 +venv_path=ansible-2.11:/opt/my-envs/2.11 + +'''), + cfg.MultiOpt("env_vars", item_type=types.Dict(), + default={'ANSIBLE_DISPLAY_FAILED_STDERR': 'true'}, + help=''' +Environment variables and their values during playbook execution + +Setting `ANSIBLE_DISPLAY_FAILED_STDERR:true` sends playbook error details to +standard error output using the default callback plugin. +This enables checking playbook errors from VNF_LCM_OP_OCC's Error field. + +These markers are assigned values like the following during execution: + +{tenant_id}: + Tenant ID invoking the playbook. + e.g., "0000000000000000000000000000000" + +{vnflcm_id}: + VNFLCM ID invoking the playbook. + e.g., "00000000-0000-0000-0000-000000000000" + +{lcm_name}: + LCM operation name. + e.g., "instantiate", "termination", "healing", "scale-in", "scale-out" + +{timestamp}: + Playbook execution time in Unix timestamp. + e.g., "1721128301" + +{date}: + Playbook execution date in yyyy-MM-dd format. + e.g., "2024-06-22" + +{vdu_name}: + Virtual Deployment Unit name. + e.g., "VDU_1" + +Example: +env_vars=ANSIBLE_DISPLAY_FAILED_STDERR:true +env_vars=ANSIBLE_LOG_PATH:/path/to/{tenant_id}_{vdu_name}_{date}.log +''') ] cfg.CONF.register_opts(OPTS, "ansible") @@ -66,6 +120,8 @@ class Executor(config_walker.VmAppConfigWalker): self._cfg_parser = None self._mgmt_executor_type = None + self._venv_paths = {} + super(Executor, self).__init__() def execute(self, **kwargs): @@ -110,6 +166,10 @@ class Executor(config_walker.VmAppConfigWalker): self._node_pair_ip = self._get_node_pair_ip(self._conf_value, self._mgmt_url) + for venv_path in cfg.CONF.ansible.venv_path: + for key, value in venv_path.items(): + self._venv_paths[key] = value + # translate some params inline_param = { 'mgmt_ip_address': self._mgmt_ip_address, @@ -155,6 +215,34 @@ class Executor(config_walker.VmAppConfigWalker): self._execute(retry_count, retry_interval, connection_wait_timeout, command_execution_wait_timeout) + def _get_playbook_path(self, playbook_cmd): + path = playbook_cmd.get("path", "") + if not path: + raise exceptions.ConfigValidationError( + vdu=self._vdu, + details="Playbook {} did not specify path".format(playbook_cmd) + ) + return self._cfg_parser.substitute(path) + + def _get_playbook_env(self): + exec_time = datetime.now() + + markers = {} + markers["tenant_id"] = self._vnf["tenant_id"] + markers["vnflcm_id"] = self._vnf["id"] + markers["vdu_name"] = self._vdu + markers["lcm_name"] = self._action_key + markers["timestamp"] = str(int(exec_time.timestamp())) + markers["date"] = exec_time.strftime('%Y-%m-%d') + + playbook_env = {} + for env_var in cfg.CONF.ansible.env_vars: + for key, value in env_var.items(): + # replace the marker and store value + playbook_env[key] = value.format(**markers) + + return playbook_env + def _execute(self, retry_count, retry_interval, connection_wait_timeout, command_execution_wait_timeout): for order in sorted(self._queue): @@ -168,10 +256,20 @@ class Executor(config_walker.VmAppConfigWalker): cmd = self._get_final_command(playbook_cmd) LOG.debug("command for execution: {}".format(cmd)) + playbook_dir = os.path.dirname( + self._get_playbook_path(playbook_cmd) + ) + LOG.debug("execution dir: {}".format(playbook_dir)) + + playbook_env = self._get_playbook_env() + LOG.debug("execution env: {}".format(playbook_env)) + res_code = -1 try: - res_code, host = self._execute_cmd( + res_code, host, std_error = self._execute_cmd( cmd, + playbook_dir, + playbook_env, retry_count, retry_interval, connection_wait_timeout, @@ -213,7 +311,8 @@ class Executor(config_walker.VmAppConfigWalker): entity_list.append(playbook_cmd) self._queue[order] = entity_list - def _execute_cmd(self, cmd, retry_count, retry_interval, + def _execute_cmd(self, cmd, playbook_dir, playbook_env, + retry_count, retry_interval, connection_wait_timeout, command_execution_wait_timeout): if self._local_execute_host: @@ -235,16 +334,19 @@ class Executor(config_walker.VmAppConfigWalker): cmd, self._vdu, host, user, password, host_private_key_file)) # create command executor - result = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, shell=True, universal_newlines=True) + result = subprocess.Popen(cmd, cwd=playbook_dir, env=playbook_env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True, universal_newlines=True) std_out, std_err = result.communicate() LOG.debug("command execution result code: {}".format( result.returncode)) - LOG.debug("command execution result code: {}".format(std_out)) - LOG.debug("command execution result code: {}".format(std_err)) - return result.returncode, host + if cfg.CONF.ansible.write_output_to_log: + LOG.info("command execution result stdout: {}".format(std_out)) + LOG.info("command execution result stderr: {}".format(std_err)) + + return result.returncode, host, std_err def _post_execution(self, cmd, res_code, host): diff --git a/samples/mgmt_driver/ansible/config_validator_schema.py b/samples/mgmt_driver/ansible/config_validator_schema.py index 9e1bc213a..b8c7dd03a 100644 --- a/samples/mgmt_driver/ansible/config_validator_schema.py +++ b/samples/mgmt_driver/ansible/config_validator_schema.py @@ -108,6 +108,8 @@ properties: properties: path: type: string + ansible_version: + type: string params: type: object command: