Add a Sample Ansible Driver

Added a sample Ansible Driver as an option for users
who want to use ansible for configuration of VNFs.

In this Sample Ansible Driver, the following key LCMs
are supported:
    - instantiate_end
    - scale_start
    - scale_end
    - heal_end
    - terminate_start

Implements: blueprint add-ansible-mgmt-driver-sample
Spec: https://review.opendev.org/c/openstack/tacker-specs/+/814689
Change-Id: I539f1ab5442196865155f12fe0b2b4106feedeae
This commit is contained in:
Aldinson Esto 2022-02-25 08:40:15 +00:00
parent b89ad00b58
commit 611b8b1043
19 changed files with 2135 additions and 0 deletions

View File

@ -0,0 +1,13 @@
---
features:
- |
Add a Sample Ansible Driver as an option for users who want to use ansible
for configuration of VNFs. This Ansible Driver supports the key LCMs such as
instantiate_end, scale_start, scale_end, heal_end and terminate_start.
A Sample VNF package which contains sample usage of Ansible Driver is
provided. User manual is also provided to explain the steps in preparing the
environment to use Ansible Driver.
issues:
- |
Regarding Sample Ansible Driver, currently, deployment flavors share only
one config.yaml due to a limitation in Management Driver.

View File

View File

@ -0,0 +1,144 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_log import log as logging
from oslo_utils import encodeutils
from tacker.common import log
from tacker.vnfm.mgmt_drivers import constants as mgmt_constants
from tacker.vnfm.mgmt_drivers import vnflcm_abstract_driver
from tacker.vnfm.mgmt_drivers.ansible import ansible_driver
LOG = logging.getLogger(__name__)
def get_class():
'''Returns the class name of the action driver'''
return 'DeviceMgmtAnsible'
class DeviceMgmtAnsible(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver):
def get_type(self):
return "ansible_driver"
def get_name(self):
return "ansible_driver"
def get_description(self):
return "Tacker VNFMgmt Ansible Driver"
def instantiate_start(self, context, vnf_instance,
instantiate_vnf_request, grant,
grant_request, **kwargs):
pass
@log.log
def instantiate_end(self, context, vnf_instance,
instantiate_vnf_request, grant,
grant_request, **kwargs):
try:
LOG.debug("Start of Ansible Driver")
driver = ansible_driver.AnsibleDriver()
driver._driver_process_flow(context, vnf_instance,
mgmt_constants.ACTION_INSTANTIATE_VNF,
instantiate_vnf_request, **kwargs)
except Exception as e:
raise Exception("Ansible Driver Error: %s",
encodeutils.exception_to_unicode(e))
@log.log
def terminate_start(self, context, vnf_instance,
terminate_vnf_request, grant,
grant_request, **kwargs):
try:
LOG.debug("Start of Ansible Driver")
driver = ansible_driver.AnsibleDriver()
driver._driver_process_flow(context, vnf_instance,
mgmt_constants.ACTION_TERMINATE_VNF,
terminate_vnf_request, **kwargs)
except Exception as e:
raise Exception("Ansible Driver Error: %s",
encodeutils.exception_to_unicode(e))
def terminate_end(self, context, vnf_instance,
terminate_vnf_request, grant,
grant_request, **kwargs):
pass
@log.log
def scale_start(self, context, vnf_instance,
scale_vnf_request, grant,
grant_request, **kwargs):
try:
LOG.debug("Start of Ansible Driver")
driver = ansible_driver.AnsibleDriver()
driver._driver_process_flow(context, vnf_instance,
mgmt_constants.ACTION_SCALE_IN_VNF,
scale_vnf_request, **kwargs)
except Exception as e:
raise Exception("Ansible Driver Error: %s",
encodeutils.exception_to_unicode(e))
@log.log
def scale_end(self, context, vnf_instance,
scale_vnf_request, grant,
grant_request, **kwargs):
try:
LOG.debug("Start of Ansible Driver")
driver = ansible_driver.AnsibleDriver()
driver._driver_process_flow(context, vnf_instance,
mgmt_constants.ACTION_SCALE_OUT_VNF,
scale_vnf_request, **kwargs)
except Exception as e:
raise Exception("Ansible Driver Error: %s",
encodeutils.exception_to_unicode(e))
def heal_start(self, context, vnf_instance,
heal_vnf_request, grant,
grant_request, **kwargs):
pass
@log.log
def heal_end(self, context, vnf_instance,
heal_vnf_request, grant,
grant_request, **kwargs):
try:
LOG.debug("Start of Ansible Driver")
driver = ansible_driver.AnsibleDriver()
driver._driver_process_flow(context, vnf_instance,
mgmt_constants.ACTION_HEAL_VNF,
heal_vnf_request, **kwargs)
except Exception as e:
raise Exception("Ansible Driver Error: %s",
encodeutils.exception_to_unicode(e))
def change_external_connectivity_start(
self, context, vnf_instance,
change_ext_conn_request, grant,
grant_request, **kwargs):
pass
def change_external_connectivity_end(
self, context, vnf_instance,
change_ext_conn_request, grant,
grant_request, **kwargs):
pass
def modify_information_start(
self, context, vnf_instance,
modify_vnf_request, **kwargs):
pass
def modify_information_end(
self, context, vnf_instance,
modify_vnf_request, **kwargs):
pass

View File

