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
This commit is contained in:
Hitomi Koba 2025-02-20 15:07:40 +09:00
parent 29e0d5bf1f
commit 025a21d9f3
4 changed files with 203 additions and 21 deletions

View File

@ -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.

View File

@ -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),

View File

@ -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):

View File

@ -108,6 +108,8 @@ properties:
properties:
path:
type: string
ansible_version:
type: string
params:
type: object
command: