Merge "Enhancement of the Ansible Driver"
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user