@ -0,0 +1,468 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ast
import datetime
import re
import six
import yaml
from heatclient import client as hclient
from keystoneauth1 import loading
from keystoneauth1 import session
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.ansible_config_parser_cfg\
import CONFIG_PARSER_MAP
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
RE_PATTERN = r"(_VAR_[a-zA-Z_.0-9-<>]+)"
STND_MSG = "Ansible Config Parser: {}"
class ConfigParser():
def __init__(self):
self._vim_id = ""
self._auth_url = ""
self._username = ""
self._password = ""
self._project_name = ""
self._project_domain_name = ""
self._user_domain_name = ""
self._sess = ""
self._handle_map = {}
self._vnf = {}
self._config_yaml = {}
self._current_target_ip = ""
self._current_target_vdu = ""
self._failed_vdu_name = ""
self._context = {}
def configure(self, context, vnf, plugin, config_yaml, stack_map):
LOG.info(STND_MSG.format("Configuring parser"))
try:
# load context
self._context = context
# load configurable_properties
self._config_yaml = config_yaml
# get vim info
access_info = plugin.get_vim(context, vnf)
# get vim details
vim_auth = access_info["vim_auth"]
self._auth_url = vim_auth["auth_url"]
self._username = vim_auth["username"]
self._password = vim_auth["password"]
self._project_name = vim_auth["project_name"]
self._project_domain_name = vim_auth["project_domain_name"]
self._user_domain_name = vim_auth["user_domain_name"]
# changed password type bytes -> string
if isinstance(self._password, bytes):
self._password = self._password.decode('utf-8')
# append failed_vdu_name,failed_vdu_instance_ip to vnf_dict
failed_vdu_name = vnf.get('failed_vdu_name', "")
# make sure vnf['mgmt_ip_address'] is of type string
vnf_mgmt_ip_address = vnf['mgmt_ip_address']
# make sure vnf['mgmt_ip_address'] is of type string
if isinstance(vnf_mgmt_ip_address, bytes):
vnf['mgmt_ip_address'] = vnf_mgmt_ip_address.decode('utf-8')
# if vnf has scaling policy,
# get failed vdu instance ip from heat stack_map
# else the vnf has no scaling policy,
# directly get mgmt_ip address from vnf_dict
if stack_map:
vnf['failed_vdu_instance_ip'] = stack_map.get(failed_vdu_name,
[""])[0] if failed_vdu_name else ""
else:
if vnf_mgmt_ip_address:
failed_vdu_mgmt_ip_list = ast.literal_eval(
vnf['mgmt_ip_address']).get(failed_vdu_name, "")
if isinstance(failed_vdu_mgmt_ip_list, list):
vnf['failed_vdu_instance_ip'] = \
(failed_vdu_mgmt_ip_list[0]
if failed_vdu_name else "")
else:
vnf['failed_vdu_instance_ip'] = \
(failed_vdu_mgmt_ip_list
if failed_vdu_name else "")
# load vnf dict
self._vnf = vnf
LOG.debug("Auth: {} {} {} {} {} {}".format(
self._auth_url, self._username, self._password,
self._project_name, self._project_domain_name,
self._user_domain_name))
# validate config file
config = CONFIG_PARSER_MAP
if not config and not type(config):
raise exceptions.InternalErrorException(
details="Configuration file is not valid.")
for section, options in config.items():
create_function = getattr(self, "_handle_{}".format(section))
self._handle_map[section] = ConfigHandle(
value_map=options, create_function=create_function)
LOG.info(STND_MSG.format("Parser configured"))
except exceptions.AnsibleDriverException:
raise
except Exception as ex:
raise exceptions.ConfigParserConfigurationError(
ex_type=type(ex), details=ex)
def substitute(self, command, **kwargs):
try:
int_flag = False
list_flag = False
dict_flag = False
if isinstance(command, int):
int_flag = True
elif isinstance(command, list):
list_flag = True
elif isinstance(command, dict):
dict_flag = True
elif not isinstance(command, six.string_types):
raise Exception(
"Value '{}' of type '{}' is not yet supported "
" for parameter substitution."
.format(command, type(command)))
txt = str(command)
target_dict = {}
# check for variables fetched @ runtime
self._current_target_ip = kwargs.get("mgmt_ip_address", "")
self._current_target_vdu = kwargs.get("vdu", "")
# get equivalent value for each _VAR_XXX
res = re.findall(RE_PATTERN, txt)
# remove duplicates and sorted the _VAR_XXX in
# descending order based on the string length
res = list(dict.fromkeys(res))
res.sort(key=len, reverse=True)
for key in res:
target_dict[key] = self._get_value(key)
# substitute text with fetch value
for key, val in target_dict.items():
txt = re.sub(key, val, txt)
if int_flag:
txt = int(txt)
if list_flag or dict_flag:
txt = eval(txt)
return txt
except Exception as ex:
raise exceptions.ConfigParserParsingError(
cmd=command, ex_type=type(ex), details=ex)
def _get_value(self, key, **kwargs):
val = ""
# Get Resource Type, and varible name
raw_var = key.rsplit("_VAR_", 1)
if len(raw_var) != 2:
raise Exception("{} is not valid".format(key))
# handle key with no attributes, default handle to 'default'
if raw_var[1].find(".") == -1:
handle_name = 'default'
var = raw_var[1]
if var == 'VDU_INSTANCE_IP':
return self._get_vdu_instance_ip()
if var == 'VDU_INSTANCE_NAME':
return self._get_vdu_instance_name()
else:
handle_name, var = raw_var[1].split(".", 1)
# get Handle based on resource type, and get value based on parameter
# Return Handle Execution Result
try:
val = self._handle_map[handle_name].get_value(var)
except KeyError:
LOG.error("Cannot get value for {}".format(var))
raise
return val
def _get_vdu_instance_ip(self):
if not self._current_target_ip:
LOG.error("Cannot get value for {}".format('VDU_INSTANCE_IP'))
raise KeyError('VDU_INSTANCE_IP')
return self._current_target_ip
def _get_vdu_instance_name(self):
if not self._current_target_vdu:
LOG.error("Cannot get value for {}".format('VDU_INSTANCE_NAME'))
raise KeyError('VDU_INSTANCE_NAME')
return self._current_target_vdu
# The following defines handle create functions, parameter
# -> value_map, return value_dict
def _handle_vnf_resource(self, value_map=None):
value_dict = {}
source = yaml.safe_load(self._vnf['attributes']['heat_template'])
if not source:
raise ValueError("Data source is not valid!")
for option, values in value_map.items():
value_dict[option] = self._generate_data(source, values)
return value_dict
def _handle_vnf(self, value_map=None):
source = self._vnf
value_dict = {}
if not source:
raise ValueError("Data source is not valid!")
# if no specific value map, load everything
if not value_map:
for option, values in source.items():
value_dict[option] = self._generate_data(source, option)
return value_dict
for roption, values in value_map.items():
# check if item is optional,'*'
if len(roption.split('*', 1)) == 2:
option = roption.split('*', 1)[1]
required = False
else:
option = roption
required = True
value_dict[option] = self._generate_data(source, values, required)
return value_dict
def _handle_resource(self, value_map=None):
# get necessary details to create resource tree
value_dict = {}
vnf_instance_id = self._vnf['instance_id']
loader = loading.get_plugin_loader('password')
auth = loader.load_from_options(
auth_url=self._auth_url,
username=self._username,
password=self._password,
project_name=self._project_name,
user_domain_name=self._user_domain_name,
project_domain_name=self._project_domain_name)
sess = session.Session(auth=auth)
heat = hclient.Client('1', session=sess)
# if no scaling group defined
if not self._vnf['attributes'].get('scaling_group_names'):
# get stack id
target_stack = next(
heat.stacks.list(filters={'id': vnf_instance_id}))
if not target_stack:
raise ValueError("Internal Error: Target Stack not found!")
# create resource tree from resource names
resources = yaml.safe_load(
self._vnf['attributes']['heat_template'])['resources']
if not resources:
raise ValueError("Internal Error: Resources not fetched!")
for resource in resources.keys():
resource_info = \
heat.resources.get(target_stack.id, resource).to_dict()
value_dict[resource] = resource_info['attributes']
return value_dict
def _handle_default(self, value_map=None):
value_dict = {}
configurable_properties = \
self._config_yaml.get("configurable_properties", {})
if not configurable_properties:
return value_dict
for option_raw, value in configurable_properties.items():
# len(re.findall(RE_PATTERN,option_raw)) > 0
option = option_raw.rsplit("_VAR_", 1)
if len(option) != 2:
msg = \
"Config file validation error. {} is not valid "\
"configurable_properties key".format(option_raw)
LOG.error(msg)
raise ValueError(msg)
value_dict[option[1]] = value
return value_dict
def _generate_data(self, source, value, required=True):
# get filters, "... where key=value"
if len(value.rsplit(" where ", 1)) == 2:
raw_attributes, raw_filter = value.rsplit(" where ", 1)
else:
raw_filter = ""
raw_attributes = value.strip()
# get qualifiers, "(key/val) in ..."
if len(raw_attributes.split(" in ", 1)) == 2:
attribute_option, attributes = raw_attributes.split(" in ", 1)
else:
# default None, return everything
attribute_option = ""
attributes = raw_attributes.strip()
builder = "{}".format('source')
for attribute in attributes.strip().split('.'):
builder = builder + "['{}']".format(attribute)
# evalute string as python expression
try:
items = eval(builder)
except KeyError:
if required:
raise KeyError("{} not found".format(builder))
else:
return ""
# create filter funciton
def filter_func(target_item):
if raw_filter and len(raw_filter.strip().split('=')) == 2:
filter_key, filter_val = raw_filter.strip().split('=')
if target_item[1][filter_key] == filter_val:
return True
else:
return False
else:
return True
# apply filters
if isinstance(items, dict):
raw_result = dict(filter(filter_func, items.items()))
elif isinstance(items, six.string_types):
# check if instance is string dict, else return as string,
# since item is a single value
try:
raw_result = dict(
filter(filter_func, ast.literal_eval(items).items()))
except Exception as ex:
LOG.warning(
'Defaulting item to \'{}\' since cannot be evaluated. "\
"Error: {}'.format(items, ex))
return items
elif isinstance(items, (datetime.datetime, type(None))):
return items
else:
raise Exception(
"Source items data struct of type {} is not supported."
.format(type(items)))
# apply qualifiers
result = []
# apply qualifier if present and return result as list
for raw_res_key, raw_res_val in raw_result.items():
if attribute_option.strip() == 'key':
result.append(raw_res_key)
elif attribute_option.strip() == 'val':
result.append(raw_res_val)
# else default, return all as dict
if not attribute_option.strip():
result = {}
for raw_res_key, raw_res_val in raw_result.items():
result[raw_res_key] = raw_res_val
return result
class ConfigHandle():
def __init__(self, value_map, create_function=None):
"""parameter
value_map: contains hash map for finding value
value_dict: contains the fetch value based on value_map
create_function:
ld the function to generate value_dict, parameter is value_map
"""
self._value_map = value_map
self._value_dict = {}
self._create_function = create_function
# Populate Value Dict
if not create_function:
raise ValueError("Config Handle: Create function is Null!")
self._value_dict = self._create_function(self._value_map)
LOG.debug("Value Dict: {}".format(self._value_dict))
def get_value(self, key):
# this function returns the string equivalent of the data requested
# handle exec <custom_function>
if key in self._value_dict.keys():
if isinstance(self._value_dict[key], str) and\
self._value_dict[key].find(" exec ") != -1:
custom_func = \
self._value_dict[key].rsplit(" exec ", 1)[1].strip()
# execute custom function
try:
res = getattr(self, "_custom_{}".format(custom_func))()
except Exception as ex:
LOG.exception(ex)
raise Exception(
"Config Handle: error encountered on "
"executing custom function {}".format(custom_func))
return res
# for normal operation
res = ""
builder = "{}".format('self._value_dict')
for attribute in key.strip().split('.'):
builder = builder + "['{}']".format(attribute)
items = eval(builder)
# if item is a type of dict, tuple, or list, seriliaze values
# into space delimeted string
# else if string, return as is
if isinstance(items, (dict, list, tuple)):
for item in items:
if not res:
res = res + "{}".format(item)
else:
res = res + ",{}".format(item)
elif isinstance(items, six.string_types):
res = items
else:
raise Exception(
"Result items of type {} data struct is not supported."
.format(type(items)))
return res

