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:
parent
b89ad00b58
commit
611b8b1043
@ -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.
|
0
samples/mgmt_driver/ansible/__init__.py
Normal file
0
samples/mgmt_driver/ansible/__init__.py
Normal file
144
samples/mgmt_driver/ansible/ansible.py
Normal file
144
samples/mgmt_driver/ansible/ansible.py
Normal 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
|
468
samples/mgmt_driver/ansible/ansible_config_parser.py
Normal file
468
samples/mgmt_driver/ansible/ansible_config_parser.py
Normal 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
|
40
samples/mgmt_driver/ansible/ansible_config_parser_cfg.py
Normal file
40
samples/mgmt_driver/ansible/ansible_config_parser_cfg.py
Normal 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", {})
|
||||
}
|
290
samples/mgmt_driver/ansible/ansible_driver.py
Normal file
290
samples/mgmt_driver/ansible/ansible_driver.py
Normal 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
|
@ -0,0 +1,4 @@
|
||||
|
||||
class AbstractConfigAction(object):
|
||||
def execute(self, **kwargs):
|
||||
raise NotImplementedError
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
60
samples/mgmt_driver/ansible/config_validator.py
Normal file
60
samples/mgmt_driver/ansible/config_validator.py
Normal 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
|
135
samples/mgmt_driver/ansible/config_validator_schema.py
Normal file
135
samples/mgmt_driver/ansible/config_validator_schema.py
Normal 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
|
||||
"""
|
25
samples/mgmt_driver/ansible/event_handler.py
Normal file
25
samples/mgmt_driver/ansible/event_handler.py
Normal 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)
|
144
samples/mgmt_driver/ansible/exceptions.py
Normal file
144
samples/mgmt_driver/ansible/exceptions.py
Normal 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"
|
56
samples/mgmt_driver/ansible/heat_client.py
Normal file
56
samples/mgmt_driver/ansible/heat_client.py
Normal 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
|
47
samples/mgmt_driver/ansible/utils.py
Normal file
47
samples/mgmt_driver/ansible/utils.py
Normal 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
|
Loading…
Reference in New Issue
Block a user