tacker/tacker/sol_refactored/infra_drivers/kubernetes/kubernetes_resource.py

621 lines
20 KiB
Python

# Copyright (C) 2022 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.
import inspect
import ipaddress
import re
import time
from kubernetes import client
from oslo_log import log as logging
from tacker.sol_refactored.common import config
from tacker.sol_refactored.common import exceptions as sol_ex
LOG = logging.getLogger(__name__)
CONF = config.CONF
CHECK_INTERVAL = 10
VNF_INSTANCE_ID_LABEL = 'tacker_vnf_instance_id'
def convert(name):
return re.sub('([A-Z])', lambda x: '_' + x.group(1).lower(), name)
class CommonResource:
# Default API Class
api_class = client.CoreV1Api
def __init__(self, k8s_api_client, k8s_res):
self.k8s_api_client = k8s_api_client
self.k8s_client = self.api_class(api_client=self.k8s_api_client)
self.kind = k8s_res['kind']
self.namespace = k8s_res.get('metadata', {}).get('namespace')
self.name = k8s_res.get('metadata', {}).get('name')
self.metadata = k8s_res.get('metadata', {})
self.body = k8s_res
self.inst_id = k8s_res.get('metadata', {}).get('labels', {}).get(
VNF_INSTANCE_ID_LABEL)
def create(self):
pass
def read(self):
pass
def delete(self, body):
pass
def is_exists(self):
try:
info = self.read()
if info is None:
# resource not exists
return False
# resource exists
# check if the operation uses helm.
# if helm is used, self.inst_id is None.
if self.inst_id is None:
# it means check is not necessary
return True
# check whether other made it by "metadata.labels"
if info.metadata.labels is None:
# labels not exists. it means made by other
return False
if info.metadata.labels.get(VNF_INSTANCE_ID_LABEL) != self.inst_id:
# made by other
return False
return True
except sol_ex.K8sResourceNotFound:
return False
def is_ready(self):
return True
class NamespacedResource(CommonResource):
def create(self):
method = getattr(self.k8s_client,
'create_namespaced' + convert(self.__class__.__name__))
try:
method(namespace=self.namespace, body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def read(self):
method = getattr(self.k8s_client,
'read_namespaced' + convert(self.__class__.__name__))
try:
return method(namespace=self.namespace, name=self.name)
except Exception as ex:
if isinstance(ex, client.ApiException) and ex.status == 404:
raise sol_ex.K8sResourceNotFound(rsc_name=self.name)
else:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def delete(self, body):
method = getattr(self.k8s_client,
'delete_namespaced' + convert(self.__class__.__name__))
try:
method(namespace=self.namespace, name=self.name, body=body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def patch(self):
method = getattr(self.k8s_client,
'patch_namespaced' + convert(self.__class__.__name__))
try:
method(namespace=self.namespace, name=self.name, body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def replace(self):
method = getattr(self.k8s_client,
'replace_namespaced' + convert(self.__class__.__name__))
try:
method(namespace=self.namespace, name=self.name, body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def scale(self, scale_replicas):
body = {'spec': {'replicas': scale_replicas}}
method = getattr(self.k8s_client,
'patch_namespaced' + convert(self.__class__.__name__) + '_scale')
try:
method(namespace=self.namespace, name=self.name, body=body)
self.body['spec']['replicas'] = scale_replicas
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def delete_pod(self, pod_name):
body = client.V1DeleteOptions(propagation_policy='Foreground')
v1 = client.CoreV1Api(api_client=self.k8s_api_client)
try:
v1.delete_namespaced_pod(namespace=self.namespace,
name=pod_name, body=body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class ClusterResource(CommonResource):
def create(self):
method = getattr(self.k8s_client,
'create' + convert(self.__class__.__name__))
try:
method(body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def read(self):
method = getattr(self.k8s_client,
'read' + convert(self.__class__.__name__))
try:
return method(name=self.name)
except Exception as ex:
if isinstance(ex, client.ApiException) and ex.status == 404:
raise sol_ex.K8sResourceNotFound(rsc_name=self.name)
else:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def delete(self, body):
method = getattr(self.k8s_client,
'delete' + convert(self.__class__.__name__))
try:
method(name=self.name, body=body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def patch(self):
method = getattr(self.k8s_client,
'patch' + convert(self.__class__.__name__))
try:
method(namespace=self.namespace, name=self.name, body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class AuthenticationResource(CommonResource):
def create(self):
method = getattr(self.k8s_client,
'create' + convert(self.__class__.__name__))
try:
method(body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class ComponentStatus(CommonResource):
pass
class ConfigMap(NamespacedResource):
pass
class Container(CommonResource):
pass
class LimitRange(NamespacedResource):
pass
class Namespace(ClusterResource):
def is_ready(self):
namespace_info = self.read()
return (namespace_info.status.phase and
namespace_info.status.phase == 'Active')
class Node(ClusterResource):
def is_ready(self):
node_info = self.read()
for condition in node_info.status.conditions:
if condition.type == 'Ready' and condition.status == 'True':
return True
return False
class PersistentVolume(ClusterResource):
def is_ready(self):
volume_info = self.read()
return (volume_info.status.phase and
volume_info.status.phase in ['Available', 'Bound'])
class PersistentVolumeClaim(NamespacedResource):
def is_ready(self):
claim_info = self.read()
return claim_info.status.phase and claim_info.status.phase == 'Bound'
class Pod(NamespacedResource):
def delete_pod(self, pod_name):
# Get Pod information before deletition
pod_info = self.read()
body = client.V1DeleteOptions(propagation_policy='Foreground')
self.delete(body=body)
timeout = CONF.v2_vnfm.kubernetes_vim_rsc_wait_timeout
max_check_count = (timeout / CHECK_INTERVAL)
check_count = 0
while (check_count < max_check_count):
if not self.is_exists():
break
check_count += 1
time.sleep(CHECK_INTERVAL)
else:
raise sol_ex.K8sOperaitionTimeout()
create_info = client.V1Pod(metadata=self.metadata, spec=pod_info.spec)
self.k8s_client.create_namespaced_pod(
namespace=self.namespace, body=create_info)
def is_ready(self):
pod_info = self.read()
return pod_info.status.phase and pod_info.status.phase == 'Running'
def is_update(self, pods_info, old_pods_names):
return self.is_ready()
class PodTemplate(NamespacedResource):
pass
class ResourceQuota(NamespacedResource):
pass
class Secret(NamespacedResource):
pass
class Service(NamespacedResource):
def is_ready(self):
def _check_is_ip(ip_addr):
try:
ipaddress.ip_address(ip_addr)
return True
except ValueError:
return False
service_info = self.read()
if (service_info.spec.cluster_ip == "None"
or _check_is_ip(service_info.spec.cluster_ip)):
try:
endpoint_info = self.k8s_client.read_namespaced_endpoints(
namespace=self.namespace, name=self.name)
if endpoint_info:
return True
except Exception as ex:
sol_title = "Read Endpoint failed"
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class ServiceAccount(NamespacedResource):
pass
class Volume(CommonResource):
pass
class ControllerRevision(NamespacedResource):
api_class = client.AppsV1Api
def delete(self, body):
body = client.V1DeleteOptions(
propagation_policy='Background')
try:
self.k8s_client.delete_namespaced_controller_revision(
namespace=self.namespace, name=self.name, body=body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class DaemonSet(NamespacedResource):
api_class = client.AppsV1Api
def is_ready(self):
daemonset_info = self.read()
return (daemonset_info.status.desired_number_scheduled and
(daemonset_info.status.desired_number_scheduled ==
daemonset_info.status.number_ready))
def is_update(self, pods_info, old_pods_names):
daemonset_info = self.read()
replicas = daemonset_info.status.desired_number_scheduled
for pod_info in pods_info:
if (pod_info.status.phase != 'Running' or
pod_info.metadata.name in old_pods_names):
return False
return len(pods_info) == replicas
class Deployment(NamespacedResource):
api_class = client.AppsV1Api
def is_ready(self):
deployment_info = self.read()
return (deployment_info.status.replicas and
(deployment_info.status.replicas ==
deployment_info.status.ready_replicas))
def is_update(self, pods_info, old_pods_names):
deployment_info = self.read()
replicas = deployment_info.spec.replicas
for pod_info in pods_info:
if (pod_info.status.phase != 'Running' or
pod_info.metadata.name in old_pods_names):
return False
return len(pods_info) == replicas
class ReplicaSet(NamespacedResource):
api_class = client.AppsV1Api
def is_ready(self):
replicaset_info = self.read()
return (replicaset_info.status.replicas and
(replicaset_info.status.replicas ==
replicaset_info.status.ready_replicas))
def is_update(self, pods_info, old_pods_names):
replicaset_info = self.read()
replicas = replicaset_info.spec.replicas
for pod_info in pods_info:
if (pod_info.status.phase != 'Running' or
pod_info.metadata.name in old_pods_names):
return False
return len(pods_info) == replicas
class StatefulSet(NamespacedResource):
api_class = client.AppsV1Api
def delete(self, body):
pvcs_for_delete = []
try:
resp_read_sfs = self.read()
sfs_spec = resp_read_sfs.spec
volume_claim_templates = sfs_spec.volume_claim_templates
v1 = client.CoreV1Api(api_client=self.k8s_api_client)
resps_pvc = v1.list_namespaced_persistent_volume_claim(
namespace=self.namespace)
pvcs = resps_pvc.items
for volume_claim_template in volume_claim_templates:
pvc_template_metadata = volume_claim_template.metadata
match_pattern = '-'.join(
[pvc_template_metadata.name, self.name, ""])
for pvc in pvcs:
pvc_metadata = pvc.metadata
pvc_name = pvc_metadata.name
match_result = re.match(
match_pattern + '[0-9]+$', pvc_name)
if match_result is not None:
pvcs_for_delete.append(pvc_name)
except Exception:
pass
try:
self.k8s_client.delete_namespaced_stateful_set(
namespace=self.namespace, name=self.name, body=body)
for delete_pvc_name in pvcs_for_delete:
try:
v1 = client.CoreV1Api(api_client=self.k8s_api_client)
v1.delete_namespaced_persistent_volume_claim(
name=delete_pvc_name, namespace=self.namespace,
body=body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
def is_ready(self):
statefulset_info = self.read()
replicas = statefulset_info.status.replicas
if replicas == statefulset_info.status.ready_replicas:
volume_claim_templates = (
statefulset_info.spec.volume_claim_templates)
if volume_claim_templates is None:
return True
for i in range(0, statefulset_info.spec.replicas):
for volume_claim_template in volume_claim_templates:
pvc_name = "-".join(
[volume_claim_template.metadata.name,
self.name, str(i)])
v1 = client.CoreV1Api(api_client=self.k8s_api_client)
persistent_volume_claim = (
v1.read_namespaced_persistent_volume_claim(
namespace=self.namespace, name=pvc_name))
if persistent_volume_claim.status.phase != 'Bound':
return False
return True
else:
return False
def is_update(self, pods_info, old_pods_names):
statefulset_info = self.read()
replicas = statefulset_info.spec.replicas
for pod_info in pods_info:
if pod_info.status.phase != 'Running':
return False
return len(pods_info) == replicas
class HorizontalPodAutoscaler(NamespacedResource):
api_class = client.AutoscalingV1Api
class Job(NamespacedResource):
api_class = client.BatchV1Api
def is_ready(self):
job_info = self.read()
return (job_info.spec.completions and
job_info.spec.completions == job_info.status.succeeded)
class APIService(ClusterResource):
api_class = client.ApiregistrationV1Api
def is_ready(self):
api_service_info = self.read()
for condition in api_service_info.status.conditions:
if condition.type == 'Available':
if condition.status != 'True':
return False
return True
class TokenReview(AuthenticationResource):
api_class = client.AuthenticationV1Api
class LocalSubjectAccessReview(AuthenticationResource):
api_class = client.AuthorizationV1Api
def create(self):
try:
self.k8s_client.create_namespaced_local_subject_access_review(
namespace=self.namespace, body=self.body)
except Exception as ex:
operation = inspect.currentframe().f_code.co_name
sol_title = "%s failed" % operation
raise sol_ex.K8sOperationFailed(sol_title=sol_title,
sol_detail=str(ex))
class SelfSubjectAccessReview(AuthenticationResource):
api_class = client.AuthorizationV1Api
class SelfSubjectRulesReview(AuthenticationResource):
api_class = client.AuthorizationV1Api
class SubjectAccessReview(AuthenticationResource):
api_class = client.AuthorizationV1Api
class Lease(NamespacedResource):
api_class = client.CoordinationV1Api
class NetworkPolicy(NamespacedResource):
api_class = client.NetworkingV1Api
class ClusterRole(ClusterResource):
api_class = client.RbacAuthorizationV1Api
class ClusterRoleBinding(ClusterResource):
api_class = client.RbacAuthorizationV1Api
class Role(NamespacedResource):
api_class = client.RbacAuthorizationV1Api
class RoleBinding(NamespacedResource):
api_class = client.RbacAuthorizationV1Api
class PriorityClass(ClusterResource):
api_class = client.SchedulingV1Api
class StorageClass(ClusterResource):
api_class = client.StorageV1Api
class VolumeAttachment(ClusterResource):
api_class = client.StorageV1Api
def is_ready(self):
volume_info = self.read()
return volume_info.status.attached