View File

@ -0,0 +1,40 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
CONFIG_PARSER_MAP = {
'vnf': {
# Data source -> vnf_dict
# '*' Denotes optional parameters
# 'Comment all entries to load everything from vnf_dict
'mgmt_ip_address': 'mgmt_ip_address',
'*failed_vdu_name': 'failed_vdu_name',
'*failed_vdu_instance_ip': 'failed_vdu_instance_ip',
'*vduname': 'vdu_name'
},
'vnf_resource': {
# Data source -> yaml.safe_load(
# vnf_dict['attributes']['heat_template'])
'RESOURCE_LIST': 'key in resources',
'VDUNAME_LIST': 'key in resources where type=OS::Nova::Server',
'CPNAME_LIST': 'key in resources where type=OS::Neutron::Port',
'ALARMNAME_LIST': 'key in resources where type=OS::Aodh::EventAlarm',
'VBNAME_LIST': 'key in resources where type=OS::Cinder::Volume',
'CBNAME_LIST': 'key in resources where '
'type=OS::Cinder::VolumeAttachment',
},
'resource': {},
# Data source -> heat.resources.get(
# target_stack.id,<RESOURCE NAME>).to_dict()
# Loads all resources
'default': {}
# Default Data source -> config_yaml.get("configurable_properties", {})
}

View File

