From 905b0231e93ce2409a45dd6c4f5f983689fdb790 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Wed, 1 Nov 2017 11:56:50 +0800 Subject: Add compass-deck RESTful API and DB Handlers for Compass Change-Id: I1ce411f279943764c286ea48dca9185d453cf254 Signed-off-by: Harry Huang --- compass-deck/actions/__init__.py | 13 ++ compass-deck/actions/clean.py | 192 +++++++++++++++++ compass-deck/actions/cli.py | 179 ++++++++++++++++ compass-deck/actions/install_callback.py | 181 ++++++++++++++++ compass-deck/actions/poll_switch.py | 162 +++++++++++++++ compass-deck/actions/update_progress.py | 298 +++++++++++++++++++++++++++ compass-deck/actions/util.py | 342 +++++++++++++++++++++++++++++++ 7 files changed, 1367 insertions(+) create mode 100644 compass-deck/actions/__init__.py create mode 100644 compass-deck/actions/clean.py create mode 100644 compass-deck/actions/cli.py create mode 100644 compass-deck/actions/install_callback.py create mode 100644 compass-deck/actions/poll_switch.py create mode 100644 compass-deck/actions/update_progress.py create mode 100644 compass-deck/actions/util.py (limited to 'compass-deck/actions') diff --git a/compass-deck/actions/__init__.py b/compass-deck/actions/__init__.py new file mode 100644 index 0000000..4ee55a4 --- /dev/null +++ b/compass-deck/actions/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/compass-deck/actions/clean.py b/compass-deck/actions/clean.py new file mode 100644 index 0000000..8cb00b5 --- /dev/null +++ b/compass-deck/actions/clean.py @@ -0,0 +1,192 @@ +# 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. + +"""Module to clean installers +""" +import chef +import logging +import xmlrpclib + +from compass.actions import util + + +class CobblerInstaller(object): + """cobbler installer""" + CREDENTIALS = "credentials" + USERNAME = 'username' + PASSWORD = 'password' + + INSTALLER_URL = "cobbler_url" + + def __init__(self, settings): + username = settings[self.CREDENTIALS][self.USERNAME] + password = settings[self.CREDENTIALS][self.PASSWORD] + cobbler_url = settings[self.INSTALLER_URL] + try: + self.remote = xmlrpclib.Server(cobbler_url) + self.token = self.remote.login(username, password) + logging.info('cobbler %s client created', cobbler_url) + except Exception as error: + logging.error( + 'failed to login %s with (%s, %s)', + cobbler_url, username, password + ) + logging.exception(error) + + def clean(self): + systems = self.remote.get_systems() + for system in systems: + system_name = system['name'] + try: + self.remote.remove_system(system_name, self.token) + logging.info('system %s is removed', system_name) + except Exception as error: + logging.error( + 'failed to remove system %s', system_name + ) + logging.exception(error) + + +class AnsibleInstaller(object): + + def __init__(self, settings): + return + + def clean(self): + pass + + +class ChefInstaller(object): + 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, settings): + installer_url = settings.get(self.CHEFSERVER_URL, None) + key_dir = settings.get(self.KEY_DIR, None) + client = settings.get(self.CLIENT, None) + try: + if installer_url and key_dir and client: + self.api = chef.ChefAPI(installer_url, key_dir, client) + else: + self.api = chef.autoconfigure() + logging.info( + 'chef client created %s(%s, %s)', + installer_url, key_dir, client + ) + except Exception as error: + logging.error( + 'failed to create chef client %s(%s, %s)', + installer_url, key_dir, client + ) + logging.exception(error) + + def clean(self): + try: + for node_name in chef.Node.list(api=self.api): + node = chef.Node(node_name, api=self.api) + node.delete() + logging.info('delete node %s', node_name) + except Exception as error: + logging.error('failed to delete some nodes') + logging.exception(error) + + try: + for client_name in chef.Client.list(api=self.api): + if client_name in ['chef-webui', 'chef-validator']: + continue + client = chef.Client(client_name, api=self.api) + client.delete() + logging.info('delete client %s', client_name) + except Exception as error: + logging.error('failed to delete some clients') + logging.exception(error) + + try: + for env_name in chef.Environment.list(api=self.api): + if env_name == '_default': + continue + env = chef.Environment(env_name, api=self.api) + env.delete() + logging.info('delete env %s', env_name) + except Exception as error: + logging.error('failed to delete some envs') + logging.exception(error) + + try: + for databag_name in chef.DataBag.list(api=self.api): + databag = chef.DataBag(databag_name, api=self.api) + for item_name, item in databag.items(): + item.delete() + logging.info( + 'delete item %s from databag %s', + item_name, databag_name + ) + except Exception as error: + logging.error('failed to delete some databag items') + logging.exception(error) + + +OS_INSTALLERS = { + 'cobbler': CobblerInstaller +} +PK_INSTALLERS = { + 'chef_installer': ChefInstaller, + 'ansible_installer': AnsibleInstaller +} + + +def clean_os_installer( + os_installer_name, os_installer_settings +): + with util.lock('serialized_action', timeout=100) as lock: + if not lock: + raise Exception( + 'failed to acquire lock to clean os installer' + ) + + if os_installer_name not in OS_INSTALLERS: + logging.error( + '%s not found in os_installers', + os_installer_name + ) + + os_installer = OS_INSTALLERS[os_installer_name]( + os_installer_settings + ) + os_installer.clean() + + +def clean_package_installer( + package_installer_name, package_installer_settings +): + with util.lock('serialized_action', timeout=100) as lock: + if not lock: + raise Exception( + 'failed to acquire lock to clean package installer' + ) + + if package_installer_name not in PK_INSTALLERS: + logging.error( + '%s not found in os_installers', + package_installer_name + ) + + package_installer = PK_INSTALLERS[package_installer_name]( + package_installer_settings + ) + package_installer.clean() diff --git a/compass-deck/actions/cli.py b/compass-deck/actions/cli.py new file mode 100644 index 0000000..c9058ed --- /dev/null +++ b/compass-deck/actions/cli.py @@ -0,0 +1,179 @@ +# 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. + +"""Compass Command Line Interface""" +import logging +import subprocess +import sys + +from compass.actions.health_check import check +from compass.db.api import database + +from compass.utils import flags +from compass.utils import logsetting +from compass.utils import setting_wrapper as setting +from compass.utils.util import pretty_print + + +ACTION_MAP = { + "check": "apache celery dhcp dns hds misc os_installer " + "package_installer squid tftp".split(" "), + "refresh": "db sync".split(" "), +} + + +class BootCLI(object): + """CLI to do compass check.""" + + def __init__(self): + return + + def run(self, args): + """cli takes the commands and calls respective modules.""" + action = self.get_action(args) + if action is None: + self.print_help() + else: + module = self.get_module(action, args) + if module == "invalid": + self.print_help(action) + else: + method = "self.run_" + action + "(module)" + eval(method) + + @classmethod + def get_action(cls, args): + """This method returns an action type. + + .. note:: + For 'compass check dhcp' command, it will return 'check'. + """ + if len(args) == 1: + return None + elif args[1] in ACTION_MAP.keys(): + return args[1] + return None + + @classmethod + def get_module(cls, action, args): + """This method returns a module. + + .. note:: + For 'compass check dhcp' command, it will return 'dhcp'. + """ + if len(args) <= 2: + return None + elif args[2] in ACTION_MAP[action]: + return args[2] + return "invalid" + + def run_check(self, module=None): + """This provides a flexible sanity check. + + .. note:: + param module default set to None. + if parameter module is none. Compass checks all modules. + If module specified, Compass will only check such module. + """ + if module is None: + pretty_print("Starting: Compass Health Check", + "==============================") + chk = check.BootCheck() + res = chk.run() + self.output_check_result(res) + + else: + pretty_print("Checking Module: %s" % module, + "============================") + chk = check.BootCheck() + method = "chk._check_" + module + "()" + res = eval(method) + print "\n".join(msg for msg in res[1]) + + @classmethod + def output_check_result(cls, result): + """output check result.""" + if result == {}: + return + pretty_print("\n", + "===============================", + "* Compass Health Check Report *", + "===============================") + successful = True + for key in result.keys(): + if result[key][0] == 0: + successful = False + print "%s" % "\n".join(item for item in result[key][1]) + + print "====================" + if successful is True: + print "Compass Check completes. No problems found, all systems go" + sys.exit(0) + else: + print ( + "Compass has ERRORS shown above. Please fix them before " + "deploying!") + sys.exit(1) + + @classmethod + def run_refresh(cls, action=None): + """Run refresh.""" + # TODO(xicheng): replace refresh.sh with refresh.py + if action is None: + pretty_print("Refreshing Compass...", + "=================") + subprocess.Popen( + ['/opt/compass/bin/refresh.sh'], shell=True) + elif action == "db": + pretty_print("Refreshing Compass Database...", + "===================") + subprocess.Popen( + ['/opt/compass/bin/manage_db.py createdb'], shell=True) + else: + pretty_print("Syncing with Installers...", + "================") + subprocess.Popen( + ['/opt/compass/bin/manage_db.py sync_from_installers'], + shell=True + ) + + @classmethod + def print_help(cls, module_help=""): + """print help.""" + if module_help == "": + pretty_print("usage\n=====", + "compass ", + "type 'compass {action} --help' for detailed " + "command list") + + elif module_help == "refresh": + pretty_print("usage\n=====", + "compass refresh [%s]" % + "|".join(action for action in ACTION_MAP['refresh'])) + + else: + pretty_print("usage\n=====", + "compass check [%s]" % + "|".join(action for action in ACTION_MAP['check'])) + sys.exit(2) + + +def main(): + """Compass cli entry point.""" + flags.init() + logsetting.init() + database.init() + cli = BootCLI() + output = cli.run(sys.argv) + return sys.exit(output) diff --git a/compass-deck/actions/install_callback.py b/compass-deck/actions/install_callback.py new file mode 100644 index 0000000..aae955a --- /dev/null +++ b/compass-deck/actions/install_callback.py @@ -0,0 +1,181 @@ +# 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. + +"""Module to receive installation callback. + + .. moduleauthor:: Xiaodong Wang +""" +import logging + +from compass.actions import util +from compass.db.api import cluster as cluster_api +from compass.db.api import host as host_api +from compass.db.api import user as user_db +from compass.deployment.deploy_manager import DeployManager +from compass.deployment.utils import constants as const + + +def os_installed( + host_id, clusterhosts_ready, clusters_os_ready, + username=None +): + """Callback when os is installed. + + :param host_id: host that os is installed. + :type host_id: integer + :param clusterhosts_ready: the clusterhosts that should trigger ready. + :param clusters_os_ready: the cluster that should trigger os ready. + + .. note:: + The function should be called out of database session. + """ + with util.lock('serialized_action') as lock: + if not lock: + raise Exception( + 'failed to acquire lock to ' + 'do the post action after os installation' + ) + logging.info( + 'os installed on host %s ' + 'with cluster host ready %s cluster os ready %s', + host_id, clusterhosts_ready, clusters_os_ready + ) + if username: + user = user_db.get_user_object(username) + else: + user = None + os_installed_triggered = False + for cluster_id, clusterhost_ready in clusterhosts_ready.items(): + if not clusterhost_ready and os_installed_triggered: + continue + + cluster_info = util.ActionHelper.get_cluster_info( + cluster_id, user) + adapter_id = cluster_info[const.ADAPTER_ID] + + adapter_info = util.ActionHelper.get_adapter_info( + adapter_id, cluster_id, user) + hosts_info = util.ActionHelper.get_hosts_info( + cluster_id, [host_id], user) + + deploy_manager = DeployManager( + adapter_info, cluster_info, hosts_info) + + if not os_installed_triggered: + deploy_manager.os_installed() + util.ActionHelper.host_ready(host_id, True, user) + os_installed_triggered = True + + if clusterhost_ready: + # deploy_manager.cluster_os_installed() + util.ActionHelper.cluster_host_ready( + cluster_id, host_id, False, user + ) + + if util.ActionHelper.is_cluster_os_ready(cluster_id, user): + logging.info("deploy_manager begin cluster_os_installed") + deploy_manager.cluster_os_installed() + + +def package_installed( + cluster_id, host_id, cluster_ready, + host_ready, username=None +): + """Callback when package is installed. + + :param cluster_id: cluster id. + :param host_id: host id. + :param cluster_ready: if the cluster should trigger ready. + :param host_ready: if the host should trigger ready. + + .. note:: + The function should be called out of database session. + """ + with util.lock('serialized_action') as lock: + if not lock: + raise Exception( + 'failed to acquire lock to ' + 'do the post action after package installation' + ) + logging.info( + 'package installed on cluster %s host %s ' + 'with cluster ready %s host ready %s', + cluster_id, host_id, cluster_ready, host_ready + ) + + if username: + user = user_db.get_user_object(username) + else: + user = None + cluster_info = util.ActionHelper.get_cluster_info(cluster_id, user) + adapter_id = cluster_info[const.ADAPTER_ID] + + adapter_info = util.ActionHelper.get_adapter_info( + adapter_id, cluster_id, user) + hosts_info = util.ActionHelper.get_hosts_info( + cluster_id, [host_id], user) + + deploy_manager = DeployManager(adapter_info, cluster_info, hosts_info) + + deploy_manager.package_installed() + util.ActionHelper.cluster_host_ready(cluster_id, host_id, True, user) + if cluster_ready: + util.ActionHelper.cluster_ready(cluster_id, False, user) + if host_ready: + util.ActionHelper.host_ready(host_id, False, user) + + +def cluster_installed( + cluster_id, clusterhosts_ready, + username=None +): + """Callback when cluster is installed. + + :param cluster_id: cluster id + :param clusterhosts_ready: clusterhosts that should trigger ready. + + .. note:: + The function should be called out of database session. + """ + with util.lock('serialized_action') as lock: + if not lock: + raise Exception( + 'failed to acquire lock to ' + 'do the post action after cluster installation' + ) + logging.info( + 'package installed on cluster %s with clusterhosts ready %s', + cluster_id, clusterhosts_ready + ) + if username: + user = user_db.get_user_object(username) + else: + user = None + cluster_info = util.ActionHelper.get_cluster_info(cluster_id, user) + adapter_id = cluster_info[const.ADAPTER_ID] + + adapter_info = util.ActionHelper.get_adapter_info( + adapter_id, cluster_id, user) + hosts_info = util.ActionHelper.get_hosts_info( + cluster_id, clusterhosts_ready.keys(), user) + + deploy_manager = DeployManager(adapter_info, cluster_info, hosts_info) + + deploy_manager.cluster_installed() + util.ActionHelper.cluster_ready(cluster_id, True, user) + for host_id, clusterhost_ready in clusterhosts_ready.items(): + if clusterhost_ready: + util.ActionHelper.cluster_host_ready( + cluster_id, host_id, False, user + ) diff --git a/compass-deck/actions/poll_switch.py b/compass-deck/actions/poll_switch.py new file mode 100644 index 0000000..5c29b01 --- /dev/null +++ b/compass-deck/actions/poll_switch.py @@ -0,0 +1,162 @@ +# 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. + +"""Module to provider function to poll switch.""" +import logging +import netaddr + +from compass.actions import util +from compass.db.api import database +from compass.db.api import switch as switch_api +from compass.db.api import user as user_api +from compass.hdsdiscovery.hdmanager import HDManager + + +def _poll_switch(ip_addr, credentials, req_obj='mac', oper="SCAN"): + """Poll switch by ip addr. + + + Args: + ip_addr: ip addr of the switch. + credentials: credentials of the switch. + + Returns: switch attributes dict and list of machine attributes dict. + """ + under_monitoring = 'under_monitoring' + unreachable = 'unreachable' + polling_error = 'error' + hdmanager = HDManager() + vendor, state, err_msg = hdmanager.get_vendor(ip_addr, credentials) + if not vendor: + logging.info("*****error_msg: %s****", err_msg) + logging.error('no vendor found or match switch %s', ip_addr) + return ( + { + 'vendor': vendor, 'state': state, 'err_msg': err_msg + }, { + } + ) + + logging.debug( + 'hdmanager learn switch from %s', ip_addr + ) + results = [] + try: + results = hdmanager.learn( + ip_addr, credentials, vendor, req_obj, oper + ) + except Exception as error: + logging.exception(error) + state = unreachable + err_msg = ( + 'SNMP walk for querying MAC addresses timedout' + ) + return ( + { + 'vendor': vendor, 'state': state, 'err_msg': err_msg + }, { + } + ) + + logging.info("pollswitch %s result: %s", ip_addr, results) + if not results: + logging.error( + 'no result learned from %s', ip_addr + ) + state = polling_error + err_msg = 'No result learned from SNMP walk' + return ( + {'vendor': vendor, 'state': state, 'err_msg': err_msg}, + {} + ) + + logging.info('poll switch result: %s' % str(results)) + machine_dicts = {} + for machine in results: + mac = machine['mac'] + port = machine['port'] + vlan = int(machine['vlan']) + if vlan: + vlans = [vlan] + else: + vlans = [] + if mac not in machine_dicts: + machine_dicts[mac] = {'mac': mac, 'port': port, 'vlans': vlans} + else: + machine_dicts[mac]['port'] = port + machine_dicts[mac]['vlans'].extend(vlans) + + logging.debug('update switch %s state to under monitoring', ip_addr) + state = under_monitoring + return ( + {'vendor': vendor, 'state': state, 'err_msg': err_msg}, + machine_dicts.values() + ) + + +def poll_switch(poller_email, ip_addr, credentials, + req_obj='mac', oper="SCAN"): + """Query switch and update switch machines. + + .. note:: + When polling switch succeeds, for each mac it got from polling switch, + A Machine record associated with the switch is added to the database. + + :param ip_addr: switch ip address. + :type ip_addr: str + :param credentials: switch crednetials. + :type credentials: dict + :param req_obj: the object requested to query from switch. + :type req_obj: str + :param oper: the operation to query the switch. + :type oper: str, should be one of ['SCAN', 'GET', 'SET'] + + .. note:: + The function should be called out of database session scope. + """ + poller = user_api.get_user_object(poller_email) + ip_int = long(netaddr.IPAddress(ip_addr)) + with util.lock('poll switch %s' % ip_addr, timeout=120) as lock: + if not lock: + raise Exception( + 'failed to acquire lock to poll switch %s' % ip_addr + ) + + # TODO(grace): before repoll the switch, set the state to repolling. + # and when the poll switch is timeout, set the state to error. + # the frontend should only consider some main state like INTIALIZED, + # ERROR and SUCCESSFUL, REPOLLING is as an intermediate state to + # indicate the switch is in learning the mac of the machines connected + # to it. + logging.debug('poll switch: %s', ip_addr) + switch_dict, machine_dicts = _poll_switch( + ip_addr, credentials, req_obj=req_obj, oper=oper + ) + switches = switch_api.list_switches(ip_int=ip_int, user=poller) + if not switches: + logging.error('no switch found for %s', ip_addr) + return + + for switch in switches: + for machine_dict in machine_dicts: + logging.info('add machine: %s', machine_dict) + machine_dict['owner_id'] = poller.id + switch_api.add_switch_machine( + switch['id'], False, user=poller, **machine_dict + ) + switch_api.update_switch( + switch['id'], + user=poller, + **switch_dict + ) diff --git a/compass-deck/actions/update_progress.py b/compass-deck/actions/update_progress.py new file mode 100644 index 0000000..67a9963 --- /dev/null +++ b/compass-deck/actions/update_progress.py @@ -0,0 +1,298 @@ +# 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. + +"""Module to update status and installing progress of the given cluster. + + .. moduleauthor:: Xiaodong Wang +""" +import logging + +from compass.actions import util +from compass.db.api import adapter_holder as adapter_api +from compass.db.api import cluster as cluster_api +from compass.db.api import host as host_api +from compass.db.api import user as user_api +from compass.log_analyzor import progress_calculator +from compass.utils import setting_wrapper as setting + + +def update_progress(): + """Update status and installing progress of the given cluster. + + :param cluster_hosts: clusters and hosts in each cluster to update. + :type cluster_hosts: dict of int or str to list of int or str + + .. note:: + The function should be called out of the database session scope. + In the function, it will update the database cluster_state and + host_state table for the deploying cluster and hosts. + + The function will also query log_progressing_history table to get + the lastest installing progress and the position of log it has + processed in the last run. The function uses these information to + avoid recalculate the progress from the beginning of the log file. + After the progress got updated, these information will be stored back + to the log_progressing_history for next time run. + """ + with util.lock('log_progressing', timeout=60, blocking=False) as lock: + if not lock: + logging.error( + 'failed to acquire lock to calculate installation progress' + ) + return + + logging.info('update installing progress') + + user = user_api.get_user_object(setting.COMPASS_ADMIN_EMAIL) + hosts = host_api.list_hosts(user=user) + host_mapping = {} + for host in hosts: + if 'id' not in host: + logging.error('id is not in host %s', host) + continue + host_id = host['id'] + if 'os_name' not in host: + logging.error('os_name is not in host %s', host) + continue + if 'os_installer' not in host: + logging.error('os_installer is not in host %s', host) + continue + host_dirname = setting.HOST_INSTALLATION_LOGDIR_NAME + if host_dirname not in host: + logging.error( + '%s is not in host %s', host_dirname, host + ) + continue + host_state = host_api.get_host_state(host_id, user=user) + if 'state' not in host_state: + logging.error('state is not in host state %s', host_state) + continue + if host_state['state'] == 'INSTALLING': + host_log_histories = host_api.get_host_log_histories( + host_id, user=user + ) + host_log_history_mapping = {} + for host_log_history in host_log_histories: + if 'filename' not in host_log_history: + logging.error( + 'filename is not in host log history %s', + host_log_history + ) + continue + host_log_history_mapping[ + host_log_history['filename'] + ] = host_log_history + host_mapping[host_id] = ( + host, host_state, host_log_history_mapping + ) + else: + logging.info( + 'ignore host state %s since it is not in installing', + host_state + ) + adapters = adapter_api.list_adapters(user=user) + adapter_mapping = {} + for adapter in adapters: + if 'id' not in adapter: + logging.error( + 'id not in adapter %s', adapter + ) + continue + if 'package_installer' not in adapter: + logging.info( + 'package_installer not in adapter %s', adapter + ) + continue + adapter_id = adapter['id'] + adapter_mapping[adapter_id] = adapter + clusters = cluster_api.list_clusters(user=user) + cluster_mapping = {} + for cluster in clusters: + if 'id' not in cluster: + logging.error('id not in cluster %s', cluster) + continue + cluster_id = cluster['id'] + if 'adapter_id' not in cluster: + logging.error( + 'adapter_id not in cluster %s', + cluster + ) + continue + cluster_state = cluster_api.get_cluster_state( + cluster_id, + user=user + ) + if 'state' not in cluster_state: + logging.error('state not in cluster state %s', cluster_state) + continue + cluster_mapping[cluster_id] = (cluster, cluster_state) + clusterhosts = cluster_api.list_clusterhosts(user=user) + clusterhost_mapping = {} + for clusterhost in clusterhosts: + if 'clusterhost_id' not in clusterhost: + logging.error( + 'clusterhost_id not in clusterhost %s', + clusterhost + ) + continue + clusterhost_id = clusterhost['clusterhost_id'] + if 'cluster_id' not in clusterhost: + logging.error( + 'cluster_id not in clusterhost %s', + clusterhost + ) + continue + cluster_id = clusterhost['cluster_id'] + if cluster_id not in cluster_mapping: + logging.info( + 'ignore clusterhost %s ' + 'since the cluster_id ' + 'is not in cluster_mapping %s', + clusterhost, cluster_mapping + ) + continue + cluster, _ = cluster_mapping[cluster_id] + if 'flavor_name' not in cluster: + logging.error( + 'flavor_name is not in clusterhost %s related cluster', + clusterhost + ) + continue + clusterhost_dirname = setting.CLUSTERHOST_INATALLATION_LOGDIR_NAME + if clusterhost_dirname not in clusterhost: + logging.error( + '%s is not in clusterhost %s', + clusterhost_dirname, clusterhost + ) + continue + adapter_id = cluster['adapter_id'] + if adapter_id not in adapter_mapping: + logging.info( + 'ignore clusterhost %s ' + 'since the adapter_id %s ' + 'is not in adaper_mapping %s', + clusterhost, adapter_id, adapter_mapping + ) + continue + adapter = adapter_mapping[adapter_id] + if 'package_installer' not in adapter: + logging.info( + 'ignore clusterhost %s ' + 'since the package_installer is not define ' + 'in adapter %s', + clusterhost, adapter + ) + continue + package_installer = adapter['package_installer'] + clusterhost['package_installer'] = package_installer + clusterhost['adapter_name'] = adapter['name'] + clusterhost_state = cluster_api.get_clusterhost_self_state( + clusterhost_id, user=user + ) + if 'state' not in clusterhost_state: + logging.error( + 'state not in clusterhost_state %s', + clusterhost_state + ) + continue + if clusterhost_state['state'] == 'INSTALLING': + clusterhost_log_histories = ( + cluster_api.get_clusterhost_log_histories( + clusterhost_id, user=user + ) + ) + clusterhost_log_history_mapping = {} + for clusterhost_log_history in clusterhost_log_histories: + if 'filename' not in clusterhost_log_history: + logging.error( + 'filename not in clusterhost_log_history %s', + clusterhost_log_history + ) + continue + clusterhost_log_history_mapping[ + clusterhost_log_history['filename'] + ] = clusterhost_log_history + clusterhost_mapping[clusterhost_id] = ( + clusterhost, clusterhost_state, + clusterhost_log_history_mapping + ) + else: + logging.info( + 'ignore clusterhost state %s ' + 'since it is not in installing', + clusterhost_state + ) + + progress_calculator.update_host_progress( + host_mapping) + for host_id, (host, host_state, host_log_history_mapping) in ( + host_mapping.items() + ): + host_api.update_host_state( + host_id, user=user, + percentage=host_state.get('percentage', 0), + message=host_state.get('message', ''), + severity=host_state.get('severity', 'INFO') + ) + for filename, host_log_history in ( + host_log_history_mapping.items() + ): + host_api.add_host_log_history( + host_id, filename=filename, user=user, + position=host_log_history.get('position', 0), + percentage=host_log_history.get('percentage', 0), + partial_line=host_log_history.get('partial_line', ''), + message=host_log_history.get('message', ''), + severity=host_log_history.get('severity', 'INFO'), + line_matcher_name=host_log_history.get( + 'line_matcher_name', 'start' + ) + ) + progress_calculator.update_clusterhost_progress( + clusterhost_mapping) + for ( + clusterhost_id, + (clusterhost, clusterhost_state, clusterhost_log_history_mapping) + ) in ( + clusterhost_mapping.items() + ): + cluster_api.update_clusterhost_state( + clusterhost_id, user=user, + percentage=clusterhost_state.get('percentage', 0), + message=clusterhost_state.get('message', ''), + severity=clusterhost_state.get('severity', 'INFO') + ) + for filename, clusterhost_log_history in ( + clusterhost_log_history_mapping.items() + ): + cluster_api.add_clusterhost_log_history( + clusterhost_id, user=user, filename=filename, + position=clusterhost_log_history.get('position', 0), + percentage=clusterhost_log_history.get('percentage', 0), + partial_line=clusterhost_log_history.get( + 'partial_line', ''), + message=clusterhost_log_history.get('message', ''), + severity=clusterhost_log_history.get('severity', 'INFO'), + line_matcher_name=( + clusterhost_log_history.get( + 'line_matcher_name', 'start' + ) + ) + ) + progress_calculator.update_cluster_progress( + cluster_mapping) + for cluster_id, (cluster, cluster_state) in cluster_mapping.items(): + cluster_api.update_cluster_state( + cluster_id, user=user + ) diff --git a/compass-deck/actions/util.py b/compass-deck/actions/util.py new file mode 100644 index 0000000..4d9f855 --- /dev/null +++ b/compass-deck/actions/util.py @@ -0,0 +1,342 @@ +# 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. + +"""Module to provide util for actions + + .. moduleauthor:: Xiaodong Wang ,xiaodongwang@huawei.com> +""" +import logging +import redis + +from contextlib import contextmanager + +from compass.db.api import adapter_holder as adapter_db +from compass.db.api import cluster as cluster_db +from compass.db.api import host as host_db +from compass.db.api import machine as machine_db +from compass.deployment.utils import constants as const + + +@contextmanager +def lock(lock_name, blocking=True, timeout=10): + """acquire a lock to do some actions. + + The lock is acquired by lock_name among the whole distributed + systems. + """ + # TODO(xicheng): in future we should explicitly told which redis + # server we want to talk to make the lock works on distributed + # systems. + redis_instance = redis.Redis() + instance_lock = redis_instance.lock(lock_name, timeout=timeout) + owned = False + try: + locked = instance_lock.acquire(blocking=blocking) + if locked: + owned = True + logging.debug('acquired lock %s', lock_name) + yield instance_lock + else: + logging.info('lock %s is already hold', lock_name) + yield None + + except Exception as error: + logging.info( + 'redis fails to acquire the lock %s', lock_name) + logging.exception(error) + yield None + + finally: + if owned: + instance_lock.acquired_until = 0 + instance_lock.release() + logging.debug('released lock %s', lock_name) + else: + logging.debug('nothing to release %s', lock_name) + + +class ActionHelper(object): + + @staticmethod + def get_adapter_info(adapter_id, cluster_id, user): + """Get adapter information. Return a dictionary as below, + + { + "id": 1, + "name": "xxx", + "flavors": [ + { + "flavor_name": "xxx", + "roles": ['xxx', 'yyy', ...], + "template": "xxx.tmpl" + }, + ... + ], + "metadata": { + "os_config": { + ... + }, + "package_config": { + ... + } + }, + "os_installer": { + "name": "cobbler", + "settings": {....} + }, + "pk_installer": { + "name": "chef", + "settings": {....} + }, + ... + } + To view a complete output, please refer to backend doc. + """ + + adapter_info = adapter_db.get_adapter(adapter_id, user=user) + metadata = cluster_db.get_cluster_metadata(cluster_id, user=user) + adapter_info.update({const.METADATA: metadata}) + + for flavor_info in adapter_info[const.FLAVORS]: + roles = flavor_info[const.ROLES] + flavor_info[const.ROLES] = ActionHelper._get_role_names(roles) + + return adapter_info + + @staticmethod + def _get_role_names(roles): + return [role[const.NAME] for role in roles] + + @staticmethod + def get_cluster_info(cluster_id, user): + """Get cluster information.Return a dictionary as below, + + { + "id": 1, + "adapter_id": 1, + "os_version": "CentOS-6.5-x86_64", + "name": "cluster_01", + "flavor": { + "flavor_name": "zzz", + "template": "xx.tmpl", + "roles": [...] + } + "os_config": {..}, + "package_config": {...}, + "deployed_os_config": {}, + "deployed_package_config": {}, + "owner": "xxx" + } + """ + + cluster_info = cluster_db.get_cluster(cluster_id, user=user) + + # convert roles retrieved from db into a list of role names + roles_info = cluster_info.setdefault( + const.FLAVOR, {}).setdefault(const.ROLES, []) + cluster_info[const.FLAVOR][const.ROLES] = \ + ActionHelper._get_role_names(roles_info) + + # get cluster config info + cluster_config = cluster_db.get_cluster_config(cluster_id, user=user) + cluster_info.update(cluster_config) + + deploy_config = cluster_db.get_cluster_deployed_config(cluster_id, + user=user) + cluster_info.update(deploy_config) + + return cluster_info + + @staticmethod + def get_hosts_info(cluster_id, hosts_id_list, user): + """Get hosts information. Return a dictionary as below, + + { + "hosts": { + 1($host_id): { + "reinstall_os": True, + "mac": "xxx", + "name": "xxx", + "roles": [xxx, yyy] + }, + "networks": { + "eth0": { + "ip": "192.168.1.1", + "netmask": "255.255.255.0", + "is_mgmt": True, + "is_promiscuous": False, + "subnet": "192.168.1.0/24" + }, + "eth1": {...} + }, + "os_config": {}, + "package_config": {}, + "deployed_os_config": {}, + "deployed_package_config": {} + }, + 2: {...}, + .... + } + } + """ + + hosts_info = {} + for host_id in hosts_id_list: + info = cluster_db.get_cluster_host(cluster_id, host_id, user=user) + logging.debug("checking on info %r %r" % (host_id, info)) + + info[const.ROLES] = ActionHelper._get_role_names(info[const.ROLES]) + + # TODO(grace): Is following line necessary?? + info.setdefault(const.ROLES, []) + + config = cluster_db.get_cluster_host_config(cluster_id, + host_id, + user=user) + info.update(config) + + networks = info[const.NETWORKS] + networks_dict = {} + # Convert networks from list to dictionary format + for entry in networks: + nic_info = {} + nic_info = { + entry[const.NIC]: { + const.IP_ADDR: entry[const.IP_ADDR], + const.NETMASK: entry[const.NETMASK], + const.MGMT_NIC_FLAG: entry[const.MGMT_NIC_FLAG], + const.PROMISCUOUS_FLAG: entry[const.PROMISCUOUS_FLAG], + const.SUBNET: entry[const.SUBNET] + } + } + networks_dict.update(nic_info) + + info[const.NETWORKS] = networks_dict + + hosts_info[host_id] = info + + return hosts_info + + @staticmethod + def save_deployed_config(deployed_config, user): + """Save deployed config.""" + cluster_config = deployed_config[const.CLUSTER] + cluster_id = cluster_config[const.ID] + del cluster_config[const.ID] + + cluster_db.update_cluster_deployed_config(cluster_id, user=user, + **cluster_config) + + hosts_id_list = deployed_config[const.HOSTS].keys() + for host_id in hosts_id_list: + config = deployed_config[const.HOSTS][host_id] + cluster_db.update_cluster_host_deployed_config(cluster_id, + host_id, + user=user, + **config) + + @staticmethod + def update_state( + cluster_id, host_id_list, user, **kwargs + ): + # update all clusterhosts state + for host_id in host_id_list: + cluster_db.update_cluster_host_state( + cluster_id, + host_id, + user=user, + **kwargs + ) + + # update cluster state + cluster_db.update_cluster_state( + cluster_id, + user=user, + **kwargs + ) + + @staticmethod + def delete_cluster( + cluster_id, host_id_list, user, delete_underlying_host=False + ): + """Delete cluster. + + If delete_underlying_host is set, underlying hosts will also + be deleted. + """ + if delete_underlying_host: + for host_id in host_id_list: + host_db.del_host( + host_id, True, True, user=user + ) + cluster_db.del_cluster( + cluster_id, True, True, user=user + ) + + @staticmethod + def delete_cluster_host( + cluster_id, host_id, user, delete_underlying_host=False + ): + """Delete clusterhost. + + If delete_underlying_host set, also delete underlying host. + """ + if delete_underlying_host: + host_db.del_host( + host_id, True, True, user=user + ) + cluster_db.del_cluster_host( + cluster_id, host_id, True, True, user=user + ) + + @staticmethod + def delete_host(host_id, user): + host_db.del_host( + host_id, True, True, user=user + ) + + @staticmethod + def host_ready(host_id, from_database_only, user): + """Trigger host ready.""" + host_db.update_host_state_internal( + host_id, from_database_only=from_database_only, + user=user, ready=True + ) + + @staticmethod + def cluster_host_ready( + cluster_id, host_id, from_database_only, user + ): + """Trigger clusterhost ready.""" + cluster_db.update_cluster_host_state_internal( + cluster_id, host_id, from_database_only=from_database_only, + user=user, ready=True + ) + + @staticmethod + def is_cluster_os_ready(cluster_id, user=None): + return cluster_db.is_cluster_os_ready(cluster_id, user=user) + + @staticmethod + def cluster_ready(cluster_id, from_database_only, user): + """Trigger cluster ready.""" + cluster_db.update_cluster_state_internal( + cluster_id, from_database_only=from_database_only, + user=user, ready=True + ) + + @staticmethod + def get_machine_IPMI(machine_id, user): + machine_info = machine_db.get_machine(machine_id, user=user) + return machine_info[const.IPMI_CREDS] -- cgit 1.2.3-korg