From ce07c3b7dc5591f7ae9ed2853287e3941eb4e8e1 Mon Sep 17 00:00:00 2001 From: xiaodongwang Date: Mon, 27 Oct 2014 15:59:36 -0700 Subject: [PATCH] clean installers when refresh Change-Id: I86c27b0d9be0af6ef58748d97938a2df54b297c3 --- bin/clean_installers.py | 163 ++++++++++++++++++++++++++++++ bin/delete_clusters.py | 5 +- bin/refresh.sh | 5 +- compass/actions/clean.py | 182 ++++++++++++++++++++++++++++++++++ compass/db/api/metadata.py | 15 ++- compass/db/validator.py | 96 ++++++++++++++++-- compass/tasks/tasks.py | 27 +++++ conf/os_field/domain.conf | 2 + conf/os_field/ip_list.conf | 3 + conf/os_field/url.conf | 2 + conf/os_metadata/general.conf | 12 +-- install/install_func.sh | 7 +- install/prepare.sh | 8 +- regtest/regtest.sh | 6 +- tox.ini | 2 +- 15 files changed, 504 insertions(+), 31 deletions(-) create mode 100755 bin/clean_installers.py create mode 100644 compass/actions/clean.py create mode 100644 conf/os_field/domain.conf create mode 100644 conf/os_field/ip_list.conf create mode 100644 conf/os_field/url.conf diff --git a/bin/clean_installers.py b/bin/clean_installers.py new file mode 100755 index 00000000..a2fef6fe --- /dev/null +++ b/bin/clean_installers.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# +# 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. + +"""Scripts to delete cluster and it hosts""" +import logging +import os +import os.path +import sys + + +current_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(current_dir) + + +import switch_virtualenv + +from compass.actions import clean +from compass.db.api import adapter_holder as adapter_api +from compass.db.api import database +from compass.db.api import user as user_api +from compass.tasks.client import celery +from compass.utils import flags +from compass.utils import logsetting +from compass.utils import setting_wrapper as setting + + +flags.add_bool('async', + help='run in async mode', + default=True) + +flags.add('os_installers', + help='comma seperated os installers', + default='') +flags.add('package_installers', + help='comma separated package installers', + default='') + + +def clean_installers(): + os_installers = [ + os_installer + for os_installer in flags.OPTIONS.os_installers.split(',') + if os_installer + ] + package_installers = [ + package_installer + for package_installer in flags.OPTIONS.package_installers.split(',') + if package_installer + ] + user = user_api.get_user_object(setting.COMPASS_ADMIN_EMAIL) + adapters = adapter_api.list_adapters(user) + filtered_os_installers = {} + filtered_package_installers = {} + for adapter in adapters: + logging.info( + 'got adapter: %s', adapter + ) + if 'os_installer' in adapter: + os_installer = adapter['os_installer'] + os_installer_name = os_installer['alias'] + if not os_installers or os_installer_name in os_installers: + filtered_os_installers[os_installer_name] = os_installer + else: + logging.info( + 'ignore os isntaller %s', os_installer_name + ) + else: + logging.info( + 'cannot find os installer in adapter %s', + adapter['name'] + ) + if 'package_installer' in adapter: + package_installer = adapter['package_installer'] + package_installer_name = package_installer['alias'] + if ( + not package_installers or + package_installer_name in package_installers + ): + filtered_package_installers[package_installer_name] = ( + package_installer + ) + else: + logging.info( + 'ignore package installer %s', package_installer_name + ) + else: + logging.info( + 'cannot find package installer in adapter %s', + adapter['name'] + ) + logging.info( + 'clean os installers: %s', filtered_os_installers.keys() + ) + logging.info( + 'clean package installers: %s', filtered_package_installers.keys() + ) + if flags.OPTIONS.async: + for os_installer_name, os_installer in filtered_os_installers.items(): + celery.send_task( + 'compass.tasks.clean_os_installer', + ( + os_installer['name'], + os_installer['settings'] + ) + ) + for package_installer_name, package_installer in ( + filtered_package_installers.items() + ): + celery.send_task( + 'compass.tasks.clean_package_installer', + ( + package_installer['name'], + package_installer['settings'] + ) + ) + else: + for os_installer_name, os_installer in ( + filtered_os_installers.items() + ): + try: + clean.clean_os_installer( + os_installer['name'], + os_installer['settings'] + ) + except Exception as error: + logging.error( + 'failed to clean os installer %s', os_installer_name + ) + logging.exception(error) + for package_installer_name, package_installer in ( + filtered_package_installers.items() + ): + try: + clean.clean_package_installer( + package_installer['name'], + package_installer['settings'] + ) + except Exception as error: + logging.error( + 'failed to clean package installer %s', + package_installer_name + ) + logging.exception(error) + + +if __name__ == '__main__': + flags.init() + logsetting.init() + database.init() + clean_installers() diff --git a/bin/delete_clusters.py b/bin/delete_clusters.py index 277daed9..c822a4f1 100755 --- a/bin/delete_clusters.py +++ b/bin/delete_clusters.py @@ -56,8 +56,11 @@ def delete_clusters(): if clustername ] user = user_api.get_user_object(setting.COMPASS_ADMIN_EMAIL) + list_cluster_args = {} + if clusternames: + list_cluster_args['name'] = clusternames clusters = cluster_api.list_clusters( - user, name=clusternames + user, **list_cluster_args ) delete_underlying_host = flags.OPTIONS.delete_hosts for cluster in clusters: diff --git a/bin/refresh.sh b/bin/refresh.sh index b2c5ac84..3b992b39 100755 --- a/bin/refresh.sh +++ b/bin/refresh.sh @@ -2,10 +2,7 @@ set -e service mysqld restart /opt/compass/bin/manage_db.py createdb -echo "You may run '/opt/compass/bin/clean_nodes.sh' to clean nodes on chef server" -echo "You may run '/opt/compass/bin/clean_clients.sh' to clean clients on chef server" -echo "you may run '/opt/compass/bin/clean_environments.sh' to clean environments on chef server" -echo "you may run '/opt/compass/bin/remove_systems.sh' to clean systems on cobbler" +/opt/compass/bin/clean_installers.py /opt/compass/bin/clean_installation_logs.py service httpd restart service rsyslog restart diff --git a/compass/actions/clean.py b/compass/actions/clean.py new file mode 100644 index 00000000..c9a5d06c --- /dev/null +++ b/compass/actions/clean.py @@ -0,0 +1,182 @@ +# 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 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 +} + + +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/db/api/metadata.py b/compass/db/api/metadata.py index dfa0813f..41122d2b 100644 --- a/compass/db/api/metadata.py +++ b/compass/db/api/metadata.py @@ -301,17 +301,28 @@ def _validate_self( metadata, whole_check, **kwargs ): + logging.debug('validate config self %s', config_path) if '_self' not in metadata: if isinstance(config, dict): _validate_config( config_path, config, metadata, whole_check, **kwargs ) return - field_type = metadata['_self'].get('field_type', 'basestring') + field_type = metadata['_self'].get('field_type', basestring) if not isinstance(config, field_type): raise exception.InvalidParameter( '%s config type is not %s' % (config_path, field_type) ) + is_required = metadata['_self'].get( + 'is_required', False + ) + required_in_whole_config = metadata['_self'].get( + 'required_in_whole_config', False + ) + if isinstance(config, basestring): + if config == '' and not is_required and not required_in_whole_config: + # ignore empty config when it is optional + return required_in_options = metadata['_self'].get( 'required_in_options', False ) @@ -333,6 +344,7 @@ def _validate_self( '%s config is not in %s' % (config_path, options) ) validator = metadata['_self'].get('validator', None) + logging.debug('validate by validator %s', validator) if validator: if not validator(config_key, config, **kwargs): raise exception.InvalidParameter( @@ -348,6 +360,7 @@ def _validate_config( config_path, config, metadata, whole_check, **kwargs ): + logging.debug('validate config %s', config_path) generals = {} specified = {} for key, value in metadata.items(): diff --git a/compass/db/validator.py b/compass/db/validator.py index c8c4d3a2..730bb52c 100644 --- a/compass/db/validator.py +++ b/compass/db/validator.py @@ -13,6 +13,7 @@ # limitations under the License. """Validator methods.""" +import logging import netaddr import re import socket @@ -23,87 +24,162 @@ from compass.utils import util def is_valid_ip(name, ip_addr, **kwargs): """Valid the format of an IP address.""" + if isinstance(ip_addr, list): + return all([ + is_valid_ip(name, item, **kwargs) for item in ip_addr + ]) try: netaddr.IPAddress(ip_addr) except Exception: + logging.debug('%s invalid ip addr %s', name, ip_addr) return False return True def is_valid_network(name, ip_network, **kwargs): """Valid the format of an Ip network.""" + if isinstance(ip_network, list): + return all([ + is_valid_network(name, item, **kwargs) for item in ip_network + ]) try: netaddr.IPNetwork(ip_network) except Exception: + logging.debug('%s invalid network %s', name, ip_network) return False - return False + return True def is_valid_netmask(name, ip_addr, **kwargs): """Valid the format of a netmask.""" + if isinstance(ip_addr, list): + return all([ + is_valid_netmask(name, item, **kwargs) for item in ip_addr + ]) if not is_valid_ip(ip_addr): return False ip = netaddr.IPAddress(ip_addr) if ip.is_netmask(): return True - else: - return False + logging.debug('%s invalid netmask %s', name, ip_addr) + return False def is_valid_gateway(name, ip_addr, **kwargs): """Valid the format of gateway.""" + if isinstance(ip_addr, list): + return all([ + is_valid_gateway(name, item, **kwargs) for item in ip_addr + ]) if not is_valid_ip(ip_addr): return False ip = netaddr.IPAddress(ip_addr) if ip.is_private() or ip.is_public(): return True - else: - return False + logging.debug('%s invalid gateway %s', name, ip_addr) + return False def is_valid_dns(name, dns, **kwargs): """Valid the format of DNS.""" + if isinstance(dns, list): + return all([is_valid_dns(name, item, **kwargs) for item in dns]) if is_valid_ip(dns): return True try: socket.gethostbyname_ex(dns) except Exception: + logging.debug('%s invalid dns name %s', name, dns) return False return True +def is_valid_url(name, url, **kwargs): + """Valid the format of url.""" + if isinstance(url, list): + return all([ + is_valid_url(name, item, **kwargs) for item in url + ]) + if re.match( + r'^(http|https|ftp)://([0-9A-Za-z_-]+)(\.[0-9a-zA-Z_-]+)*' + r'(:\d+)?(/[0-9a-zA-Z_-]+)*$', + url + ): + return True + logging.debug( + '%s invalid url %s', name, url + ) + return False + + +def is_valid_domain(name, domain, **kwargs): + """Validate the format of domain.""" + if isinstance(domain, list): + return all([ + is_valid_domain(name, item, **kwargs) for item in domain + ]) + if re.match( + r'^([0-9a-zA-Z_-]+)(\.[0-9a-zA-Z_-]+)*$', + domain + ): + return True + logging.debug( + '%s invalid domain %s', name, domain + ) + return False + + def is_valid_username(name, username, **kwargs): """Valid the format of username.""" - return bool(username) + if bool(username): + return True + logging.debug( + '%s username is empty', name + ) def is_valid_password(name, password, **kwargs): """Valid the format of password.""" - return bool(password) + if bool(password): + return True + logging.debug('%s password is empty', name) + return False def is_valid_partition(name, partition, **kwargs): """Valid the format of partition name.""" if name != 'swap' and not name.startswith('/'): + logging.debug( + '%s is not started with / or swap', name + ) return False if 'size' not in partition and 'percentage' not in partition: + logging.debug( + '%s partition does not contain sie or percentage', + name + ) return False return True def is_valid_percentage(name, percentage, **kwargs): """Valid the percentage.""" - return 0 <= percentage <= 100 + if 0 <= percentage <= 100: + return True + logging.debug('%s invalid percentage %s', name, percentage) def is_valid_port(name, port, **kwargs): """Valid the format of port.""" - return 0 < port < 65536 + if 0 < port < 65536: + return True + logging.debug('%s invalid port %s', name, port) def is_valid_size(name, size, **kwargs): - if re.match(r'(\d+)(K|M|G|T)?', size): + if re.match(r'^(\d+)(K|M|G|T)$', size): return True + logging.debug('%s invalid size %s', name, size) return False diff --git a/compass/tasks/tasks.py b/compass/tasks/tasks.py index eec1204d..63fbd035 100644 --- a/compass/tasks/tasks.py +++ b/compass/tasks/tasks.py @@ -21,6 +21,7 @@ import logging from celery.signals import celeryd_init from celery.signals import setup_logging +from compass.actions import clean from compass.actions import delete from compass.actions import deploy from compass.actions import poll_switch @@ -168,6 +169,32 @@ def delete_host(deleter_email, host_id, cluster_ids): logging.exception(error) +@celery.task(name='compass.tasks.clean_os_installer') +def clean_os_installer( + os_installer_name, os_installer_settings +): + """Clean os installer.""" + try: + clean.clean_os_installer( + os_installer_name, os_installer_settings + ) + except Exception as error: + logging.excception(error) + + +@celery.task(name='compass.tasks.clean_package_installer') +def clean_package_installer( + package_installer_name, package_installer_settings +): + """Clean package installer.""" + try: + clean.clean_package_installer( + package_installer_name, package_installer_settings + ) + except Exception as error: + logging.excception(error) + + @celery.task(name='compass.tasks.poweron_host') def poweron_host(host_id): """Deploy the given cluster. diff --git a/conf/os_field/domain.conf b/conf/os_field/domain.conf new file mode 100644 index 00000000..fdf74289 --- /dev/null +++ b/conf/os_field/domain.conf @@ -0,0 +1,2 @@ +NAME = 'domain' +VALIDATOR = is_valid_domain diff --git a/conf/os_field/ip_list.conf b/conf/os_field/ip_list.conf new file mode 100644 index 00000000..28969cb2 --- /dev/null +++ b/conf/os_field/ip_list.conf @@ -0,0 +1,3 @@ +NAME = 'ip_list' +FIELD_TYPE = list +VALIDATOR = is_valid_ip diff --git a/conf/os_field/url.conf b/conf/os_field/url.conf new file mode 100644 index 00000000..b6a74f35 --- /dev/null +++ b/conf/os_field/url.conf @@ -0,0 +1,2 @@ +NAME = 'url' +VALIDATOR = is_valid_url diff --git a/conf/os_metadata/general.conf b/conf/os_metadata/general.conf index 8caf1571..b6a6c481 100644 --- a/conf/os_metadata/general.conf +++ b/conf/os_metadata/general.conf @@ -29,7 +29,7 @@ METADATA = { }, 'http_proxy': { '_self': { - 'field': 'general', + 'field': 'url', 'default_callback': default_proxy, 'options_callback': proxy_options, 'mapping_to': 'http_proxy' @@ -37,7 +37,7 @@ METADATA = { }, 'https_proxy': { '_self': { - 'field': 'general', + 'field': 'url', 'default_callback': default_proxy, 'options_callback': proxy_options, 'mapping_to': 'https_proxy' @@ -55,7 +55,7 @@ METADATA = { 'ntp_server': { '_self': { 'is_required': True, - 'field': 'general', + 'field': 'ip', 'default_callback': default_ntp_server, 'options_callback': ntp_server_options, 'mapping_to': 'ntp_server' @@ -64,7 +64,7 @@ METADATA = { 'dns_servers': { '_self': { 'is_required': True, - 'field': 'general_list', + 'field': 'ip_list', 'default_callback': default_dns_servers, 'options_callback': dns_servers_options, 'mapping_to': 'nameservers' @@ -72,7 +72,7 @@ METADATA = { }, 'domain': { '_self': { - 'field': 'general', + 'field': 'domain', 'is_required' : True, 'default_callback': default_domain, 'options_callback': domain_options, @@ -96,7 +96,7 @@ METADATA = { }, 'local_repo': { '_self': { - 'field': 'general', + 'field': 'url', 'default_callback': default_localrepo, 'mapping_to': 'local_repo' } diff --git a/install/install_func.sh b/install/install_func.sh index 1f4df8e9..40670c1e 100755 --- a/install/install_func.sh +++ b/install/install_func.sh @@ -41,21 +41,18 @@ copy2dir() git clean -x -f git checkout $git_branch git reset --hard remotes/origin/$git_branch + git clean -x -f -d -q cd - else echo "create $destdir" mkdir -p $destdir - git clone $repo $destdir + git clone $repo $destdir -b $git_branch if [ $? -ne 0 ]; then echo "failed to git clone $repo $destdir" exit 1 else echo "git clone $repo $destdir suceeded" fi - cd $destdir - git checkout $git_branch - git reset --hard remotes/origin/$git_branch - cd - fi cd $destdir if [[ -z $ZUUL_PROJECT ]]; then diff --git a/install/prepare.sh b/install/prepare.sh index f0c41794..c5adf949 100755 --- a/install/prepare.sh +++ b/install/prepare.sh @@ -125,6 +125,12 @@ else fi cd $SCRIPT_DIR +remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u}) +if [[ "$?" != "0" ]]; then + remote_branch="origin/master" +fi +local_branch=$(echo ${remote_branch} | sed -e 's/origin\///g') + if [ -z $WEB_SOURCE ]; then echo "web source $WEB_SOURCE is not set" exit 1 @@ -135,7 +141,7 @@ if [ -z $ADAPTERS_SOURCE ]; then echo "adpaters source $ADAPTERS_SOURCE is not set" exit 1 fi -copy2dir "$ADAPTERS_SOURCE" "$ADAPTERS_HOME" "stackforge/compass-adapters" dev/experimental || exit $? +copy2dir "$ADAPTERS_SOURCE" "$ADAPTERS_HOME" "stackforge/compass-adapters" ${local_branch} || exit $? if [ "$tempest" == "true" ]; then echo "download tempest packages" diff --git a/regtest/regtest.sh b/regtest/regtest.sh index 2aa9498b..bc93535b 100755 --- a/regtest/regtest.sh +++ b/regtest/regtest.sh @@ -79,8 +79,6 @@ for i in `seq $VIRT_NUM`; do exit 1 fi - echo "make pxe${i} reboot if installation failing." - sed -i "// a\ " /etc/libvirt/qemu/pxe${i}.xml echo "check pxe${i} state" state=$(virsh domstate pxe${i}) if [[ "$state" == "running" ]]; then @@ -92,6 +90,10 @@ for i in `seq $VIRT_NUM`; do fi fi + echo "make pxe${i} reboot if installation failing." + sed -i "// a\ " /etc/libvirt/qemu/pxe${i}.xml + virsh define /etc/libvirt/qemu/pxe${i}.xml + echo "start pxe${i}" virsh start pxe${i} if [[ "$?" != "0" ]]; then diff --git a/tox.ini b/tox.ini index d05e7b02..5b714197 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = H302,H233,H803,F401 +ignore = H302,H304,H233,H803,F401 show-source = true builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,build