@ -0,0 +1,290 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import yaml
from oslo_log import log as logging
from oslo_serialization import jsonutils
from tacker.vnfm.mgmt_drivers.ansible import ansible_config_parser
from tacker.vnfm.mgmt_drivers.ansible import config_validator
from tacker.vnfm.mgmt_drivers.ansible import event_handler
from tacker.vnfm.mgmt_drivers.ansible import exceptions
from tacker.vnfm.mgmt_drivers.ansible import heat_client
from tacker.vnfm.mgmt_drivers.ansible import utils
from tacker.vnfm.mgmt_drivers.ansible.config_actions.\
vm_app_config import vm_app_config
from tacker.vnflcm import utils as vnflcm_utils
from tacker.vnfm.mgmt_drivers import constants as mgmt_constants
from tacker.vnfm import plugin
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
SUPPORTED_ACTIONS = [
mgmt_constants.ACTION_INSTANTIATE_VNF,
mgmt_constants.ACTION_TERMINATE_VNF,
mgmt_constants.ACTION_HEAL_VNF,
mgmt_constants.ACTION_UPDATE_VNF,
mgmt_constants.ACTION_SCALE_IN_VNF,
mgmt_constants.ACTION_SCALE_OUT_VNF,
]
class AnsibleDriver(object):
def __init__(self):
self._config_queue = {}
self._has_error = False
self._cfg_parser = ansible_config_parser.ConfigParser()
self._cfg_validator = config_validator.AnsibleConfigValidator()
self._config_actions = {}
self._config_actions["vm_app_config"] = \
vm_app_config.VmAppConfigAction()
self._vnf = None
self._plugin = plugin.VNFMPlugin()
self._vnf_instance = None
self._context = None
LOG.debug("Ansible Driver initialized successfully!")
def get_type(self):
"""Mgmt driver for ansible"""
pass
def get_name(self):
"""Ansible Mgmt Driver"""
pass
def get_description(self):
pass
def _driver_process_flow(self, context, vnf_instance, action,
request_obj, **kwargs):
# set global, prevent passing for every function call
self._vnf = kwargs['vnf']
self._vnf_instance = vnf_instance
self._context = context
start_msg = ("Ansible Management Driver invoked for configuration of"
"VNF: {}".format(self._vnf.get("name")))
insta_info = self._vnf_instance.instantiated_vnf_info
if action == mgmt_constants.ACTION_HEAL_VNF:
for vnfc_info in insta_info.vnfc_resource_info:
is_exist = [instance_id for instance_id in
request_obj.vnfc_instance_id
if instance_id == vnfc_info.id]
if (not len(request_obj.vnfc_instance_id)
or len(is_exist)):
self._vnf['failed_vdu_name'] = vnfc_info.vdu_id
break
EVENT_HANDLER.create_event(context, self._vnf,
utils.get_event_by_action(action,
self._vnf.get("failed_vdu_name", None)),
start_msg)
# get the mgmt_url
vnf_mgmt_ip_address = self._vnf.get("mgmt_ip_address", None)
if vnf_mgmt_ip_address is not None:
mgmt_url = jsonutils.loads(vnf_mgmt_ip_address)
LOG.debug("mgmt_url %s", mgmt_url)
else:
LOG.info("Unable to retrieve mgmt_ip_address of VNF")
return
# load the configuration file
config_yaml = self._load_ansible_config(request_obj)
if not config_yaml:
return
# validate config file
self._cfg_validator.validate(config_yaml)
# filter VDUs
if (action != mgmt_constants.ACTION_SCALE_IN_VNF and
action != mgmt_constants.ACTION_SCALE_OUT_VNF):
config_yaml = self._cfg_validator.filter_vdus(context, self._vnf,
utils.get_event_by_action(action,
self._vnf.get("failed_vdu_name", None)), mgmt_url,
config_yaml)
# load stack map
stack_map = self._get_stack_map(action, **kwargs)
# configure config parser for vnf parameter passing
self._cfg_parser.configure(self._context, self._vnf, self._plugin,
config_yaml, stack_map)
self._sort_config(config_yaml)
self._process_config(stack_map, config_yaml, mgmt_url, action)
def _sort_config(self, config_yaml):
self._config_queue = {}
for vdu, vdu_dict in config_yaml.get("vdus", {}).items():
self._add_to_config_queue(vdu, vdu_dict.get("config", {}))
def _process_config(self, stack_map, config_yaml, mgmt_url, action):
for vdu_order in sorted(self._config_queue):
config_info_list = self._config_queue[vdu_order]
for config_info in config_info_list:
vdu = config_info["vdu"]
config = config_info["config"]
for key, conf_value in config.items():
if key not in self._config_actions:
continue
LOG.debug("Processing configuration: {}".format(key))
self._config_actions[key].execute(
vdu=vdu,
vnf=self._vnf,
context=self._context,
conf_value=conf_value,
mgmt_url=mgmt_url,
cfg_parser=self._cfg_parser,
stack_map=stack_map,
config_yaml=config_yaml,
action=action,
)
def _add_to_config_queue(self, vdu, config):
if "order" not in config:
raise exceptions.MandatoryKeyNotDefinedError(vdu=vdu, key="order")
try:
order = int(config["order"])
except ValueError:
raise exceptions.InvalidValueError(vdu=vdu, key="order")
config_info = {
"vdu": vdu,
"config": config
}
if order in self._config_queue:
self._config_queue[order].append(config_info)
else:
entity_list = []
entity_list.append(config_info)
self._config_queue[order] = entity_list
def _get_stack_map(self, action, **kwargs):
stack_id_map = {}
stack_id = None
stack_id_list = None
scaling_actions = [
mgmt_constants.ACTION_SCALE_IN_VNF,
mgmt_constants.ACTION_SCALE_OUT_VNF,
]
if action in scaling_actions:
stack_id = kwargs["scale_stack_id"]
else:
stack_id = self._vnf_instance.instantiated_vnf_info.instance_id
LOG.debug("stack_id: {}".format(stack_id))
if stack_id:
if not isinstance(stack_id, list):
stack_id_list = [stack_id]
else:
stack_id_list = stack_id
hc = heat_client.AnsibleHeatClient(self._context, self._plugin,
self._vnf)
for stack_ids in stack_id_list:
parent_stack_id = hc.get_parent_stack_id(stack_ids)
for resource in hc.get_resource_list(parent_stack_id):
if resource.physical_resource_id == stack_ids:
attr = hc.get_resource_attributes(
parent_stack_id, resource.resource_name)
stack_id_map = self._add_to_stack_map(stack_id_map, attr)
LOG.debug("stack_id_map: {}".format(stack_id_map))
return stack_id_map
def _add_to_stack_map(self, map, attributes):
for key, value in attributes.items():
if "mgmt_ip-" in key:
vdu_name = key.replace("mgmt_ip-", "")
if vdu_name in map:
map[vdu_name].append(value)
else:
map[vdu_name] = [value]
return map
def _get_config(self, config_data, config_params, vnf_package_path):
configurable_properties = config_data.get('configurable_properties')
# add vnf_package_path to vnf_configurable properties
if not configurable_properties:
configurable_properties = {
'_VAR_vnf_package_path': vnf_package_path + '/'
}
else:
configurable_properties.update(
{'_VAR_vnf_package_path': vnf_package_path + '/'})
for k, v in config_params.items():
var_key = '_VAR_' + k
configurable_properties.update({var_key: v})
config_data.update(
{'configurable_properties': configurable_properties})
LOG.debug('Modified config {}'.format(config_data))
return yaml.dump(config_data)
def _load_ansible_config(self, request_obj):
# load vnf package path
vnf_package_path = vnflcm_utils._get_vnf_package_path(self._context,
self._vnf_instance.vnfd_id)
script_ansible_path = os.path.join(vnf_package_path,
utils.CONFIG_FOLDER)
script_ansible_config = None
# load ScriptANSIBLE/config.yaml
if os.path.exists(script_ansible_path):
for file in os.listdir(script_ansible_path):
if file.endswith('yaml') and file.startswith('config'):
with open(
os.path.join(
script_ansible_path, file)) as file_obj:
script_ansible_config = yaml.safe_load(file_obj)
if script_ansible_config is None:
LOG.error("not defined ansible script config")
config_params = {}
if hasattr(self._vnf_instance.instantiated_vnf_info,
'additional_params'):
config_params = self._vnf_instance.instantiated_vnf_info.\
additional_params
elif hasattr(request_obj, 'additional_params'):
config_params = request_obj.additional_params
self._vnf['attributes']['config'] = self._get_config(
script_ansible_config, config_params, vnf_package_path)
return script_ansible_config

View File

@ -0,0 +1,4 @@
class AbstractConfigAction(object):
def execute(self, **kwargs):
raise NotImplementedError

View File

