From 0d09e2b7301defd1f408d17373cbf991a922c693 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Wed, 26 Apr 2017 14:14:43 +0800 Subject: Implement the deployment script with python 1. deploy.py: control the work flow of the deployment 2. daisy_server.py: maintain the ssh connection with daisy server and execute ssh commands 3. environment.py: create/find/delete the nodes/vms, install operating system and openstack on nodes 4. libvirt_utils.py: deal with the vm templates and call virsh commands 5. utils.py: some common functions such as file/directory/bash operation Change-Id: I1caa4b0b3118665e15410e8f02bcb6473e5a530b Signed-off-by: Alex Yang --- deploy/daisy_server.py | 264 ++++++++++++++++++++++++++++++++++++++++++++++++ deploy/deploy.py | 212 ++++++++++++++++++++++++++++++++++++++ deploy/environment.py | 262 +++++++++++++++++++++++++++++++++++++++++++++++ deploy/libvirt_utils.py | 211 ++++++++++++++++++++++++++++++++++++++ deploy/utils.py | 138 +++++++++++++++++++++++++ requirements.txt | 4 + test-requirements.txt | 4 + 7 files changed, 1095 insertions(+) create mode 100644 deploy/daisy_server.py create mode 100644 deploy/deploy.py create mode 100644 deploy/environment.py create mode 100644 deploy/libvirt_utils.py create mode 100644 deploy/utils.py diff --git a/deploy/daisy_server.py b/deploy/daisy_server.py new file mode 100644 index 00000000..ec30ec95 --- /dev/null +++ b/deploy/daisy_server.py @@ -0,0 +1,264 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corporation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import os +import paramiko +import scp +import time + +from utils import ( + WORKSPACE, + LD, + LI, + LW, + err_exit, + log_bar, + path_join, + update_config +) + +TIMEOUT = 300 +BLOCK_SIZE = 1024 + + +def log_from_stream(res, data, log_func): + lines = data.splitlines() + res_data = res + if res_data: + lines[0] = res_data + lines[0] + res_data = None + + if not data.endswith("\n"): + res_data = lines[-1] + del (lines[-1]) + for string in lines: + log_func(string) + + if res_data and len(res_data) >= BLOCK_SIZE: + log_func(res_data) + res_data = None + + return res_data + + +LEN_OF_NAME_PART = 50 +LEN_OF_SIZE_PART = 15 + + +def log_scp(filename, size, send): + if size != send: + return + unit = " B" + if size > 1024: + size /= 1024 + unit = " KB" + if size > 1024: + size /= 1024 + unit = " MB" + + name_part = 'SCP: ' + filename + ' ' + size_part = ' ' + str(size) + unit + ' 100%' + if len(name_part) <= LEN_OF_NAME_PART: + LD(name_part.ljust(LEN_OF_NAME_PART, '.') + size_part.rjust(LEN_OF_SIZE_PART, '.')) + else: + LD(name_part) + LD(" ".ljust(LEN_OF_NAME_PART, '.') + size_part.rjust(LEN_OF_SIZE_PART, '.')) + + +class DaisyServer(object): + def __init__(self, name, address, password, remote_dir, bin_file, adapter): + self.name = name + self.address = address + self.password = password + self.remote_dir = remote_dir + self.bin_file = bin_file + self.adapter = adapter + self.ssh_client = None + + def connect(self): + LI('Try to connect to Daisy Server ...') + self.ssh_client = paramiko.SSHClient() + self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + count = 0 + MAX_COUNT = 120 + while count < MAX_COUNT: + try: + self.ssh_client.connect(hostname=self.address, + username='root', + password=self.password, + timeout=TIMEOUT) + except (paramiko.ssh_exception.SSHException, + paramiko.ssh_exception.NoValidConnectionsError): + count += 1 + LD('Attempted SSH connection %d time(s)' % count) + time.sleep(2) + else: + break + if count >= MAX_COUNT: + err_exit('SSH connect to Daisy Server failed') + + LI('SSH connection established') + LI('Try ssh_run: ls -al') + self.ssh_run('ls -al', check=True) + + def close(self): + self.ssh_client.close() + + def ssh_exec_cmd(self, cmd): + stdin, stdout, stderr = self.ssh_client.exec_command(cmd, timeout=TIMEOUT) + response = stdout.read().strip() + error = stderr.read().strip() + + if error: + self.close() + err_exit('SSH client error occurred') + else: + return response + + def ssh_run(self, cmd, check=False, exit_msg='Ssh_run failed'): + transport = self.ssh_client.get_transport() + transport.set_keepalive(1) + session = transport.open_session() + res_data = None + session.exec_command(cmd) + while True: + if session.recv_ready(): + data = session.recv(BLOCK_SIZE) + while data: + res_data = log_from_stream(res_data, data, LI) + data = session.recv(BLOCK_SIZE) + if res_data: + LI(res_data) + res_data = None + + if session.recv_stderr_ready(): + data = session.recv_stderr(BLOCK_SIZE) + while data: + res_data = log_from_stream(res_data, data, LW) + data = session.recv_stderr(BLOCK_SIZE) + if res_data: + LW(res_data) + res_data = None + if session.exit_status_ready(): + break + + status = session.recv_exit_status() + if check and status: + err_exit(exit_msg) + + return status + + def scp_get(self, remote, local='.'): + scp_client = scp.SCPClient(self.ssh_client.get_transport(), + progress=log_scp, + socket_timeout=TIMEOUT) + scp_client.get(remote, local_path=local, recursive=True) + + def scp_put(self, local, remote='.'): + scp_client = scp.SCPClient(self.ssh_client.get_transport(), + progress=log_scp, + socket_timeout=TIMEOUT) + scp_client.put(local, remote_path=remote, recursive=True) + + def create_dir(self, remote_dir): + cmd = 'mkdir -p %s' % remote_dir + self.ssh_exec_cmd(cmd) + + def delete_dir(self, remote_dir): + cmd = 'if [[ -f {DIR} || -d {DIR} ]]; then rm -fr {DIR}; fi'.format(DIR=remote_dir) + self.ssh_exec_cmd(cmd) + + def prepare_files(self): + self.delete_dir(self.remote_dir) + LI('Copy WORKSPACE directory to Daisy Server') + self.scp_put(WORKSPACE, self.remote_dir) + time.sleep(2) + LI('Copy finished') + + self.create_dir('/home/daisy_install') + LI('Write Daisy Server address into daisy.conf') + update_config(path_join(WORKSPACE, 'deploy/daisy.conf'), + 'daisy_management_ip', + self.address, + section='DEFAULT') + LI('Copy daisy.conf to Daisy Server') + self.scp_put(path_join(WORKSPACE, 'deploy/daisy.conf'), '/home/daisy_install/') + + if os.path.dirname(os.path.abspath(self.bin_file)) != WORKSPACE: + LI('Copy opnfv.bin to Daisy Server') + self.scp_put(self.bin_file, path_join(self.remote_dir, 'opnfv.bin')) + + def install_daisy(self): + self.prepare_files() + LI('Begin to install Daisy') + status = self.ssh_run('%s install' % path_join(self.remote_dir, 'opnfv.bin')) + log_bar('Daisy installation completed ! status = %s' % status) + + def prepare_configurations(self): + if self.adapter != 'libvirt': + return + LI('Prepare some configuration files') + cmd = 'bash {script} -n {net_file}'.format( + script=path_join(self.remote_dir, 'deploy/prepare.sh'), + net_file=path_join(self.remote_dir, 'network.yml')) + self.ssh_run(cmd) + + def prepare_cluster(self, deploy_file, net_file): + LI('Copy cluster configuration files to Daisy Server') + self.scp_put(deploy_file, path_join(self.remote_dir, 'deploy.yml')) + self.scp_put(net_file, path_join(self.remote_dir, 'network.yml')) + + self.prepare_configurations() + + LI('Prepare cluster and PXE') + cmd = "python {script} --dha {deploy_file} --network {net_file} --cluster \'yes\'".format( + script=path_join(self.remote_dir, 'deploy/tempest.py'), + deploy_file=path_join(self.remote_dir, 'deploy.yml'), + net_file=path_join(self.remote_dir, 'network.yml')) + self.ssh_run(cmd, check=True) + + def prepare_host_and_pxe(self): + LI('Prepare host and PXE') + cmd = "python {script} --dha {deploy_file} --network {net_file} --host \'yes\' --isbare {is_bare}".format( + script=path_join(self.remote_dir, 'deploy/tempest.py'), + deploy_file=path_join(self.remote_dir, 'deploy.yml'), + net_file=path_join(self.remote_dir, 'network.yml'), + is_bare=1 if self.adapter == 'ipmi' else 0) + self.ssh_run(cmd, check=True) + + def install_virtual_nodes(self): + LI('Daisy install virtual nodes') + cmd = "python {script} --dha {deploy_file} --network {net_file} --install \'yes\'".format( + script=path_join(self.remote_dir, 'deploy/tempest.py'), + deploy_file=path_join(self.remote_dir, 'deploy.yml'), + net_file=path_join(self.remote_dir, 'network.yml')) + self.ssh_run(cmd, check=True) + + def check_os_installation(self, nodes_num): + LI('Check Operating System installation progress') + cmd = '{script} -d {is_bare} -n {nodes_num}'.format( + script=path_join(self.remote_dir, 'deploy/check_os_progress.sh'), + is_bare=1 if self.adapter == 'ipmi' else 0, + nodes_num=nodes_num) + self.ssh_run(cmd, check=True) + + def check_openstack_installation(self, nodes_num): + LI('Check OpenStack installation progress') + cmd = '{script} -n {nodes_num}'.format( + script=path_join(self.remote_dir, 'deploy/check_openstack_progress.sh'), + nodes_num=nodes_num) + self.ssh_run(cmd, check=True) + + def post_deploy(self): + LI('Post deploy ...') + cmd = 'bash {script} -n {net_file}'.format( + script=path_join(self.remote_dir, 'deploy/post.sh'), + net_file=path_join(self.remote_dir, 'network.yml')) + self.ssh_run(cmd, check=False) diff --git a/deploy/deploy.py b/deploy/deploy.py new file mode 100644 index 00000000..23464b5a --- /dev/null +++ b/deploy/deploy.py @@ -0,0 +1,212 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corporation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +############################################################################## +# TODO: +# [ ] 1. specify VM templates (Server, Controller & Compute) in deploy.yml +# [ ] 2. specify network templates in deploy.yml +# [ ] 3. specify adapter(ipmi, libvirt) in deploy.yml +# [ ] 4. get ipmi user/password from PDF (Pod Descriptor File) +# [ ] 5. get pxe bridge from jjb +# [ ] 6. enlarge the vm size of Controller & Compute in deploy.yml +############################################################################## + +import argparse +import yaml + +from utils import ( + WORKSPACE, + save_log_to_file, + LI, + log_bar, + path_join, + check_sudo_privilege, + check_file_exists, + make_file_executable, + confirm_dir_exists +) + +from environment import ( + DaisyEnvironment, +) + + +class DaisyDeployment(object): + def __init__(self, lab_name, pod_name, deploy_file, net_file, bin_file, + daisy_only, cleanup_only, remote_dir, work_dir, storage_dir, + pxe_bridge, deploy_log): + self.lab_name = lab_name + self.pod_name = pod_name + + self.deploy_file = deploy_file + with open(deploy_file) as yaml_file: + self.deploy_struct = yaml.safe_load(yaml_file) + + if not cleanup_only: + self.net_file = net_file + with open(net_file) as yaml_file: + self.net_struct = yaml.safe_load(yaml_file) + else: + self.net_struct = None + + self.bin_file = bin_file + self.daisy_only = daisy_only + self.cleanup_only = cleanup_only + self.remote_dir = remote_dir + self.work_dir = work_dir + self.storage_dir = storage_dir + self.pxe_bridge = pxe_bridge + self.deploy_log = deploy_log + + self.adapter = self._get_adapter_info() + LI('The adapter is %s' % self.adapter) + + # TODO: modify the jjb code to provide bridge name + if self.adapter == 'libvirt': + self.pxe_bridge = 'daisy1' + else: + self.pxe_bridge = 'br7' + + self.daisy_server_info = self._get_daisy_server_info() + + self.daisy_env = DaisyEnvironment(self.deploy_struct, + self.net_struct, + self.adapter, + self.pxe_bridge, + self.daisy_server_info, + self.work_dir, + self.storage_dir) + + def _get_adapter_info(self): + # TODO: specify the adapter info in deploy.yml + if 'adapter' in self.deploy_struct: + return self.deploy_struct['adapter'] + elif self.pod_name and 'virtual' in self.pod_name: + return 'libvirt' + else: + return 'ipmi' + + def _get_daisy_server_info(self): + address = self.deploy_struct.get('daisy_ip', '10.20.11.2') + gateway = self.deploy_struct.get('daisy_gateway', '10.20.11.1') + password = self.deploy_struct.get('daisy_passwd', 'r00tme') + disk_size = self.deploy_struct.get('disks', {'daisy': 50})['daisy'] + # TODO: get VM name of daisy server from deploy.yml or vm template + name = 'daisy' + image = path_join(self.storage_dir, name + '.qcow2') + + return {'name': name, + 'image': image, + 'address': address, + 'gateway': gateway, + 'password': password, + 'disk_size': disk_size} + + def run(self): + self.daisy_env.delete_old_environment() + if self.cleanup_only: + return + self.daisy_env.create_daisy_server() + if self.daisy_only: + log_bar('Create Daisy Server successfully !') + return + self.daisy_env.install_daisy(self.remote_dir, self.bin_file) + self.daisy_env.deploy(self.deploy_file, self.net_file) + log_bar('Daisy deploy successfully !') + + +def config_arg_parser(): + parser = argparse.ArgumentParser() + + parser.add_argument('-lab', dest='lab_name', action='store', nargs='?', + default=None, + help='Lab Name') + parser.add_argument('-pod', dest='pod_name', action='store', nargs='?', + default=None, + help='Pod Name') + + parser.add_argument('-bin', dest='bin_file', action='store', nargs='?', + default=path_join(WORKSPACE, 'opnfv.bin'), + help='OPNFV Daisy BIN File') + + parser.add_argument('-do', dest='daisy_only', action='store_true', + default=False, + help='Install Daisy Server only') + parser.add_argument('-co', dest='cleanup_only', action='store_true', + default=False, + help='Cleanup VMs and Virtual Networks') + # parser.add_argument('-nd', dest='no_daisy', action='store_true', + # default=False, + # help='Do not install Daisy Server when it exists') + + parser.add_argument('-rdir', dest='remote_dir', action='store', nargs='?', + default='/home/daisy', + help='Code directory on Daisy Server') + + parser.add_argument('-wdir', dest='work_dir', action='store', nargs='?', + default='/tmp/workdir', + help='Temporary working directory') + parser.add_argument('-sdir', dest='storage_dir', action='store', nargs='?', + default='/home/qemu/vms', + help='Storage directory for VM images') + parser.add_argument('-B', dest='pxe_bridge', action='store', nargs='?', + default='pxebr', + help='Linux Bridge for booting up the Daisy Server VM ' + '[default: pxebr]') + parser.add_argument('-log', dest='deploy_log', action='store', nargs='?', + default=path_join(WORKSPACE, 'deploy.log'), + help='Path and name of the deployment log file') + return parser + + +def parse_arguments(): + parser = config_arg_parser() + args = parser.parse_args() + + save_log_to_file(args.deploy_log) + LI(args) + + conf_base_dir = path_join(WORKSPACE, 'labs', args.lab_name, args.pod_name) + deploy_file = path_join(conf_base_dir, 'daisy/config/deploy.yml') + net_file = path_join(conf_base_dir, 'daisy/config/network.yml') + + check_file_exists(deploy_file) + if not args.cleanup_only: + check_file_exists(net_file) + make_file_executable(args.bin_file) + + confirm_dir_exists(args.work_dir) + confirm_dir_exists(args.storage_dir) + + kwargs = { + 'lab_name': args.lab_name, + 'pod_name': args.pod_name, + 'deploy_file': deploy_file, + 'net_file': net_file, + 'bin_file': args.bin_file, + 'daisy_only': args.daisy_only, + 'cleanup_only': args.cleanup_only, + 'remote_dir': args.remote_dir, + 'work_dir': args.work_dir, + 'storage_dir': args.storage_dir, + 'pxe_bridge': args.pxe_bridge, + 'deploy_log': args.deploy_log + } + return kwargs + + +def main(): + check_sudo_privilege() + kwargs = parse_arguments() + deploy = DaisyDeployment(**kwargs) + deploy.run() + + +if __name__ == '__main__': + main() diff --git a/deploy/environment.py b/deploy/environment.py new file mode 100644 index 00000000..088e5008 --- /dev/null +++ b/deploy/environment.py @@ -0,0 +1,262 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corporation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import os +import shutil +import time + +from daisy_server import ( + DaisyServer +) +from libvirt_utils import ( + create_virtual_disk, + create_vm, + reboot_vm, + delete_vm_and_disk, + create_virtual_network, + delete_virtual_network +) +from utils import ( + WORKSPACE, + LI, + LW, + err_exit, + run_shell, + path_join, + ipmi_reboot_node, +) + +CREATE_QCOW2_PATH = path_join(WORKSPACE, 'tools') + +VMDEPLOY_DAISY_SERVER_NET = path_join(WORKSPACE, 'templates/virtual_environment/networks/daisy.xml') +VMDEPLOY_TARGET_NODE_NET = path_join(WORKSPACE, 'templates/virtual_environment/networks/os-all_in_one.xml') +VMDEPLOY_DAISY_SERVER_VM = path_join(WORKSPACE, 'templates/virtual_environment/vms/daisy.xml') + +BMDEPLOY_DAISY_SERVER_VM = path_join(WORKSPACE, 'templates/physical_environment/vms/daisy.xml') + +ALL_IN_ONE_TEMPLATE = path_join(WORKSPACE, 'templates/virtual_environment/vms/all_in_one.xml') +CONTROLLER_TEMPLATE = path_join(WORKSPACE, 'templates/virtual_environment/vms/controller01.xml') +COMPUTE_TEMPLATE = path_join(WORKSPACE, 'templates/virtual_environment/vms/computer01.xml') +VIRT_NET_TEMPLATE_PATH = path_join(WORKSPACE, 'templates/virtual_environment/networks') + + +class DaisyEnvironment(object): + def __new__(cls, deploy_struct, net_struct, adapter, pxe_bridge, + daisy_server_info, work_dir, storage_dir): + if adapter == 'libvirt': + return VirtualEnvironment(deploy_struct, net_struct, + adapter, pxe_bridge, + daisy_server_info, work_dir, storage_dir) + else: + return BareMetalEnvironment(deploy_struct, net_struct, + adapter, pxe_bridge, + daisy_server_info, work_dir, storage_dir) + + +class DaisyEnvironmentBase(object): + def __init__(self, deploy_struct, net_struct, adapter, pxe_bridge, + daisy_server_info, work_dir, storage_dir): + self.deploy_struct = deploy_struct + self.net_struct = net_struct + self.adapter = adapter + self.pxe_bridge = pxe_bridge + self.work_dir = work_dir + self.storage_dir = storage_dir + self.daisy_server_info = daisy_server_info + self.server = None + LI('Daisy Environment Initialized') + + def delete_daisy_server(self): + delete_vm_and_disk(self.daisy_server_info['name']) + + def create_daisy_server_image(self): + LI('Begin to create Daisy Server image') + script = path_join(CREATE_QCOW2_PATH, 'daisy-img-modify.sh') + sub_script = path_join(CREATE_QCOW2_PATH, 'centos-img-modify.sh') + cmd = '{script} -c {sub_script} -a {address} -g {gateway} -s {disk_size}'.format( + script=script, + sub_script=sub_script, + address=self.daisy_server_info['address'], + gateway=self.daisy_server_info['gateway'], + disk_size=self.daisy_server_info['disk_size']) + LI('Command is: ') + LI(' %s' % cmd) + # status, output = commands.getstatusoutput(cmd) + status = run_shell(cmd) + if status: + err_exit('Failed to create Daisy Server image') + if os.access(self.daisy_server_info['image'], os.R_OK): + os.remove(self.daisy_server_info['image']) + image = path_join(self.work_dir, 'daisy/centos7.qcow2') + shutil.move(image, self.daisy_server_info['image']) + LI('Daisy Server image is created %s' % self.daisy_server_info['image']) + + def install_daisy(self, remote_dir, bin_file): + self.server = DaisyServer(self.daisy_server_info['name'], + self.daisy_server_info['address'], + self.daisy_server_info['password'], + remote_dir, + bin_file, + self.adapter) + self.server.connect() + self.server.install_daisy() + + +class BareMetalEnvironment(DaisyEnvironmentBase): + def delete_old_environment(self): + LW('Begin to delete old environment !') + self.delete_daisy_server() + LW('Old environment cleanup finished !') + + def create_daisy_server(self): + self.create_daisy_server_image() + self.create_daisy_server_vm() + + def create_daisy_server_vm(self): + # TODO: refactor the structure of deploy.yml, add VM template param of Daisy Server + # add self.pxe_bridge into the vm template + if 'template' in self.deploy_struct: + # get VM name of Daisy Server from the template + template = self.deploy_struct['template'] + else: + template = BMDEPLOY_DAISY_SERVER_VM + + create_vm(template, + name=self.daisy_server_info['name'], + disk_file=self.daisy_server_info['image']) + + def reboot_nodes(self, boot_dev=None): + # TODO: add ipmi info into deploy.yml, or read from PDF + address = 106 + for node in self.deploy_struct['hosts']: + node['ipmiIp'] = '192.168.1.' + str(address) + address += 1 + if address > 111: + err_exit('the ipmi address exceeds the range 106~110') + node['ipmiUser'] = 'zteroot' + node['ipmiPass'] = 'superuser' + ipmi_reboot_node(node['ipmiIp'], node['ipmiUser'], + node['ipmiPass'], boot_source=boot_dev) + + def deploy(self, deploy_file, net_file): + self.server.prepare_cluster(deploy_file, net_file) + self.reboot_nodes(boot_dev='pxe') + self.server.prepare_host_and_pxe() + + LI('The hosts number is %d' % len(self.deploy_struct['hosts'])) + self.server.check_os_installation(len(self.deploy_struct['hosts'])) + time.sleep(10) + self.server.check_openstack_installation(len(self.deploy_struct['hosts'])) + + +class VirtualEnvironment(DaisyEnvironmentBase): + def create_daisy_server_network(self): + net_name = create_virtual_network(VMDEPLOY_DAISY_SERVER_NET) + if net_name != self.pxe_bridge: + self.delete_virtual_network(VMDEPLOY_DAISY_SERVER_NET) + err_exit('Network name %s is wrong, pxe bridge is %s' % (net_name, self.pxe_bridge)) + + def create_daisy_server_vm(self): + # TODO: refactor the structure of deploy.yml, add VM template param of Daisy Server + # add self.pxe_bridge into the vm template + if 'template' in self.deploy_struct: + # get VM name of Daisy Server from the template + template = self.deploy_struct['template'] + else: + template = VMDEPLOY_DAISY_SERVER_VM + + create_vm(template, + name=self.daisy_server_info['name'], + disk_file=self.daisy_server_info['image']) + + def create_daisy_server(self): + self.create_daisy_server_image() + self.create_daisy_server_network() + self.create_daisy_server_vm() + + def create_virtual_node(self, node): + name = node['name'] + roles = node['roles'] + controller_size = self.deploy_struct.get('disks', {'controller': 200}).get('controller') + compute_size = self.deploy_struct.get('disks', {'compute': 200}).get('compute') + LI('Begin to create virtual node %s, roles %s' % (name, roles)) + + if 'CONTROLLER_LB' in roles: + size = controller_size + if 'COMPUTER' in roles: + size = compute_size if compute_size > controller_size else controller_size + template = ALL_IN_ONE_TEMPLATE + else: + template = CONTROLLER_TEMPLATE + else: + size = compute_size + template = COMPUTE_TEMPLATE + + if 'template' in node: + template = node['template'] + disk_file = path_join(self.storage_dir, name + '.qcow2') + # TODO: modify the sizes in deploy.yml to more than 100G + if size < 200: + size = 200 + create_virtual_disk(disk_file, size) + create_vm(template, name, disk_file) + + def create_nodes(self): + # TODO: support virtNetTemplatePath in deploy.yml + # and multi interfaces, not only all-in-one + create_virtual_network(VMDEPLOY_TARGET_NODE_NET) + for node in self.deploy_struct['hosts']: + self.create_virtual_node(node) + time.sleep(20) + + def reboot_nodes(self, boot_devs=None): + for node in self.deploy_struct['hosts']: + reboot_vm(node['name'], boot_devs=boot_devs) + + def delete_nodes(self): + for host in self.deploy_struct['hosts']: + delete_vm_and_disk(host['name']) + + def delete_networks(self): + if 'virtNetTemplatePath' in self.deploy_struct: + path = self.deploy_struct['virtNetTemplatePath'] + else: + path = VIRT_NET_TEMPLATE_PATH + + if not os.path.isdir(path): + LW('Cannot find the virtual network template path %s' % path) + return + for f in os.listdir(path): + f = path_join(path, f) + if os.path.isfile(f): + delete_virtual_network(f) + + def delete_old_environment(self): + LW('Begin to delete old environment !') + self.delete_nodes() + self.delete_daisy_server() + self.delete_networks() + LW('Old environment cleanup finished !') + + def deploy(self, deploy_file, net_file): + self.server.prepare_cluster(deploy_file, net_file) + self.create_nodes() + self.server.prepare_host_and_pxe() + LI('Begin Daisy virtual-deploy os and openstack') + self.reboot_nodes() + LI('Sleep 20s to wait the VM(s) startup') + time.sleep(20) + self.server.install_virtual_nodes() + + LI('The hosts number is %d' % len(self.deploy_struct['hosts'])) + self.server.check_os_installation(len(self.deploy_struct['hosts'])) + time.sleep(10) + self.reboot_nodes(boot_devs=['hd']) + self.server.check_openstack_installation(len(self.deploy_struct['hosts'])) + self.server.post_deploy() diff --git a/deploy/libvirt_utils.py b/deploy/libvirt_utils.py new file mode 100644 index 00000000..cd203784 --- /dev/null +++ b/deploy/libvirt_utils.py @@ -0,0 +1,211 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corporation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import commands +import libvirt +import os +import xml.etree.ElementTree as ET + +from utils import ( + LI, + LE, + LW, + WORKSPACE, + err_exit, + path_join +) + + +def get_nets_name(root): + nets = [] + for interface in root.findall('./devices/interface'): + if 'type' in interface.attrib and interface.attrib['type'] == 'network': + for source in interface.iterfind('source'): + if 'network' in source.attrib: + nets.append(source.attrib['network']) + return nets + + +def modify_vm_boot_order(root, boot_devs): + os_elem = root.find('os') + for boot_elem in os_elem.findall('boot'): + os_elem.remove(boot_elem) + for boot_dev in boot_devs: + boot_elem = ET.Element('boot', attrib={'dev': boot_dev}) + os_elem.append(boot_elem) + return root + + +def modify_vm_name(root, vm_name): + name_elem = root.find('./name') + name_elem.text = vm_name + + +def modify_vm_disk_file(root, disk_file): + for disk in root.findall('./devices/disk'): + if 'device' in disk.attrib and disk.attrib['device'] == 'disk': + for source in disk.iterfind('source'): + if 'file' in source.attrib: + source.attrib['file'] = disk_file + break + + +def create_virtual_disk(disk_file, size): + LI('Create virtual disk file %s size %d GB' % (disk_file, size)) + cmd = 'qemu-img create -f qcow2 {disk_file} {size}G'.format( + disk_file=disk_file, size=size) + status, output = commands.getstatusoutput(cmd) + if status: + LE(output) + err_exit('Fail to create qemu image !') + + +def create_vm(template, name=None, disk_file=None): + LI('Begin to create VM %s' % template) + + if name or disk_file: + tree = ET.ElementTree(file=template) + root = tree.getroot() + if name: + modify_vm_name(root, name) + if disk_file: + modify_vm_disk_file(root, disk_file) + + temp_file = path_join(WORKSPACE, 'tmp.xml') + tree.write(temp_file) + output = commands.getoutput('cat %s' % temp_file) + os.remove(temp_file) + else: + output = commands.getoutput('cat %s' % template) + + conn = libvirt.open('qemu:///system') + domain = conn.defineXML(output) + if domain is None: + err_exit('Failed to define VM %s' % template) + if domain.create() < 0: + err_exit('Failed to start VM %s' % template) + domain.setAutostart(1) + + LI('VM %s is started' % domain.name()) + return + + +def reboot_vm(vm_name, boot_devs=None): + LI('Begin to reboot VM %s', vm_name) + conn = libvirt.open('qemu:///system') + try: + vm = conn.lookupByName(vm_name) + except libvirt.libvirtError as e: + LE(e) + err_exit('VM %s is not found: ' % vm_name) + + if boot_devs: + if vm.isActive(): + vm.destroy() + LI('Destroy VM %s' % vm_name) + + # root = ET.fromstring(vm.XMLDesc()) + temp_file = path_join(WORKSPACE, 'tmp.xml') + commands.getoutput('virsh dumpxml %s > %s' % (vm_name, temp_file)) + tree = ET.parse(temp_file) + root = tree.getroot() + LI('Modify the boot order %s' % boot_devs) + modify_vm_boot_order(root, boot_devs) + tree.write(temp_file) + + LI('Re-define and start the VM %s' % vm_name) + vm.undefine() + vm = conn.defineXML(commands.getoutput('cat %s' % temp_file)) + vm.create() + vm.setAutostart(1) + else: + vm.reset() + + conn.close() + + +def get_disk_file(root): + disks = [] + for disk in root.findall('./devices/disk'): + if 'device' in disk.attrib and disk.attrib['device'] == 'disk': + for source in disk.iterfind('source'): + if 'file' in source.attrib: + disks.append(source.attrib['file']) + return disks + + +def delete_vm_and_disk(vm_name): + LI('Begin to delete VM %s', vm_name) + conn = libvirt.open('qemu:///system') + vm = None + for item in conn.listAllDomains(): + if vm_name == item.name(): + vm = item + break + if vm is None: + conn.close() + LI('VM %s is not found' % vm_name) + return + + output = vm.XMLDesc() + root = ET.fromstring(output) + + if vm.isActive(): + vm.destroy() + LI('Destroy VM %s' % vm.name()) + vm.undefine() + + for disk_file in get_disk_file(root): + if os.path.isfile(disk_file): + status, output = commands.getstatusoutput('rm -f %s' % disk_file) + if status: + LW('Failed to delete the VM disk file %s' % disk_file) + + conn.close() + LI('VM %s is removed' % vm_name) + + +def create_virtual_network(template): + LI('Begin to create virtual network %s' % template) + output = commands.getoutput('cat %s' % template) + conn = libvirt.open('qemu:///system') + network = conn.networkDefineXML(output) + if network is None: + err_exit('Failed to define a virtual network %s' % template) + + network.create() # set the network active + network.setAutostart(1) + conn.close() + LI('Virtual network %s is created' % network.name()) + return network.name() + + +def delete_virtual_network(network_xml): + LI('Begin to find and delete network %s' % network_xml) + tree = ET.ElementTree(file=network_xml) + root = tree.getroot() + names = root.findall('./name') + assert len(names) == 1 + name = names[0].text + + result = 0 + conn = libvirt.open('qemu:///system') + + for net in conn.listAllNetworks(): + if name == net.name(): + if net.isActive(): + net.destroy() + LI('Network %s is destroyed' % name) + net.undefine() + LI('Network %s is deleted' % name) + result = 1 + break + conn.close() + if not result: + LI('Network %s is not found' % name) diff --git a/deploy/utils.py b/deploy/utils.py new file mode 100644 index 00000000..0c5b1370 --- /dev/null +++ b/deploy/utils.py @@ -0,0 +1,138 @@ +############################################################################## +# Copyright (c) 2017 ZTE Corporation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import commands +from configobj import ConfigObj +import os +import logging +import subprocess +import sys + + +path_join = os.path.join +CWD = os.getcwd() +WORKSPACE = os.path.normpath(path_join(os.path.dirname(__file__), '..')) +BASE = CWD + + +def get_logger(): + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +LOG = get_logger() +LD = LOG.debug +LI = LOG.info +LW = LOG.warn +LE = LOG.error + + +def save_log_to_file(log_file): + with open(log_file, 'w+'): + pass + + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + handler = logging.FileHandler(log_file, mode='w') + handler.setFormatter(formatter) + LOG.addHandler(handler) + + +def err_exit(message): + LE('%s\n' % message) + sys.exit(1) + + +def log_bar(message, log_func=LI): + bar = '=' * len(message) + log_func(bar) + log_func(message) + log_func(bar) + + +def check_sudo_privilege(): + uid = os.getuid() + if uid != 0: + err_exit('You need run this script with sudo privilege') + + +def check_file_exists(file_path): + if not os.path.dirname(file_path): + file_path = os.path.normpath(path_join(BASE, file_path)) + if not os.access(file_path, os.R_OK): + err_exit('File %s not found\n' % file_path) + + +def make_file_executable(file_path): + if not os.path.isdir(file_path): + file_path = os.path.normpath(path_join(BASE, file_path)) + if not os.access(file_path, os.R_OK): + err_exit('File %s not found\n' % file_path) + if not os.access(file_path, os.X_OK): + LW('File %s is not executable, chmod it and continue' % file_path) + status, output = commands.getstatusoutput('chmod +x %s' % file_path) + if status: + err_exit('Cannot change the file mode of %s' % file_path) + + +def confirm_dir_exists(dir_path): + if not os.path.isdir(dir_path): + LI('Creating directory %s' % dir_path) + os.makedirs(dir_path) + + +def update_config(conf_file, key, value, section='DEFAULT'): + LI('Update_config [ %s : %s ] to file: %s' % (key, value, conf_file)) + config = ConfigObj(conf_file) + config[section][key] = value + config.write() + + +def ipmi_reboot_node(host, user, passwd, boot_source=None): + prefix = 'ipmitool -I lanplus -H {host} -U {user} -P {passwd} -R 1 '.format( + host=host, user=user, passwd=passwd) + if boot_source: + cmd = prefix + 'chassis bootdev {boot_source}'.format(boot_source=boot_source) + LI('IMPI set node %s boot from %s' % (host, boot_source)) + status, output = commands.getstatusoutput(cmd) + if status: + err_exit('IPMI command failed: %s' % output) + + cmd = prefix + 'chassis power reset' + LI('IPMI reset node %s' % host) + status, output = commands.getstatusoutput(cmd) + if status: + err_exit('IPMI command failed: %s' % output) + + +def run_shell(cmd, check=False): + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True) + while process.poll() is None: + LD(process.stdout.readline().strip()) + + response, stderr = process.communicate() + return_code = process.returncode + + if check: + if return_code > 0: + stderr = stderr.strip() + LE('Failed command: ' + str(cmd)) + LE('Command returned error: ' + str(stderr)) + err_exit('Command return code: ' + str(return_code)) + else: + LI('Successful command: ' + str(cmd)) + + return return_code diff --git a/requirements.txt b/requirements.txt index e69de29b..7cf1ddc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,4 @@ +configobj +paramiko +pyyaml +scp diff --git a/test-requirements.txt b/test-requirements.txt index 0483907e..ea421d4a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,7 @@ +configobj +paramiko +pyyaml +scp pytest pytest-cov pytest-faker -- cgit 1.2.3-korg