83a109f765
This patch adds a rollback option to create and update stacks. If "CONF.v2_vnfm.enable_rollback_stack" is True, when a resource creation fails in the create and update stack, rollback stack is executed and the create resource is deleted. This feature is implemented to successful AZ reselection at the VDU using volume. Closes-Bug: #2034886 Change-Id: Icdc70f299c65a137672935338dd6d795a3dbea73
306 lines
12 KiB
Python
306 lines
12 KiB
Python
# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation
|
|
# All Rights Reserved.
|
|
#
|
|
# 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_service import loopingcall
|
|
|
|
from tacker.sol_refactored.common import config
|
|
from tacker.sol_refactored.common import exceptions as sol_ex
|
|
from tacker.sol_refactored.common import http_client
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
CONF = config.CONF
|
|
|
|
CHECK_INTERVAL = 5
|
|
|
|
|
|
class HeatClient(object):
|
|
|
|
def __init__(self, vim_info):
|
|
base_url = None
|
|
if CONF.v2_vnfm.use_oauth2_mtls_for_heat:
|
|
auth = http_client.OAuth2MtlsAuthHandle(
|
|
endpoint=None,
|
|
token_endpoint=vim_info.interfaceInfo['tokenEndpoint'],
|
|
client_id=vim_info.accessInfo['username'],
|
|
ca_cert=CONF.v2_vnfm.heat_mtls_ca_cert_file,
|
|
client_cert=CONF.v2_vnfm.heat_mtls_client_cert_file
|
|
)
|
|
base_url = vim_info.interfaceInfo['heatEndpoint']
|
|
else:
|
|
verify = CONF.v2_vnfm.heat_verify_cert
|
|
if verify and CONF.v2_vnfm.heat_ca_cert_file:
|
|
verify = CONF.v2_vnfm.heat_ca_cert_file
|
|
auth = http_client.KeystonePasswordAuthHandle(
|
|
auth_url=vim_info.interfaceInfo['endpoint'],
|
|
username=vim_info.accessInfo['username'],
|
|
password=vim_info.accessInfo['password'],
|
|
project_name=vim_info.accessInfo['project'],
|
|
user_domain_name=vim_info.accessInfo['userDomain'],
|
|
project_domain_name=vim_info.accessInfo['projectDomain'],
|
|
verify=verify
|
|
)
|
|
|
|
self.client = http_client.HttpClient(auth,
|
|
service_type='orchestration',
|
|
base_url=base_url)
|
|
|
|
def create_stack(self, fields, wait=True):
|
|
if CONF.v2_vnfm.enable_rollback_stack:
|
|
fields['disable_rollback'] = False
|
|
path = "stacks"
|
|
resp, body = self.client.do_request(path, "POST",
|
|
expected_status=[201], body=fields)
|
|
|
|
if wait:
|
|
self.wait_stack_create(
|
|
f'{fields["stack_name"]}/{body["stack"]["id"]}')
|
|
|
|
return body['stack']['id']
|
|
|
|
def update_stack(self, stack_name, fields, wait=True):
|
|
if CONF.v2_vnfm.enable_rollback_stack:
|
|
fields['disable_rollback'] = False
|
|
path = f"stacks/{stack_name}"
|
|
# It was assumed that PATCH is used and therefore 'fields'
|
|
# contains only update parts and 'existing' is not used.
|
|
# Now full replacing 'fields' is supported and it is indicated
|
|
# by 'existing' is False. if 'existing' is specified and
|
|
# it is False, PUT is used.
|
|
method = "PATCH" if fields.pop('existing', True) else "PUT"
|
|
resp, body = self.client.do_request(path, method,
|
|
expected_status=[202], body=fields)
|
|
|
|
if wait:
|
|
self.wait_stack_update(stack_name)
|
|
|
|
def delete_stack(self, stack_name, wait=True):
|
|
path = f"stacks/{stack_name}"
|
|
resp, body = self.client.do_request(path, "DELETE",
|
|
expected_status=[204, 404])
|
|
|
|
if wait:
|
|
self.wait_stack_delete(stack_name)
|
|
|
|
def get_status(self, stack_name):
|
|
path = f"stacks/{stack_name}"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200, 404])
|
|
|
|
if resp.status_code == 404:
|
|
return None, None
|
|
|
|
return (body["stack"]["stack_status"],
|
|
body["stack"]["stack_status_reason"])
|
|
|
|
def get_stack_id(self, stack_name):
|
|
path = f"stacks/{stack_name}"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200, 404])
|
|
|
|
if resp.status_code == 404:
|
|
return None
|
|
|
|
return body["stack"]["id"]
|
|
|
|
def get_resources(self, stack_name):
|
|
# NOTE: Because it is necessary to get nested stack info, it is
|
|
# necessary to specify 'nested_depth=2'.
|
|
path = f"stacks/{stack_name}/resources?nested_depth=2"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200])
|
|
|
|
return body['resources']
|
|
|
|
def _wait_completion(self, stack_name, operation, complete_status,
|
|
progress_status, failed_status):
|
|
# NOTE: timeout is specified for each stack operation. so it is
|
|
# not forever loop.
|
|
def _check_status():
|
|
status, status_reason = self.get_status(stack_name)
|
|
if status in complete_status:
|
|
LOG.info("%s %s done.", operation, stack_name.split('/')[0])
|
|
raise loopingcall.LoopingCallDone()
|
|
elif status in failed_status:
|
|
if (status == "ROLLBACK_COMPLETE"
|
|
or status == "ROLLBACK_FAILED"):
|
|
status_reason = self.get_original_failed_reason(stack_name)
|
|
LOG.error("%s %s failed.", operation, stack_name.split('/')[0])
|
|
sol_title = "%s failed" % operation
|
|
raise sol_ex.StackOperationFailed(sol_title=sol_title,
|
|
sol_detail=status_reason)
|
|
elif status not in progress_status:
|
|
LOG.error("%s %s failed. status: %s", operation,
|
|
stack_name.split('/')[0], status)
|
|
sol_title = "%s failed" % operation
|
|
raise sol_ex.StackOperationFailed(sol_title=sol_title,
|
|
sol_detail='Unknown error')
|
|
LOG.debug("%s %s %s", operation, stack_name.split('/')[0],
|
|
progress_status)
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_check_status)
|
|
timer.start(interval=CHECK_INTERVAL).wait()
|
|
|
|
def wait_stack_create(self, stack_name):
|
|
if CONF.v2_vnfm.enable_rollback_stack:
|
|
self._wait_completion(stack_name, "Stack create",
|
|
["CREATE_COMPLETE"],
|
|
["CREATE_IN_PROGRESS", "CREATE_FAILED",
|
|
"ROLLBACK_IN_PROGRESS"],
|
|
["ROLLBACK_COMPLETE", "ROLLBACK_FAILED"])
|
|
else:
|
|
self._wait_completion(stack_name, "Stack create",
|
|
["CREATE_COMPLETE"], ["CREATE_IN_PROGRESS"],
|
|
["CREATE_FAILED"])
|
|
|
|
def wait_stack_update(self, stack_name):
|
|
if CONF.v2_vnfm.enable_rollback_stack:
|
|
self._wait_completion(stack_name, "Stack update",
|
|
["UPDATE_COMPLETE"],
|
|
["UPDATE_IN_PROGRESS", "UPDATE_FAILED",
|
|
"ROLLBACK_IN_PROGRESS"],
|
|
["ROLLBACK_COMPLETE", "ROLLBACK_FAILED"])
|
|
else:
|
|
self._wait_completion(stack_name, "Stack update",
|
|
["UPDATE_COMPLETE"], ["UPDATE_IN_PROGRESS"],
|
|
["UPDATE_FAILED"])
|
|
|
|
def wait_stack_delete(self, stack_name):
|
|
# NOTE: wait until stack is deleted in the DB since it is necessary
|
|
# for some operations (ex. heal-all).
|
|
# It is expected that it takes short time after "DELETE_COMPLETE".
|
|
# So timeout after "DELETE_COMPLETE" is not specified.
|
|
self._wait_completion(stack_name.split('/')[0], "Stack delete",
|
|
[None], ["DELETE_IN_PROGRESS", "DELETE_COMPLETE"],
|
|
["DELETE_FAILED"])
|
|
|
|
def get_resource_info(self, stack_id, resource_name):
|
|
path = f"stacks/{stack_id}/resources/{resource_name}"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200, 404])
|
|
if resp.status_code == 404:
|
|
return None
|
|
return body['resource']
|
|
|
|
def get_parameters(self, stack_name):
|
|
path = f"stacks/{stack_name}"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200])
|
|
|
|
return body["stack"]["parameters"]
|
|
|
|
def mark_unhealthy(self, stack_id, resource_name):
|
|
path = f"stacks/{stack_id}/resources/{resource_name}"
|
|
fields = {
|
|
"mark_unhealthy": True,
|
|
"resource_status_reason": "marked by tacker"
|
|
}
|
|
resp, body = self.client.do_request(path, "PATCH",
|
|
expected_status=[200], body=fields)
|
|
|
|
def get_template(self, stack_name):
|
|
path = f"stacks/{stack_name}/template"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200])
|
|
|
|
return body
|
|
|
|
def get_files(self, stack_name):
|
|
path = f"stacks/{stack_name}/files"
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200])
|
|
|
|
return body
|
|
|
|
def get_original_failed_reason(self, stack_name):
|
|
# This method gets the reason for stack failure from the stack event
|
|
# if the rollback option is enabled.
|
|
resource_name = stack_name.split('/')[0]
|
|
path = (f"stacks/{stack_name}/resources/{resource_name}/"
|
|
"events?resource_action=CREATE&resource_action=UPDATE"
|
|
"&resource_status=FAILED&sort_dir=desc&limit=1")
|
|
resp, body = self.client.do_request(path, "GET",
|
|
expected_status=[200, 404])
|
|
|
|
if resp.status_code == 404:
|
|
return None
|
|
|
|
return body["events"][0]["resource_status_reason"]
|
|
|
|
|
|
def get_reses_by_types(heat_reses, types):
|
|
return [res for res in heat_reses if res['resource_type'] in types]
|
|
|
|
|
|
def get_server_reses(heat_reses):
|
|
return get_reses_by_types(heat_reses, ['OS::Nova::Server'])
|
|
|
|
|
|
def get_network_reses(heat_reses):
|
|
return get_reses_by_types(heat_reses, ['OS::Neutron::Net'])
|
|
|
|
|
|
def get_storage_reses(heat_reses):
|
|
return get_reses_by_types(heat_reses, ['OS::Cinder::Volume'])
|
|
|
|
|
|
def get_port_reses(heat_reses):
|
|
return get_reses_by_types(heat_reses, ['OS::Neutron::Port'])
|
|
|
|
|
|
def get_stack_name(inst, stack_id=None):
|
|
"""Return different stack name for different operations
|
|
|
|
* A new "stack_id" will be generated after the instantiate and heal_all
|
|
perform the "create_stack" operation. When make instantiated_vnf_info,
|
|
the "stack_id" will be passed in as an input parameter.
|
|
:return: vnf-{inst.id}/{stack_id}
|
|
* In other lifecycle operations, "stack_id" is stored in the "metadata" of
|
|
"instantiatedVnfInfo".
|
|
:return: vnf-{inst.id}/{inst.instantiatedVnfInfo.metadata['stack_id']}
|
|
* In the instantiate and instantiate_rollback operations, the "stack_id"
|
|
is not yet known.
|
|
:return: vnf-{inst.id}
|
|
"""
|
|
stack_name = f"vnf-{inst.id}"
|
|
if stack_id:
|
|
return f"{stack_name}/{stack_id}"
|
|
# Add a check for the existence of "metadata" when instantiate again
|
|
# after terminate
|
|
elif (inst.obj_attr_is_set('instantiatedVnfInfo') and
|
|
inst.instantiatedVnfInfo.obj_attr_is_set('metadata') and
|
|
inst.instantiatedVnfInfo.metadata.get('stack_id')):
|
|
return f"{stack_name}/{inst.instantiatedVnfInfo.metadata['stack_id']}"
|
|
else:
|
|
return stack_name
|
|
|
|
|
|
def get_resource_stack_id(heat_res):
|
|
# return the form "stack_name/stack_id"
|
|
for link in heat_res.get('links', []):
|
|
if link['rel'] == 'stack':
|
|
items = link['href'].split('/')
|
|
return "{}/{}".format(items[-2], items[-1])
|
|
|
|
|
|
def get_parent_resource(heat_res, heat_reses):
|
|
parent = heat_res.get('parent_resource')
|
|
if parent:
|
|
for res in heat_reses:
|
|
if res['resource_name'] == parent:
|
|
return res
|