@ -0,0 +1,160 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
from oslo_log import log as logging
from six import iteritems
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
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
class AnsiblePlaybookExecutor(executor.Executor):
def _get_default_execute_host(self, conf_value=None):
user = self._conf_opts["user"]
password = self._conf_opts["password"]
host = self._conf_opts["host"]
host_private_key_file = self._conf_opts["private_key_file"]
return user, password, host, host_private_key_file
def _get_target_host(self, conf_value):
target_user = conf_value.get("username", "")
target_password = conf_value.get("password", "")
target_private_key_file = conf_value.get("priv_key_file", "")
target_host = {
"target_user": target_user,
"target_password": target_password,
"target_private_key_file": target_private_key_file
}
return target_host
def _get_playbook_target_hosts(self, playbook_cmd):
target_hosts = playbook_cmd.get("target_hosts", "")
host_ips = ""
if not target_hosts:
host_ips = ",{}".format(self._mgmt_ip_address)
else:
for host_name in target_hosts:
# process host
if host_name in self._mgmt_url:
# the given config value "host" is a VDU name
host_ip = self._mgmt_url.get(host_name, "")
else:
# the given config value "host" is an address
host_ip = host_name
host_ips = host_ips + ",{}".format(host_ip)
return host_ips
def _get_node_pair_ip(self, conf_value, mgmt_url):
node_pair_ip = None
if "node_pair" in conf_value:
node_pair_ip = mgmt_url.get(conf_value["node_pair"], "")
return node_pair_ip
def _get_params(self, playbook_cmd):
params = ""
obj_params = playbook_cmd.get("params", "")
if obj_params:
for key, value in iteritems(obj_params):
if isinstance(value, dict):
str_value = json.dumps(
value, separators=(',', ':')).replace('"', '\\"')
else:
str_value = "{}".format(value)
params += "{}={} ".format(key, str_value)
return params
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 {} "
"--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,
self._node_pair_ip))
ssh_args = ("ansible_ssh_extra_args='-o StrictHostKeyChecking=no "
"-o UserKnownHostsFile=/dev/null'")
target_host_param = False
ssh_creds = ""
if self._target_host:
if self._target_host["target_user"]:
ssh_creds = "ansible_ssh_user={}".format(
self._target_host["target_user"]
)
target_host_param = True
if self._target_host["target_private_key_file"]:
if not target_host_param:
ssh_creds = "ansible_ssh_private_key_file={}".format(
self._target_host["target_private_key_file"]
)
target_host_param = True
else:
ssh_creds = ssh_creds + " ansible_ssh_private_key_"
"file={}".format(
self._target_host["target_private_key_file"])
if self._target_host["target_password"]:
if not target_host_param:
ssh_creds = "ansible_ssh_pass={}".format(
self._target_host["target_password"]
)
else:
ssh_creds = ssh_creds + " ansible_ssh_pass={}".format(
self._target_host["target_password"]
)
ssh_creds = ssh_creds + "\""
mgmt_url_vars = " --extra-vars '{}'".format(
self._convert_mgmt_url_to_extra_vars(self._mgmt_url))
cmd_raw = "{} {} {} {} {}".format(
init_cmd,
ssh_args,
self._get_params(playbook_cmd),
ssh_creds,
mgmt_url_vars
)
# substitute passed VNF parameter to its corresponding value
inline_param = {
'mgmt_ip_address': self._mgmt_ip_address,
'vdu': self._vdu
}
return self._cfg_parser.substitute(cmd_raw, **inline_param)
def _is_execution_error(self, res_code):
return False if res_code == 0 else True

View File

@ -0,0 +1,45 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_log import log as logging
from tacker.vnfm.mgmt_drivers.ansible import exceptions
LOG = logging.getLogger(__name__)
class VmAppConfigWalker(object):
def __init__(self):
self._config = None
def set_config(self, config_value):
self._config = config_value.get("vdus", {})
def get_creds_from_vdu(self, vdu, vdu_name):
vdu_dict = self._config.get(vdu_name, {}).get("config", {}).get(
"vm_app_config", {})
if not vdu_dict:
exceptions.DataRetrievalError(
vdu=vdu,
details=("Unable to retrieve configuration information"
"for VDU: {}".format(vdu_name))
)
LOG.debug("vdu_dict: {}".format(vdu_dict))
creds = {
"username": vdu_dict.get("username", ""),
"password": vdu_dict.get("password", ""),
"priv_key_file": vdu_dict.get("priv_key_file", "")
}
LOG.debug("creds: {}".format(creds))
return creds

View File

