# 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 collections import os import sys import time from oslo_config import cfg from oslo_log import log as logging from kolla_kubernetes.pathfinder import PathFinder from kolla_kubernetes.utils import ExecUtils from kolla_kubernetes.utils import JinjaUtils from kolla_kubernetes.utils import StringUtils from kolla_kubernetes.utils import YamlUtils CONF = cfg.CONF LOG = logging.getLogger() class KollaKubernetesResources(object): _singleton = None _jinja_dict_cache = {} @staticmethod def Get(): if KollaKubernetesResources._singleton is None: KollaKubernetesResources._singleton = KollaKubernetesResources( PathFinder.find_config_file('service_resources.yml')) return KollaKubernetesResources._singleton @staticmethod def GetJinjaDict(service_name=None, cli_args={}, debug_regex=None): # check the cache first cache_key = ((service_name if service_name is not None else "None") + str(cli_args) + (debug_regex if debug_regex is not None else "None")) if cache_key in KollaKubernetesResources._jinja_dict_cache: return KollaKubernetesResources._jinja_dict_cache[cache_key] # Apply basic variables that aren't defined in any config file jvars = {'node_config_directory': '', 'timestamp': str(time.time())} # Add the cli args to the template vars, to be fed into templates jvars["kolla_kubernetes"] = { "cli": {"args": YamlUtils.yaml_dict_normalize(cli_args)}} # Create the prioritized list of config files that need to be # merged. Search method for config files: locks onto the first # path where the file exists. Search method for template files: # locks onto the first path that exists, and then expects the file # to be there. kolla_dir = PathFinder.find_kolla_dir() files = [ PathFinder.find_config_file('kolla-kubernetes.yml'), PathFinder.find_config_file('globals.yml'), PathFinder.find_config_file('passwords.yml'), os.path.join(kolla_dir, 'ansible/group_vars/all.yml')] if service_name is not None: service_ansible_file = os.path.join( kolla_dir, 'ansible/roles', service_name, 'defaults/main.yml') if os.path.exists(service_ansible_file): files.append(service_ansible_file) files.append(os.path.join(kolla_dir, 'ansible/roles/common/defaults/main.yml')) # Create the config dict x = JinjaUtils.merge_configs_to_dict( reversed(files), jvars, debug_regex) # Render values containing nested jinja variables r = JinjaUtils.dict_self_render(x) # Update the cache KollaKubernetesResources._jinja_dict_cache[cache_key] = r return r def __init__(self, filename): if not os.path.isfile(filename): print("configuration file={} not found".format(filename)) sys.exit(1) self.filename = filename self.y = YamlUtils.yaml_dict_from_file(filename) self.services = collections.OrderedDict() for i in self.y['kolla-kubernetes']['services']: self.services[i['name']] = Service(i) def getServices(self): return self.services def getServiceByName(self, name): r = self.getServices() if name not in r: print("unable to find service={}", name) sys.exit(1) return r[name] def __str__(self): s = self.__class__.__name__ for k, v in self.getServices().items(): s += "\n" + StringUtils.pad_str(" ", 2, str(v)) return s class Service(object): VALID_ACTIONS = 'create delete'.split(" ") VALID_RESOURCE_TYPES = 'configmap disk pv pvc svc bootstrap pod'.split(" ") # Keep old logic for LEGACY support of bootstrap, run, and kill commands # Legacy commands did not keep order. Here, we define order. # Hoping to get rid of the LEGACY commands entirely if people okay. # Otherwise, we wait until Ansible workflow engine. # SVC should really be in bootstrap command, since it is stateful # CONFIGMAP remains listed twice, since that was the old logic. LEGACY_BOOTSTRAP_RESOURCES = 'configmap disk pv pvc bootstrap'.split(" ") LEGACY_RUN_RESOURCES = 'configmap svc pod'.split(" ") def __init__(self, y): self.y = y self.pods = collections.OrderedDict() for i in self.y['pods']: self.pods[i['name']] = Pod(i) def getName(self): return self.y['name'] def getPods(self): return self.pods def getPodByName(self, name): r = self.getPods() if name not in r: print("unable to find pod={}", name) sys.exit(1) return r[name] def __str__(self): s = self.__class__.__name__ + " " + self.getName() for k, v in self.getPods().items(): s += "\n" + StringUtils.pad_str(" ", 2, str(v)) return s def do_apply(self, action, resource_types, dry_run=False): """Apply action to resource_types Example: service.apply("create", "disk") Example: service.apply("create", ["disk", "pv", "pvc"]) Example: service.apply("delete", "all") ACTION: string value of (create|delete) RESOURCE_TYPES: string value of one resource type, or list of string values of many resource types (configmap|disk|pv|pvc|svc|bootstrap|pod). In addition 'all' is a valid resource type. """ # Check action input arg for code errors assert type(action) is str assert action in Service.VALID_ACTIONS # Handle resource_types as string or list, and the special case 'all' if type(resource_types) is str: if resource_types == 'all': resource_types = Service.VALID_RESOURCE_TYPES else: resource_types = [resource_types] # Check resource_type input arg for code errors assert type(resource_types) is list for t in resource_types: assert t in Service.VALID_RESOURCE_TYPES # If action is delete, then delete the resource types in # reverse order. if action == 'delete': resource_types = reversed(resource_types) # Execute the action for each resource_type for rt in resource_types: if rt == "configmap": # Take care of configmap as a special case for pod in self.getPods().values(): for container in pod.getContainers().values(): if action == 'create': container.createConfigMaps() elif action == 'delete': container.deleteConfigMaps() else: raise Exception('Code Error') else: # Handle all other resource_types as the same self._ensureResource(action, rt) def getResourceFilesByType(self, type_): assert type_ in Service.VALID_RESOURCE_TYPES assert 'resources' in self.y # Handle where resource files not defined for type. # i.e Not all services may require 'disk' resources if type_ not in self.y['resources']: return [] if type(self.y['resources'][type_]) is not list: return [] # Fully resolve each resource file ret = [] kkdir = PathFinder.find_kolla_kubernetes_dir() for i in self.y['resources'][type_]: file_ = os.path.join(kkdir, i) assert os.path.exists(file_) ret.append(file_) return ret def _ensureResource(self, action, resource_type): resource_files = self.getResourceFilesByType(resource_type) # If action is delete, then delete the resource files in # reverse order. if action == 'delete': resource_files = reversed(resource_files) for file_ in resource_files: # Build the command based on if shell script or not. If # shell script, pipe to sh. Else, pipe to kubectl cmd = "kolla-kubernetes resource-template {} {} {} {}".format( action, resource_type, self.getName(), file_) if file_.endswith('.sh.j2'): cmd += " | sh" else: cmd += " | kubectl {} -f -".format(action) # Execute the command ExecUtils.exec_command(cmd) class Pod(object): def __init__(self, y): self.y = y self.containers = collections.OrderedDict() for i in self.y['containers']: self.containers[i['name']] = Container(i) def getName(self): return self.y['name'] def getContainers(self): return self.containers def getContainerByName(self, name): r = self.getContainers() if name not in r: print("unable to find container={}", name) sys.exit(1) return r[name] def __str__(self): s = self.__class__.__name__ + " " + self.getName() for k, v in self.getContainers().items(): s += "\n" + StringUtils.pad_str(" ", 2, str(v)) return s class Container(object): def __init__(self, y): self.y = y def getName(self): return self.y['name'] def __str__(self): s = self.__class__.__name__ s += " " + self.getName() return s def createConfigMaps(self): self._ensureConfigMaps('create') def deleteConfigMaps(self): self._ensureConfigMaps('delete') def _ensureConfigMaps(self, action): assert action in Service.VALID_ACTIONS cmd = ("kubectl {} configmap {}-configmap".format( action, self.getName())) # For the create action, add some more arguments if action == 'create': for f in PathFinder.find_config_files(self.getName()): cmd += ' --from-file={}={}'.format( os.path.basename(f).replace("_", "-"), f) # Execute the command ExecUtils.exec_command(cmd)