Merge "Enhancement of the Ansible Driver"

This commit is contained in:
Zuul
2025-03-14 02:46:16 +00:00
committed by Gerrit Code Review
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 vnflcm_noop = tacker.vnfm.mgmt_drivers.vnflcm_noop:VnflcmMgmtNoop
ansible_driver = tacker.vnfm.mgmt_drivers.ansible.ansible:DeviceMgmtAnsible ansible_driver = tacker.vnfm.mgmt_drivers.ansible.ansible:DeviceMgmtAnsible
.. _set_tacker_conf:
2. Set the tacker.conf 2. Set the tacker.conf
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
@@ -363,6 +365,55 @@ and separate by commas.
vnflcm_mgmt_driver = vnflcm_noop,ansible_driver 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 3. Update the tacker.egg-info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -486,13 +537,18 @@ by the set value.
params: params:
ansible_password: _VAR_password ansible_password: _VAR_password
order: 0 order: 0
ansible_version: ansible-2.11 # Optional
The example above shows that we have a key ``_VAR_password`` with a value The example above shows that we have a key ``_VAR_password`` with a value
``password``. It will replace the ``_VAR_password`` used in the ``password``. It will replace the ``_VAR_password`` used in the
``vm_app_config``. ``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 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 Ansible Playbook. This value is replaced by the actual vnf package path
during runtime. during runtime.

View File

@@ -14,8 +14,8 @@ import json
import os import os
from oslo_log import log as logging from oslo_log import log as logging
from tacker.vnfm.mgmt_drivers.ansible import event_handler 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.\ from tacker.vnfm.mgmt_drivers.ansible.config_actions.\
vm_app_config import executor vm_app_config import executor
@@ -82,22 +82,43 @@ class AnsiblePlaybookExecutor(executor.Executor):
params += "{}={} ".format(key, str_value) params += "{}={} ".format(key, str_value)
return params 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): def _convert_mgmt_url_to_extra_vars(self, mgmt_url):
return json.dumps(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): 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( "--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_target_hosts(playbook_cmd),
self._get_playbook_path(playbook_cmd), self._get_playbook_path(playbook_cmd),
self._mgmt_ip_address, self._mgmt_ip_address,
@@ -140,7 +161,8 @@ class AnsiblePlaybookExecutor(executor.Executor):
mgmt_url_vars = " --extra-vars '{}'".format( mgmt_url_vars = " --extra-vars '{}'".format(
self._convert_mgmt_url_to_extra_vars(self._mgmt_url)) self._convert_mgmt_url_to_extra_vars(self._mgmt_url))
cmd_raw = "{} {} {} {} {}".format( cmd_raw = "{} {} {} {} {} {}".format(
venv_activate_cmd,
init_cmd, init_cmd,
ssh_args, ssh_args,
self._get_params(playbook_cmd), self._get_params(playbook_cmd),

View File

@@ -9,8 +9,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from datetime import datetime
from oslo_config import cfg from oslo_config import cfg
from oslo_config import types
from oslo_log import log as logging from oslo_log import log as logging
from tacker.vnfm.mgmt_drivers.ansible import event_handler 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.\ from tacker.vnfm.mgmt_drivers.ansible.config_actions.\
vm_app_config import config_walker vm_app_config import config_walker
import os
import subprocess import subprocess
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -40,6 +43,57 @@ OPTS = [
help="time in seconds before ssh timeout"), help="time in seconds before ssh timeout"),
cfg.IntOpt("command_execution_wait_timeout", default=3600, cfg.IntOpt("command_execution_wait_timeout", default=3600,
help="maximum time allocated to a command to return result"), 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") cfg.CONF.register_opts(OPTS, "ansible")
@@ -66,6 +120,8 @@ class Executor(config_walker.VmAppConfigWalker):
self._cfg_parser = None self._cfg_parser = None
self._mgmt_executor_type = None self._mgmt_executor_type = None
self._venv_paths = {}
super(Executor, self).__init__() super(Executor, self).__init__()
def execute(self, **kwargs): 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._node_pair_ip = self._get_node_pair_ip(self._conf_value,
self._mgmt_url) 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 # translate some params
inline_param = { inline_param = {
'mgmt_ip_address': self._mgmt_ip_address, 'mgmt_ip_address': self._mgmt_ip_address,
@@ -155,6 +215,34 @@ class Executor(config_walker.VmAppConfigWalker):
self._execute(retry_count, retry_interval, self._execute(retry_count, retry_interval,
connection_wait_timeout, command_execution_wait_timeout) 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, def _execute(self, retry_count, retry_interval, connection_wait_timeout,
command_execution_wait_timeout): command_execution_wait_timeout):
for order in sorted(self._queue): for order in sorted(self._queue):
@@ -168,10 +256,20 @@ class Executor(config_walker.VmAppConfigWalker):
cmd = self._get_final_command(playbook_cmd) cmd = self._get_final_command(playbook_cmd)
LOG.debug("command for execution: {}".format(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 res_code = -1
try: try:
res_code, host = self._execute_cmd( res_code, host, std_error = self._execute_cmd(
cmd, cmd,
playbook_dir,
playbook_env,
retry_count, retry_count,
retry_interval, retry_interval,
connection_wait_timeout, connection_wait_timeout,
@@ -213,7 +311,8 @@ class Executor(config_walker.VmAppConfigWalker):
entity_list.append(playbook_cmd) entity_list.append(playbook_cmd)
self._queue[order] = entity_list 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): connection_wait_timeout, command_execution_wait_timeout):
if self._local_execute_host: if self._local_execute_host:
@@ -235,16 +334,19 @@ class Executor(config_walker.VmAppConfigWalker):
cmd, self._vdu, host, user, password, host_private_key_file)) cmd, self._vdu, host, user, password, host_private_key_file))
# create command executor # create command executor
result = subprocess.Popen(cmd, stdout=subprocess.PIPE, result = subprocess.Popen(cmd, cwd=playbook_dir, env=playbook_env,
stderr=subprocess.PIPE, shell=True, universal_newlines=True) stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, universal_newlines=True)
std_out, std_err = result.communicate() std_out, std_err = result.communicate()
LOG.debug("command execution result code: {}".format( LOG.debug("command execution result code: {}".format(
result.returncode)) 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): def _post_execution(self, cmd, res_code, host):

View File

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