@ -0,0 +1,361 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_config import cfg
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 import utils
from tacker.vnfm.mgmt_drivers.ansible.config_actions.\
vm_app_config import config_walker
import subprocess
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
OPTS = [
cfg.StrOpt("user", default="root",
help="user name to login ansible server"),
cfg.StrOpt("password", default="root123",
help="password to login ansible server"),
cfg.StrOpt("host", default="127.0.0.1",
help="host of the ansible server"),
cfg.StrOpt("private_key_file", default="",
help="private_key_file of the ansible server"),
cfg.IntOpt("retry_count", default=120, help="maximum no. of retries"),
cfg.IntOpt("retry_interval", default=30,
help="time in seconds before next retry"),
cfg.IntOpt("connection_wait_timeout", default=3600,
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.CONF.register_opts(OPTS, "ansible")
class Executor(config_walker.VmAppConfigWalker):
def __init__(self):
self._queue = {}
self._execute_host = {}
self._local_execute_host = {}
self._target_host = {}
self._conf_opts = {}
self._node_pair_ip = None
self._action_key = ""
self._skip_execute = False
self._failed_vdu_name = None
self._vdu = None
self._vnf = None
self._context = None
self._mgmt_ip_address = None
self._conf_value = None
self._mgmt_url = None
self._cfg_parser = None
self._mgmt_executor_type = None
super(Executor, self).__init__()
def execute(self, **kwargs):
self._vdu = kwargs["vdu"]
self._vnf = kwargs["vnf"]
self._context = kwargs["context"]
self._mgmt_ip_address = kwargs["mgmt_ip_address"]
self._conf_value = kwargs["conf_value"]
self._mgmt_url = kwargs["mgmt_url"]
self._cfg_parser = kwargs["cfg_parser"]
self._action_key = kwargs["action_key"]
self._skip_execute = kwargs["skip_execute"]
self._failed_vdu_name = kwargs["failed_vdu_name"]
self._mgmt_executor_type = kwargs["mgmt_executor_type"]
self.set_config(kwargs["config_yaml"])
if self._skip_execute:
LOG.debug("Skip execution for VDU: {}".format(self._vdu))
return
self._conf_opts = {
"user": cfg.CONF.ansible.user,
"password": cfg.CONF.ansible.password,
"host": cfg.CONF.ansible.host,
"private_key_file": cfg.CONF.ansible.private_key_file
}
retry_count = self._conf_value.get("retry_count",
cfg.CONF.ansible.retry_count)
retry_interval = self._conf_value.get("retry_interval",
cfg.CONF.ansible.retry_interval)
connection_wait_timeout = self._conf_value.get(
"connection_wait_timeout",
cfg.CONF.ansible.connection_wait_timeout)
command_execution_wait_timeout = \
self._conf_value.get("command_execution_wait_timeout",
cfg.CONF.ansible.command_execution_wait_timeout)
self._execute_host = self._get_execute_host(
execute_host=self._conf_value.get("execute-host", {}),
conf_value=self._conf_value)
self._target_host = self._get_target_host(self._conf_value)
self._node_pair_ip = self._get_node_pair_ip(self._conf_value,
self._mgmt_url)
# translate some params
inline_param = {
'mgmt_ip_address': self._mgmt_ip_address,
'vdu': self._vdu
}
retry_count = self._cfg_parser.substitute(retry_count, **inline_param)
retry_interval = self._cfg_parser.substitute(retry_interval,
**inline_param)
connection_wait_timeout = self._cfg_parser.substitute(
connection_wait_timeout, **inline_param)
command_execution_wait_timeout = self._cfg_parser.substitute(
command_execution_wait_timeout, **inline_param)
LOG.debug("Command execution settings - retry count: {}".format(
retry_count))
LOG.debug("Command execution settings - retry interval: {}".format(
retry_interval))
LOG.debug("Command execution settings - "
"connection_wait_timeout: {}".format(connection_wait_timeout))
LOG.debug("Command execution settings - "
"command_execution_wait_timeout: {}".format(
command_execution_wait_timeout))
playbook_cmd_list = self._conf_value.get(self._action_key, None)
if not playbook_cmd_list:
msg = "No '{}' configuration defined for VDU '{}' "
"with IP Address '{}'"
EVENT_HANDLER.create_event(
self._context,
self._vnf,
utils.get_event_by_action_key(self._action_key),
msg.format(self._action_key, self._vdu, self._mgmt_ip_address)
)
else:
LOG.debug("conf_value @ {} {}".format(self._action_key,
playbook_cmd_list))
self._sort_playbook_cmd_list(playbook_cmd_list)
LOG.debug("Sorted playbooks/commands: {}".format(
self._queue.values()))
self._execute(retry_count, retry_interval,
connection_wait_timeout, command_execution_wait_timeout)
def _execute(self, retry_count, retry_interval, connection_wait_timeout,
command_execution_wait_timeout):
for order in sorted(self._queue):
playbook_cmd_list = self._queue[order]
for playbook_cmd in playbook_cmd_list:
LOG.debug("playbook/command: {}".format(playbook_cmd))
self._pre_execution(playbook_cmd)
cmd = self._get_final_command(playbook_cmd)
LOG.debug("command for execution: {}".format(cmd))
res_code = -1
try:
res_code, host = self._execute_cmd(
cmd,
retry_count,
retry_interval,
connection_wait_timeout,
command_execution_wait_timeout
)
except exceptions.AnsibleDriverException:
raise
except Exception as ex:
raise exceptions.CommandExecutionError(vdu=self._vdu,
details=ex)
self._post_execution(cmd, res_code, host)
if self._is_execution_error(res_code):
raise exceptions.CommandExecutionError(
vdu=self._vdu,
details="Non-zero return code"
)
def _sort_playbook_cmd_list(self, playbook_cmd_list):
self._queue = {}
for playbook_cmd in playbook_cmd_list:
self._add_to_queue(playbook_cmd)
def _add_to_queue(self, playbook_cmd):
if "order" not in playbook_cmd:
raise exceptions.MandatoryKeyNotDefinedError(vdu=self._vdu,
key="order")
try:
order = int(playbook_cmd["order"])
except ValueError:
raise exceptions.InvalidValueError(vdu=self._vdu, key="order")
if order in self._queue:
self._queue[order].append(playbook_cmd)
else:
entity_list = []
entity_list.append(playbook_cmd)
self._queue[order] = entity_list
def _execute_cmd(self, cmd, retry_count, retry_interval,
connection_wait_timeout, command_execution_wait_timeout):
if self._local_execute_host:
user = self._local_execute_host["user"]
host = self._local_execute_host["host"]
password = self._local_execute_host["password"]
host_private_key_file = \
self._local_execute_host["host_private_key_file"]
else:
user = self._execute_host["user"]
host = self._execute_host["host"]
password = self._execute_host["password"]
host_private_key_file = \
self._execute_host["host_private_key_file"]
LOG.debug("Executing command: {} for VDU {}, host: {}, "
"username: {}, password: {}, private_key_file {}".format(
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)
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
def _post_execution(self, cmd, res_code, host):
msg = ("Command executed for the VDU '{}' with IP Address "
"'{}' on execute-host '{}' and result code '{}' => {}".format(
self._vdu,
self._mgmt_ip_address,
host,
res_code,
cmd))
EVENT_HANDLER.create_event(
self._context,
self._vnf,
utils.get_event_by_action_key(self._action_key),
msg,
self._is_execution_error(res_code)
)
# reset local execute-host details
self._local_execute_host = None
def _get_execute_host(self, execute_host, conf_value=None,
use_default=True):
# some inline variables for parameter translation
inline_param = {
'mgmt_ip_address': self._mgmt_ip_address,
'vdu': self._vdu
}
if not execute_host:
if use_default:
user, password, host, host_private_key_file = \
self._get_default_execute_host(conf_value)
else:
return None
else:
host_name = execute_host.get("host", "")
# process host
if host_name in self._mgmt_url:
# the given config value "host" is a VDU name
host = self._mgmt_url.get(host_name, "")
vdu_creds = self.get_creds_from_vdu(self._vdu, host_name)
user = vdu_creds.get("username", "")
password = vdu_creds.get("password", "")
host_private_key_file = vdu_creds.get("priv_key_file", "")
else:
# the given config value "host" is an address
host = host_name
user = execute_host.get("username", "")
password = execute_host.get("password", "")
host_private_key_file = execute_host.get("priv_key_file", "")
# validate config
if not host:
raise exceptions.DataRetrievalError(
vdu=self._vdu,
details="Unable to retrieve 'host' for execute-host"
)
if not user:
raise exceptions.DataRetrievalError(
vdu=self._vdu,
details="Unable to retrieve 'username' for execute-host"
)
if not password and not host_private_key_file:
raise exceptions.DataRetrievalError(
vdu=self._vdu,
details="Unable to retrieve either 'password' or "
"'priv_key_file' for execute-host"
)
user = self._cfg_parser.substitute(user, **inline_param)
password = self._cfg_parser.substitute(password, **inline_param)
host = self._cfg_parser.substitute(host, **inline_param)
host_private_key_file = self._cfg_parser.substitute(
host_private_key_file, **inline_param)
execute_host = {
"user": user,
"password": password,
"host": host,
"host_private_key_file": host_private_key_file
}
LOG.debug("execute-host->user: {}".format(user))
LOG.debug("execute-host->password: {}".format(password))
LOG.debug("execute-host->host: {}".format(host))
LOG.debug("execute-host->pkeyfile: {}".format(host_private_key_file))
return execute_host
def _pre_execution(self, playbook_cmd):
local_execute_host = playbook_cmd.get("execute-host", {})
LOG.debug('conf_value local_execute_host: {}'.format(
local_execute_host))
self._local_execute_host = self._get_execute_host(
execute_host=local_execute_host, use_default=False)
def _get_final_command(self, playbook_cmd):
raise NotImplementedError
def _is_execution_error(self, res_code):
raise NotImplementedError
def _get_target_host(self, conf_value):
raise NotImplementedError
def _get_node_pair_ip(self, conf_value, mgmt_url):
raise NotImplementedError
def _get_default_execute_host(self, conf_value=None):
raise NotImplementedError

View File

@ -0,0 +1,143 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_log import log as logging
from tacker.vnfm.mgmt_drivers import constants
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 import abstract_config
from tacker.vnfm.mgmt_drivers.ansible.config_actions.\
vm_app_config import ansible_playbook_exec
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
class VmAppConfigAction(abstract_config.AbstractConfigAction):
def __init__(self):
self._mgmt_executors = {}
self._mgmt_executors["ansible"] = \
ansible_playbook_exec.AnsiblePlaybookExecutor()
self._vdu = None
self._vnf = None
self._context = None
self._mgmt_executor_type = None
def execute(self, **kwargs):
vdu = kwargs["vdu"]
vnf = kwargs["vnf"]
context = kwargs["context"]
conf_value = kwargs["conf_value"]
mgmt_url = kwargs["mgmt_url"]
action = kwargs["action"]
failed_vdu_name = vnf.get("failed_vdu_name", "")
cfg_parser = kwargs["cfg_parser"]
stack_map = kwargs["stack_map"]
config_yaml = kwargs["config_yaml"]
self._vdu = vdu
self._vnf = vnf
self._context = context
mgmt_executor = \
self._get_mgmt_executor(conf_value.get("type", "ansible"))
mgmt_executor_type = self._mgmt_executor_type
action_key = self._get_action_key(action, failed_vdu_name)
skip_execute = self._is_skipped(action_key, failed_vdu_name, stack_map)
mgmt_ip_address = ""
mgmt_ip_address_list = self._get_mgmt_ip_address(skip_execute,
mgmt_url, stack_map)
for mgmt_ip_address in mgmt_ip_address_list:
mgmt_executor.execute(
vdu=vdu,
vnf=vnf,
context=context,
mgmt_ip_address=mgmt_ip_address,
conf_value=conf_value,
mgmt_url=mgmt_url,
cfg_parser=cfg_parser,
failed_vdu_name=failed_vdu_name,
action_key=action_key,
skip_execute=skip_execute,
config_yaml=config_yaml,
mgmt_executor_type=mgmt_executor_type
)
def _get_mgmt_executor(self, mgmt_type):
self._mgmt_executor_type = mgmt_type
mgmt_executor = self._mgmt_executors.get(mgmt_type, None)
if not mgmt_executor:
raise exceptions.InvalidKeyError(vdu=self._vdu, key=mgmt_type)
return mgmt_executor
def _get_mgmt_ip_address(self, skip_execute, mgmt_url, stack_map):
mgmt_ip_address = []
if not skip_execute:
if stack_map:
mgmt_ip_address = stack_map.get(self._vdu, "")
else:
mgmt_ip_address = mgmt_url.get(self._vdu, "")
if not mgmt_ip_address:
raise exceptions.DataRetrievalError(
vdu=self._vdu,
details="Cannot get mgmt address for the VDU: {}".format(
self._vdu)
)
if not isinstance(mgmt_ip_address, list):
# put ip inside list
mgmt_ip_address = [mgmt_ip_address]
return mgmt_ip_address
def _get_action_key(self, action, failed_vdu_name):
# if no key for a particular VNF operation is found,
# use the default 'playbooks'
action_key = None
if action == constants.ACTION_HEAL_VNF:
if failed_vdu_name:
LOG.debug("failed vdu name: %s", failed_vdu_name)
action_key = "healing"
elif action == constants.ACTION_INSTANTIATE_VNF:
action_key = "instantiation"
elif action == constants.ACTION_TERMINATE_VNF:
action_key = "termination"
elif action == constants.ACTION_SCALE_IN_VNF:
action_key = "scale-in"
elif action == constants.ACTION_SCALE_OUT_VNF:
action_key = "scale-out"
return action_key
def _is_skipped(self, action_key, failed_vdu_name, stack_map):
is_skipped = False
if action_key:
if action_key == "healing":
if self._vdu != failed_vdu_name:
is_skipped = True
LOG.debug("VDU %s did not fail.", self._vdu)
elif action_key == "scale-in" or action_key == "scale-out":
if self._vdu not in stack_map:
is_skipped = True
LOG.debug("VDU is not scaling target: {}".format(
self._vdu))
else:
is_skipped = True
LOG.debug("Action not supported.")
return is_skipped

View File

@ -0,0 +1,60 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import yaml
from jsonschema import exceptions as json_exp
from jsonschema import validate
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_validator_schema import SCHEMA
LOG = logging.getLogger(__name__)
EVENT_HANDLER = event_handler.AnsibleEventHandler()
class AnsibleConfigValidator(object):
def __init__(self):
self._schema = yaml.safe_load(SCHEMA)
def validate(self, config_yaml):
try:
validate(config_yaml, self._schema)
except json_exp.ValidationError as err:
raise exceptions.ConfigValidationError(
details=str(err).splitlines()[0])
def filter_vdus(self, context, vnf, event, mgmt_url, config_yaml):
vdu_config = config_yaml.get("vdus", {})
filtered_vdu_config = {}
if vdu_config:
for vdu in mgmt_url:
if vdu in vdu_config:
filtered_vdu_config[vdu] = vdu_config.get(vdu)
else:
msg = ("Could not find configuration entry for VDU '{}' "
"with IP Address '{}': skipping configuration")
EVENT_HANDLER.create_event(
context,
vnf,
event,
msg.format(vdu, json.dumps(mgmt_url.get(vdu, "")))
)
config_yaml["vdus"] = filtered_vdu_config
LOG.debug("filtered config yaml: {}".format(config_yaml))
return config_yaml

View File

@ -0,0 +1,135 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
SCHEMA = """
type: object
required:
- vdus
properties:
vdus:
type: object
additionalProperties: false
patternProperties:
^[!-~]+$:
type: object
additionalProperties: false
required:
- config
properties:
config:
type: object
additionalProperties: false
required:
- order
- vm_app_config
properties:
order:
type: integer
vm_app_config:
type: object
additionalProperties: false
anyOf:
- required:
- instantiation
- required:
- termination
- required:
- healing
- required:
- scale-in
- required:
- scale-out
properties:
type:
type: string
enum:
- ansible
- remote-command
node_pair:
type: string
username:
type: string
password:
type: string
priv_key_file:
type: string
retry_count:
type:
- integer
- string
retry_interval:
type:
- integer
- string
connection_wait_timeout:
type:
- integer
- string
command_execution_wait_timeout:
type:
- integer
- string
execute-host:
type: object
additionalProperties: false
required:
- host
properties:
host:
type: string
username:
type: string
password:
type: string
priv_key_file:
type: string
patternProperties:
^instantiation|termination|healing|scale-in|scale-out$:
type: array
items:
type: object
additionalProperties: false
required:
- order
oneOf:
- required:
- path
- required:
- command
properties:
path:
type: string
params:
type: object
command:
type: string
order:
type: integer
target_hosts:
type: array
items:
type: string
execute-host:
type: object
additionalProperties: false
required:
- host
properties:
host:
type: string
username:
type: string
password:
type: string
priv_key_file:
type: string
"""

View File

@ -0,0 +1,25 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_log import log as logging
from tacker.plugins.common import constants
from tacker.vnfm import utils
LOG = logging.getLogger(__name__)
class AnsibleEventHandler(object):
def create_event(self, context, vnf, event, msg, error_flag=False):
vnf["status"] = constants.ERROR if error_flag else vnf["status"]
utils.log_events(context, vnf, event, msg)

View File

@ -0,0 +1,144 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class AnsibleDriverException(Exception):
def __init__(self, vdu=None, **kwargs):
if "details" not in kwargs or not kwargs["details"]:
kwargs["details"] = "No error information available."
self.vdu = vdu
self.message = self.message % kwargs
super(AnsibleDriverException, self).__init__(self.message)
class InternalErrorException(AnsibleDriverException):
"""Internal Error.
Define the following upon using this exception:
- details: the exception message or error information
"""
message = "Management driver internal error: %(details)s"
class ConfigParserConfigurationError(AnsibleDriverException):
"""Config parser configuration error.
Define the following upon using this exception:
- ex_type: the exception type
- details: the exception message or error information
"""
message = "Parameter conversion error. "
"Error encountered in configuring parser: [%(ex_type)s, %(details)s]"
class ConfigParserParsingError(AnsibleDriverException):
"""Config parser parsing error.
Define the following upon using this exception:
- cmd: the command being parsed that resulted in error
- ex_type: the exception type
- details: the exception message or error information
"""
message = "Parameter conversion error. "
"Encountered error in parsing '%(cmd)s': [%(ex_type)s, %(details)s]"
class ConfigValidationError(AnsibleDriverException):
"""Config file validation error.
Define the following upon using this exception:
- details: the exception message or error information
"""
message = "Config file validation error: %(details)s"
class MandatoryKeyNotDefinedError(AnsibleDriverException):
"""Config file validation error. Mandatory key is not defined.
Define the following upon using this exception:
- key: the offending key
"""
message = "Config file validation error. "
"The key '%(key)s' is not defined."
class InvalidValueError(AnsibleDriverException):
"""Config file validation error. Invalid value for a key is defined.
Define the following upon using this exception:
- key: the offending key
"""
message = "Config file validation error. "
"Invalid value of '%(key)s' is defined."
class PlaybooksCommandsNotFound(AnsibleDriverException):
"""Config file validation error. Playbooks or commands not found.
Define the following upon using this exception:
- key: the offending action key
"""
message = "Config file validation error. "
"Playbooks or commands not found for action key: %(key)s"
class InvalidKeyError(AnsibleDriverException):
"""Config file validation error. Invalid key error.
Define the following upon using this exception:
- key: the offending key
"""
message = "Config file validation error. The key '%(key)s' is not valid."
class DataRetrievalError(AnsibleDriverException):
"""Data retrieval error.
Define the following upon using this exception:
- details: the exception message or error information
"""
message = "Data retrieval error: %(details)s"
class CommandExecutionError(AnsibleDriverException):
"""Command execution error.
Define the following upon using this exception:
- details: the exception message or error information
"""
message = "Command execution error: %(details)s"
class CommandExecutionTimeoutError(AnsibleDriverException):
"""Command execution timeout error.
Define the following upon using this exception:
- host: the target host for execution
- cmd: the command executed that caused the error
"""
message = "Command execution has reached timeout. "
"Target: %(host)s Command: %(cmd)s"
class CommandConnectionLimitReached(AnsibleDriverException):
"""Command connection attempt limit reached."""
message = "Connection attempt has reached its limit."
class CommandConnectionError(AnsibleDriverException):
"""Command connection error.
Define the following upon using this exception:
- details: the exception message or error information
"""
message = "Connection attempt error: %(details)s"

View File

@ -0,0 +1,56 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 oslo_log import log as logging
from tacker.common import clients
LOG = logging.getLogger(__name__)
class AnsibleHeatClient():
def __init__(self, context, plugin, vnf):
access_info = plugin.get_vim(context, vnf)
vim_auth = access_info["vim_auth"]
auth_attr = {
"username": vim_auth["username"],
"password": vim_auth["password"],
"project_name": vim_auth["project_name"],
"cert_verify": vim_auth["cert_verify"],
"user_domain_name": vim_auth["user_domain_name"],
"auth_url": vim_auth["auth_url"],
"project_id": vim_auth["project_id"],
"project_domain_name": vim_auth["project_domain_name"]
}
region_name = vnf.get('placement_attr', {}).get('region_name', None)
self._heat_client = \
clients.OpenstackClients(auth_attr, region_name).heat
def get_parent_stack_id(self, stack_id):
stack = self._heat_client.stacks.get(stack_id)
return stack.parent
def get_resource_list(self, stack_id):
resource_list = []
if stack_id:
resource_list = self._heat_client.resources.list(stack_id)
return resource_list
def get_resource(self, stack_id, resource_name):
return self._heat_client.resources.get(stack_id, resource_name)
def get_resource_attributes(self, stack_id, resource_name):
return self.get_resource(stack_id, resource_name).attributes

View File

@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 tacker.plugins.common import constants as plg_constants
from tacker.vnfm.mgmt_drivers import constants as mgmt_constants
CONFIG_FOLDER = "ScriptANSIBLE"
def get_event_by_action(action, failed_vdu_name):
event_type = None
if action == mgmt_constants.ACTION_INSTANTIATE_VNF:
event_type = plg_constants.RES_EVT_INSTANTIATE
elif action == mgmt_constants.ACTION_TERMINATE_VNF:
event_type = plg_constants.RES_EVT_TERMINATE
elif action == mgmt_constants.ACTION_HEAL_VNF:
if failed_vdu_name:
event_type = plg_constants.RES_EVT_HEAL
elif action == mgmt_constants.ACTION_SCALE_IN_VNF:
event_type = plg_constants.RES_EVT_SCALE
elif action == mgmt_constants.ACTION_SCALE_OUT_VNF:
event_type = plg_constants.RES_EVT_SCALE
return event_type
def get_event_by_action_key(action_key):
event_type = None
if action_key == "instantiation":
event_type = plg_constants.RES_EVT_INSTANTIATE
elif action_key == "termination":
event_type = plg_constants.RES_EVT_TERMINATE
elif action_key == "healing":
event_type = plg_constants.RES_EVT_HEAL
elif action_key == "scale-in":
event_type = plg_constants.RES_EVT_SCALE
elif action_key == "scale-out":
event_type = plg_constants.RES_EVT_SCALE
return event_type