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:
parent
29e0d5bf1f
commit
025a21d9f3
@ -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.
|
||||
|
@ -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),
|
||||
|
@ -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):
|
||||
|
||||
|
@ -108,6 +108,8 @@ properties:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
ansible_version:
|
||||
type: string
|
||||
params:
|
||||
type: object
|
||||
command:
|
||||
|
Loading…
x
Reference in New Issue
Block a user