# Copyright 2014 Huawei Technologies Co. Ltd # # 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. __author__ = "Grace Yu (grace.yu@huawei.com)" """package installer: chef plugin.""" from copy import deepcopy import logging from netaddr import IPAddress import os import re import shutil from compass.deployment.installers.installer import PKInstaller from compass.deployment.utils import constants as const from compass.utils import setting_wrapper as compass_setting from compass.utils import util NAME = 'ChefInstaller' class ChefInstaller(PKInstaller): """chef package installer.""" ENV_TMPL_DIR = 'environments' NODE_TMPL_DIR = 'nodes' DATABAG_TMPL_DIR = 'databags' TMPL_DIR = 'chef_installer/templates' # keywords in package installer settings of adapter info DATABAGS = "databags" CHEFSERVER_URL = "chef_url" CHEFSERVER_DNS = "chef_server_dns" CHEFSERVER_IP = "chef_server_ip" KEY_DIR = "key_dir" CLIENT = "client_name" def __init__(self, config_manager): super(ChefInstaller, self).__init__() self.config_manager = config_manager adapter_name = self.config_manager.get_dist_system_name() self.tmpl_dir = ChefInstaller.get_tmpl_path(adapter_name) installer_settings = self.config_manager.get_pk_installer_settings() self.installer_url = self.get_chef_url(installer_settings) key, client = self.get_chef_credentials(installer_settings) self.chef_api = self._get_chef_api(key, client) self.all_chef_roles = self.get_all_roles() logging.debug('%s instance created', self) @classmethod def get_tmpl_path(cls, adapter_name): tmpl_path = os.path.join( os.path.join(compass_setting.PLUGINS_DIR, cls.TMPL_DIR), adapter_name ) return tmpl_path def __repr__(self): return '%s[name=%s,installer_url=%s]' % ( self.__class__.__name__, self.NAME, self.installer_url) def _get_chef_api(self, key=None, client=None): """Initializes chef API client.""" import chef chef_api = None try: if key and client: chef_api = chef.ChefAPI(self.installer_url, key, client) else: chef_api = chef.autoconfigure() except Exception as ex: err_msg = "Failed to instantiate chef API, error: %s" % ex.message raise Exception(err_msg) return chef_api def get_all_roles(self): import chef if not self.chef_api: logging.info("chefAPI is None! Cannot retrieve roles from server.") return None roles_objs = chef.Role.list(self.chef_api) return [name for name, value in roles_objs.items()] def get_env_name(self, dist_sys_name, cluster_name): """Generate environment name.""" return "-".join((dist_sys_name, cluster_name)) def get_create_databag(self, databag_name): """Get databag object from chef server. Creates the databag if it does not exist. """ import chef databag = None if databag_name not in chef.DataBag.list(api=self.chef_api): databag = chef.DataBag.create(databag_name, api=self.chef_api) else: databag = chef.DataBag(databag_name, api=self.chef_api) return databag def get_create_node(self, node_name, env_name=None): """Get chef node Gets the node if existing, otherwise create one and set its environment. :param str node_name: The name for this node. :param str env_name: The environment name for this node. """ import chef if not self.chef_api: logging.info("Cannot find ChefAPI object!") raise Exception("Cannot find ChefAPI object!") node = chef.Node(node_name, api=self.chef_api) if node not in chef.Node.list(api=self.chef_api): if env_name: node.chef_environment = env_name node.save() return node def delete_hosts(self, delete_cluster=False): hosts_id_list = self.config_manager.get_host_id_list() for host_id in hosts_id_list: self.delete_node(host_id) if delete_cluster: self.delete_environment() def delete_environment(self): adapter_name = self.config_manager.get_adapter_name() cluster_name = self.config_manager.get_clustername() env_name = self.get_env_name(adapter_name, cluster_name) env = self.get_create_environment(env_name) if env: self._delete_environment(env) def delete_node(self, host_id): fullname = self.config_manager.get_host_fullname(host_id) node = self.get_create_node(fullname) if node: self._delete_node(node) def _delete_environment(self, env): """clean env attributes about arget system.""" if env is None: raise Exception("env is None, cannot delete a bnone env.") env_name = env.name try: env.delete() except Exception as error: logging.debug( 'failed to delete env %s, error: %s', env_name, error ) def _delete_node(self, node): """clean node attributes about target system.""" import chef if node is None: raise Exception("Node is None, cannot delete a none node.") node_name = node.name client_name = node_name # Clean log for this node first log_dir_prefix = compass_setting.INSTALLATION_LOGDIR[NAME] self._clean_log(log_dir_prefix, node_name) # Delete node and its client on chef server try: node.delete() client = chef.Client(client_name, api=self.chef_api) client.delete() logging.debug('delete node %s', node_name) except Exception as error: logging.debug('failed to delete node %s, error: %s', node_name, error) def add_roles(self, node, roles): """Add roles to the node. :param object node: The node object. :param list roles: The list of roles for this node. """ if node is None: raise Exception("Node is None!") if not roles: logging.info("Role list is None. Run list will no change.") return run_list = node.run_list for role in roles: if role not in self.all_chef_roles: raise Exception("Cannot find role '%s' on chef server!") node_role = 'role[%s]' % role if node_role not in run_list: node.run_list.append(node_role) node.save() logging.debug('Runlist for node %s is %s', node.name, node.run_list) def _generate_node_attributes(self, roles, host_vars_dict): """Generate node attributes. Generates from templates according to its roles. The templates are named by roles without '-'. Return the dictionary of attributes defined in the templates. :param list roles: The roles for this node, used to load the specific template. :param dict host_vars_dict: The dict used in cheetah searchList to render attributes from templates. """ if not roles: return {} node_tmpl_dir = os.path.join(self.tmpl_dir, self.NODE_TMPL_DIR) node_attr = {} for role in roles: role = role.replace('-', '_') tmpl_name = '.'.join((role, 'tmpl')) node_tmpl = os.path.join(node_tmpl_dir, tmpl_name) util.merge_dict( node_attr, self.get_config_from_template(node_tmpl, host_vars_dict) ) return node_attr def update_node_attributes_by_roles(self, node, roles, host_vars_dict): """Update node attributes to chef server.""" if node is None: raise Exception("Node is None!") if not roles: logging.info("The list of roles is None.") return # Update node attributes. node_config = self._generate_node_attributes(roles, host_vars_dict) available_attrs = ['default', 'normal', 'override'] for attr in node_config: if attr in available_attrs: node_attributes = getattr(node, attr).to_dict() util.merge_dict(node_attributes, node_config[attr]) setattr(node, attr, node_attributes) node.save() def _generate_env_attributes(self, global_vars_dict): """Get environment attributes from env templates.""" tmpl_name = self.config_manager.get_cluster_flavor_template() env_tmpl_path = os.path.join( os.path.join(self.tmpl_dir, self.ENV_TMPL_DIR), tmpl_name ) if not os.path.exists(env_tmpl_path): logging.error("Environment template '%s' doesn't exist", tmpl_name) raise Exception("Template '%s' does not exist!" % tmpl_name) logging.debug("generating env from template %s", env_tmpl_path) return self.get_config_from_template(env_tmpl_path, global_vars_dict) def get_create_environment(self, env_name): import chef env = chef.Environment(env_name, api=self.chef_api) env.save() return env def _update_environment(self, env, env_attrs): # By default, pychef provides these attribute keys: # 'description', 'cookbook_versions', 'default_attributes', # 'override_attributes' for name, value in env_attrs.iteritems(): if name in env.attributes: logging.debug("Updating env with attr %s", name) setattr(env, name, value) else: logging.info("Ignoring attr %s for env", name) env.save() def upload_environment(self, env_name, global_vars_dict): """Generate environment attributes :param str env_name: The environment name. :param dict vars_dict: The dictionary used in cheetah searchList to render attributes from templates. """ env_config = self._generate_env_attributes(global_vars_dict) env = self.get_create_environment(env_name) self._update_environment(env, env_config) def _generate_databagitem_attributes(self, tmpl_dir, vars_dict): return self.get_config_from_template(tmpl_dir, vars_dict) def update_databags(self, global_vars_dict): """Update databag item attributes. :param dict vars_dict: The dictionary used to get attributes from templates. """ databag_names = self.get_chef_databag_names() if not databag_names: return import chef databags_dir = os.path.join(self.tmpl_dir, self.DATABAG_TMPL_DIR) for databag_name in databag_names: tmpl_filename = databag_name + ".tmpl" databag_tmpl_filepath = os.path.join(databags_dir, tmpl_filename) databagitem_attrs = self._generate_databagitem_attributes( databag_tmpl_filepath, global_vars_dict ) if not databagitem_attrs: logging.info("Databag template not found or vars_dict is None") logging.info("databag template is %s", databag_tmpl_filepath) continue databag = self.get_create_databag(databag_name) for item, item_values in databagitem_attrs.iteritems(): databagitem = chef.DataBagItem(databag, item, self.chef_api) for key, value in item_values.iteritems(): databagitem[key] = value databagitem.save() def _get_host_tmpl_vars(self, host_id, global_vars_dict): """Generate templates variables dictionary. For cheetah searchList based on host package config. :param int host_id: The host ID. :param dict global_vars_dict: The vars_dict got from cluster level package_config. The output format is the same as cluster_vars_dict. """ host_vars_dict = {} if global_vars_dict: host_vars_dict = deepcopy(global_vars_dict) # Update host basic info host_baseinfo = self.config_manager.get_host_baseinfo(host_id) util.merge_dict( host_vars_dict.setdefault(const.BASEINFO, {}), host_baseinfo ) pk_config = self.config_manager.get_host_package_config(host_id) if pk_config: # Get host template variables and merge to vars_dict metadata = self.config_manager.get_pk_config_meatadata() meta_dict = self.get_tmpl_vars_from_metadata(metadata, pk_config) util.merge_dict( host_vars_dict.setdefault(const.PK_CONFIG, {}), meta_dict ) # Override role_mapping for host if host role_mapping exists mapping = self.config_manager.get_host_roles_mapping(host_id) if mapping: host_vars_dict[const.ROLES_MAPPING] = mapping return host_vars_dict def _get_cluster_tmpl_vars(self): """Generate template variables dict based on cluster level config. The vars_dict will be: { "baseinfo": { "id":1, "name": "cluster01", ... }, "package_config": { .... //mapped from original package config based on metadata }, "role_mapping": { .... } } """ cluster_vars_dict = {} # set cluster basic information to vars_dict cluster_baseinfo = self.config_manager.get_cluster_baseinfo() cluster_vars_dict[const.BASEINFO] = cluster_baseinfo # get and set template variables from cluster package config. pk_metadata = self.config_manager.get_pk_config_meatadata() pk_config = self.config_manager.get_cluster_package_config() meta_dict = self.get_tmpl_vars_from_metadata(pk_metadata, pk_config) cluster_vars_dict[const.PK_CONFIG] = meta_dict # get and set roles_mapping to vars_dict mapping = self.config_manager.get_cluster_roles_mapping() cluster_vars_dict[const.ROLES_MAPPING] = mapping return cluster_vars_dict def validate_roles(self, hosts_id_list): hosts_roles = self.config_manager.get_all_hosts_roles(hosts_id_list) for role in hosts_roles: if role not in self.all_chef_roles: logging.error("Role: %s cannot be found on chef server!", role) return False return True def deploy(self): """Start to deploy a distributed system. Return both cluster and hosts deployed configs. The return format: { "cluster": { "id": 1, "deployed_package_config": { "roles_mapping": {...}, "service_credentials": {...}, .... } }, "hosts": { 1($clusterhost_id): { "deployed_package_config": {...} }, .... } } """ host_list = self.config_manager.get_host_id_list() if not host_list: return {} if self.validate_roles(host_list) is False: raise Exception("Some role cannot be found in chef server!") adapter_name = self.config_manager.get_adapter_name() cluster_name = self.config_manager.get_clustername() env_name = self.get_env_name(adapter_name, cluster_name) global_vars_dict = self._get_cluster_tmpl_vars() # Upload environment to chef server self.upload_environment(env_name, global_vars_dict) # Update Databag item # TODO(grace): Fix the databag template rendering. # self.update_databags(global_vars_dict) hosts_deployed_configs = {} for host_id in host_list: node_name = self.config_manager.get_host_fullname(host_id) roles = self.config_manager.get_host_roles(host_id) node = self.get_create_node(node_name, env_name) self.add_roles(node, roles) vars_dict = self._get_host_tmpl_vars(host_id, global_vars_dict) # set each host deployed config host_config = {} temp = vars_dict.setdefault(const.PK_CONFIG, {}) temp[const.ROLES_MAPPING] = vars_dict.setdefault( const.ROLES_MAPPING, {} ) host_config = { host_id: {const.DEPLOYED_PK_CONFIG: temp} } hosts_deployed_configs.update(host_config) # set cluster deployed config cluster_config = {} cluster_config = global_vars_dict.setdefault(const.PK_CONFIG, {}) cluster_config[const.ROLES_MAPPING] = global_vars_dict.setdefault( const.ROLES_MAPPING, {} ) return { const.CLUSTER: { const.ID: self.config_manager.get_cluster_id(), const.DEPLOYED_PK_CONFIG: cluster_config }, const.HOSTS: hosts_deployed_configs } def generate_installer_config(self): """Render chef config file (client.rb) by OS installing right after OS is installed successfully. The output format: { '1'($host_id/clusterhost_id):{ 'tool': 'chef', 'chef_url': 'https://xxx', 'chef_client_name': '$host_name', 'chef_node_name': '$host_name', 'chef_server_ip': 'xxx',(op) 'chef_server_dns': 'xxx' (op) }, ..... } """ host_ids = self.config_manager.get_host_id_list() os_installer_configs = {} regex = "http[s]?://([^:/]+)[:\d]?.*" for host_id in host_ids: fullname = self.config_manager.get_host_fullname(host_id) temp = { "tool": "chef", "chef_url": self.installer_url } chef_host = re.search(regex, self.installer_url).groups()[0] try: IPAddress(chef_host) temp['chef_server_ip'] = chef_host temp['chef_server_dns'] = self.get_chef_server_dns() except Exception: chef_server_ip = self.get_chef_server_ip() if chef_server_ip: temp['chef_server_ip'] = chef_server_ip temp['chef_server_dns'] = chef_host temp['chef_client_name'] = fullname temp['chef_node_name'] = fullname os_installer_configs[host_id] = temp return os_installer_configs def clean_progress(self): """Clean all installing log about the node.""" log_dir_prefix = compass_setting.INSTALLATION_LOGDIR[self.NAME] hosts_list = self.config_manager.get_host_id_list() for host_id in hosts_list: fullname = self.config_manager.get_host_fullname() self._clean_log(log_dir_prefix, fullname) def _clean_log(self, log_dir_prefix, node_name): log_dir = os.path.join(log_dir_prefix, node_name) shutil.rmtree(log_dir, True) def get_supported_dist_systems(self): """get target systems from chef. All target_systems for compass will be stored in the databag called "compass". """ databag = self.__get_compass_databag() target_systems = {} for system_name, item in databag: target_systems[system_name] = item return target_systems def _clean_databag_item(self, databag, item_name): """clean databag item.""" import chef if item_name not in chef.DataBagItem.list(api=self.chef_api): logging.info("Databag item '%s' is not found!", item_name) return bag_item = databag[item_name] try: bag_item.delete() logging.debug('databag item %s is removed from databag', item_name) bag_item.save() except Exception as error: logging.debug('Failed to delete item %s from databag! Error: %s', item_name, error) def redeploy(self): """reinstall host.""" pass def get_chef_server_ip(self, installer_settings=None): settings = installer_settings if settings is None: settings = self.config_manager.get_pk_installer_settings() return settings.setdefault(self.CHEFSERVER_IP, None) def get_chef_url(self, installer_settings=None): settings = installer_settings if settings is None: settings = self.config_manager.get_pk_installer_settings() if self.CHEFSERVER_URL not in settings: err_msg = "%s is not in chef server settings" % self.CHEFSERVER_URL raise Exception(err_msg) return settings[self.CHEFSERVER_URL] def get_chef_credentials(self, installer_settings=None): settings = installer_settings if settings is None: settings = self.config_manager.get_pk_installer_settings() key_dir = settings.setdefault(self.KEY_DIR, None) client = settings.setdefault(self.CLIENT, None) return (key_dir, client) def get_chef_databag_names(self, installer_settings=None): settings = installer_settings if settings is None: settings = self.config_manager.get_pk_installer_settings() if self.DATABAGS not in settings: logging.info("No databags is set!") return None return settings[self.DATABAGS] def get_chef_server_dns(self, installer_settings=None): settings = installer_settings if settings is None: settings = self.config_manager.get_pk_installer_settings() return settings.setdefault(self.CHEFSERVER_DNS, None) def check_cluster_health(self, callback_url): import chef cluster_name = self.config_manager.get_clustername() nodes = chef.Search( 'node', 'tags:rally_node AND name:*%s' % cluster_name, api=self.chef_api ) if not nodes: err_msg = "Cannot find Rally node!" logging.info(err_msg) raise Exception(err_msg) rally_node_name = None for node in nodes: rally_node_name = node.object.name break rally_node = chef.Node(rally_node_name, api=self.chef_api) rally_node_ip = rally_node['ipaddress'] command = self.config_manager.get_adapter_health_check_cmd() command = command.replace('$cluster_name', cluster_name) option = '--url %s --clustername %s' % (callback_url, cluster_name) command = ' '.join((command, option)) username, pwd = self.config_manager.get_server_credentials() util.execute_cli_by_ssh( command, rally_node_ip, username=username, password=pwd, nowait=True )