From 29d8730686b539af7252e54f10b1bf19be3423e5 Mon Sep 17 00:00:00 2001 From: Szilard Cserey Date: Tue, 7 Apr 2015 10:29:42 +0200 Subject: [PATCH] Automatic Deployment - configure environment - configure initial settings - configure networking - hardware adapter for HP - fuel VM deployment JIRA: [BGS-2] Create Fuel deployment scrip Change-Id: If305477833c54547efe7e6ebfddafde0ab31ebd2 Signed-off-by: Szilard Cserey --- fuel/ci/deploy.sh | 13 +- fuel/deploy/common.py | 29 ++ fuel/deploy/configure_environment.py | 70 ++++ fuel/deploy/configure_network.py | 91 +++++ fuel/deploy/configure_settings.py | 88 +++++ fuel/deploy/dea.py | 42 ++- fuel/deploy/dea.yaml | 35 +- fuel/deploy/deploy.py | 112 +++--- fuel/deploy/deploy_fuel.sh | 106 ++++++ fuel/deploy/dha.py | 41 +- fuel/deploy/hardware_adapters/hp/hp_adapter.py | 411 +++++++++++++++++++++ fuel/deploy/hardware_adapters/hp/run_oa_command.py | 113 ++++++ 12 files changed, 1061 insertions(+), 90 deletions(-) create mode 100644 fuel/deploy/common.py create mode 100644 fuel/deploy/configure_environment.py create mode 100644 fuel/deploy/configure_network.py create mode 100644 fuel/deploy/configure_settings.py create mode 100755 fuel/deploy/deploy_fuel.sh create mode 100644 fuel/deploy/hardware_adapters/hp/hp_adapter.py create mode 100644 fuel/deploy/hardware_adapters/hp/run_oa_command.py diff --git a/fuel/ci/deploy.sh b/fuel/ci/deploy.sh index d11c65a..df23249 100755 --- a/fuel/ci/deploy.sh +++ b/fuel/ci/deploy.sh @@ -1,3 +1,12 @@ -# To be able to deploy on a certain metal environment there needs to be a Deployment Environment Adaptor executable" -# properly added to $PATH such that deploy.sh can call it by $dea [options] as indicated by ./deploy -h. +#!/bin/bash -x +set -o xtrace +set -o errexit +set -o nounset +set -o pipefail +WORKSPACE=$(readlink -e ..) +ISO_LOCATION="$(readlink -f $(find $WORKSPACE -iname 'fuel*iso' -type f))" +INTERFACE="fuel" + +cd "${WORKSPACE}/deploy" +./deploy_fuel.sh "$ISO_LOCATION" $INTERFACE 2>&1 | tee deploy_fuel.log diff --git a/fuel/deploy/common.py b/fuel/deploy/common.py new file mode 100644 index 0000000..cd5085c --- /dev/null +++ b/fuel/deploy/common.py @@ -0,0 +1,29 @@ +import subprocess +import sys + + +N = {'id': 0, 'status': 1, 'name': 2, 'cluster': 3, 'ip': 4, 'mac': 5, + 'roles': 6, 'pending_roles': 7, 'online': 8} +E = {'id': 0, 'status': 1, 'name': 2, 'mode': 3, 'release_id': 4, + 'changes': 5, 'pending_release_id': 6} +R = {'id': 0, 'name': 1, 'state': 2, 'operating_system': 3, 'version': 4} +RO = {'name': 0, 'conflicts': 1} + +def exec_cmd(cmd): + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True) + return process.communicate()[0] + +def parse(printout): + parsed_list = [] + lines = printout.splitlines() + for l in lines[2:]: + parsed = [e.strip() for e in l.split('|')] + parsed_list.append(parsed) + return parsed_list + +def err(error_message): + sys.stderr.write(error_message) + sys.exit(1) diff --git a/fuel/deploy/configure_environment.py b/fuel/deploy/configure_environment.py new file mode 100644 index 0000000..9aca904 --- /dev/null +++ b/fuel/deploy/configure_environment.py @@ -0,0 +1,70 @@ +import common +import os +import shutil +import yaml + + +from configure_settings import ConfigureSettings +from configure_network import ConfigureNetwork + +N = common.N +E = common.E +R = common.R +RO = common.RO +exec_cmd = common.exec_cmd +parse = common.parse +err = common.err + +class ConfigureEnvironment(object): + + def __init__(self, dea, yaml_config_dir): + self.env_id = None + self.dea = dea + self.yaml_config_dir = yaml_config_dir + self.env_name = dea.get_environment_name() + + def env_exists(self, env_name): + env_list = parse(exec_cmd('fuel env --list')) + for env in env_list: + if env[E['name']] == env_name and env[E['status']] == 'new': + return True + return False + + def get_env_id(self, env_name): + env_list = parse(exec_cmd('fuel env --list')) + for env in env_list: + if env[E['name']] == env_name: + return env[E['id']] + + def configure_environment(self, dea): + exec_cmd('fuel env -c --name %s --release %s --mode ha --net neutron ' + '--nst vlan' % (self.env_name, + self.supported_release[R['id']])) + + self.env_id = self.get_env_id(self.env_name) + if not self.env_exists(self.env_name): + err("Failed to create environment %s" % self.env_name) + + self.config_settings() + self.config_network() + + def config_settings(self): + if os.path.exists(self.yaml_config_dir): + shutil.rmtree(self.yaml_config_dir) + os.makedirs(self.yaml_config_dir) + + settings = ConfigureSettings(self.yaml_config_dir, self.env_id) + settings.config_settings() + + + def config_network(self): + network_yaml=self.yaml_config_dir + '/network_%s.yaml' % self.env_id + os.remove(network_yaml) + + network = ConfigureNetwork(self.yaml_config_dir, network_yaml, + self.env_id, self.dea) + network.config_network() + + + + diff --git a/fuel/deploy/configure_network.py b/fuel/deploy/configure_network.py new file mode 100644 index 0000000..0b298e5 --- /dev/null +++ b/fuel/deploy/configure_network.py @@ -0,0 +1,91 @@ +import common +import os +import yaml +import io +import re + +N = common.N +E = common.E +R = common.R +RO = common.RO +exec_cmd = common.exec_cmd +parse = common.parse +err = common.err + +P1 = re.compile('!\s.*') + +class ConfigureNetwork(object): + + def __init__(self, yaml_config_dir, network_yaml, env_id, dea): + self.yaml_config_dir = yaml_config_dir + self.network_yaml = network_yaml + self.env_id = env_id + self.dea = dea + + def download_settings(self): + exec_cmd('fuel network --env %s --download --dir %s' + % (self.env_id, self.yaml_config_dir)) + + def upload_settings(self): + exec_cmd('fuel network --env %s --upload --dir %s' + % (self.env_id, self.yaml_config_dir)) + + def config_network(self): + + self.download_settings() + + self.apply_network_config() + + self.upload_settings() + + self.verify() + + def apply_network_config(self): + + with io.open(self.network_yaml) as stream: + network_config = yaml.load(stream) + networks = network_config['networks'] + + net = self.dea.get_networks() + net['fuelweb_admin'] = net['management'] + if 'vlan' in net['fuelweb_admin']: + del net['fuelweb_admin']['vlan'] + del net['management'] + net_names = [n for n in net.iterkeys()] + + for i in range(len(networks)): + if networks[i]['name'] == 'management': + networks = networks[:i] + networks[i+1:] + network_config['networks'] = networks + break + + for network in networks: + name = network['name'] + if name in net_names: + if ('vlan' in net[name] and net[name]['vlan'] is not None): + network['vlan_start'] = net[name]['vlan'] + network['cidr'] = net[name]['cidr'] + network['ip_ranges'][0][0] = net[name]['start'] + network['ip_ranges'][0][1] = net[name]['end'] + + with io.open(self.network_yaml, 'w') as stream: + yaml.dump(network_config, stream, default_flow_style=False) + + def verify(self): + ret = exec_cmd('mktemp -d') + temp_dir = ret.splitlines()[0] + + exec_cmd('fuel network --env %s --download --dir %s' + % (self.env_id, temp_dir)) + + ret = exec_cmd('diff -C0 %s %s' + % (self.network_yaml, + temp_dir + '/network_%s.yaml' % self.env_id)) + diff_list = [] + for l in ret.splitlines(): + m = P1.match(l) + if m and '_vip' not in l: + diff_list.append(l) + if diff_list: + err('Uploaded network yaml rejected by Fuel\n') + \ No newline at end of file diff --git a/fuel/deploy/configure_settings.py b/fuel/deploy/configure_settings.py new file mode 100644 index 0000000..cdeea49 --- /dev/null +++ b/fuel/deploy/configure_settings.py @@ -0,0 +1,88 @@ +import common +import os +import yaml +import io +import re + +N = common.N +E = common.E +R = common.R +RO = common.RO +exec_cmd = common.exec_cmd +parse = common.parse +err = common.err + +class ConfigureSettings(object): + + def __init__(self, yaml_config_dir, env_id): + self.yaml_config_dir = yaml_config_dir + self.env_id = env_id + + def download_settings(self): + exec_cmd('fuel --env %s settings --download' % self.env_id) + + def upload_settings(self): + exec_cmd('fuel --env %s settings --upload' % self.env_id) + + + def config_settings(self): + self.download_settings() + self.modify_settings() + self.upload_settings() + + # Fix console speed + def fix_console_speed(data): + # First remove all console= from the kernel cmdline + cmdline = data["editable"]["kernel_params"]["kernel"]["value"] + pat = re.compile(r"console=[\w,]+\s+") + repl = 1 + while repl != 0: + cmdline, repl = pat.subn("", cmdline) + + # Then add the console info we want + cmdline = re.sub(r"^", "console=tty0 console=ttyS0,115200 ", cmdline) + data["editable"]["kernel_params"]["kernel"]["value"] = cmdline + + # Initialize kernel audit + def initialize_kernel_audit(data): + cmdline = data["editable"]["kernel_params"]["kernel"]["value"] + cmdline = "audit=1 " + cmdline + data["editable"]["kernel_params"]["kernel"]["value"] = cmdline + + # Add crashkernel parameter to boot parameters. W/o this we can't + # make crash dumps after initial deploy. Standard grub setup will add + # crashkernel= options - with bad values but that is another issue - but + # that only enables crash dumps after first reboot + def add_crashkernel_support(data): + cmdline = data["editable"]["kernel_params"]["kernel"]["value"] + cmdline += " crashkernel=256M" + data["editable"]["kernel_params"]["kernel"]["value"] = cmdline + + + def modify_settings(self): + + filename = "%s/settings_%d.yaml" % (self.yaml_config_dir, self.env_id) + if not os.path.isfile(filename): + err("Failed to find %s\n" % filename) + + with io.open(filename) as stream: + data = yaml.load(stream) + + self.fix_console_speed(data) + + self.initialize_kernel_audit(data) + + self.add_crashkernel_support(data) + + # Make sure we have the correct libvirt type + data["editable"]["common"]["libvirt_type"]["value"] = "kvm" + + + # Save the settings into the file from which we loaded them + with io.open(filename, "w") as stream: + yaml.dump(data, stream, default_flow_style=False) + + + + + diff --git a/fuel/deploy/dea.py b/fuel/deploy/dea.py index 0ab215d..5f306a2 100644 --- a/fuel/deploy/dea.py +++ b/fuel/deploy/dea.py @@ -1,16 +1,19 @@ import yaml +import io class DeploymentEnvironmentAdapter(object): def __init__(self): self.dea_struct = None - self.blade_ids = {} - self.blades = {} + self.blade_ids_per_shelves = {} + self.blades_per_shelves = {} self.shelf_ids = [] + self.networks = {} def parse_yaml(self, yaml_path): - with open(yaml_path) as yaml_file: + with io.open(yaml_path) as yaml_file: self.dea_struct = yaml.load(yaml_file) self.collect_shelf_and_blade_info() + self.collect_network_info() def get_no_of_blades(self): no_of_blades = 0 @@ -19,7 +22,13 @@ class DeploymentEnvironmentAdapter(object): return no_of_blades def get_server_type(self): - return self.dea_struct['server_type'] + return self.dea_struct['server']['type'] + + def get_server_info(self): + return (self.dea_struct['server']['type'], + self.dea_struct['server']['mgmt_ip'], + self.dea_struct['server']['username'], + self.dea_struct['server']['password']) def get_environment_name(self): return self.dea_struct['name'] @@ -27,17 +36,20 @@ class DeploymentEnvironmentAdapter(object): def get_shelf_ids(self): return self.shelf_ids - def get_blade_ids(self, shelf_id): - return self.blade_ids[shelf_id] + def get_blade_ids_per_shelf(self, shelf_id): + return self.blade_ids_per_shelves[shelf_id] + + def get_blade_ids_per_shelves(self): + return self.blade_ids_per_shelves def collect_shelf_and_blade_info(self): - self.blade_ids = {} - self.blades = {} + self.blade_ids_per_shelves = {} + self.blades_per_shelves = {} self.shelf_ids = [] for shelf in self.dea_struct['shelf']: self.shelf_ids.append(shelf['id']) - blade_ids = self.blade_ids[shelf['id']] = [] - blades = self.blades[shelf['id']] = {} + blade_ids = self.blade_ids_per_shelves[shelf['id']] = [] + blades = self.blades_per_shelves[shelf['id']] = {} for blade in shelf['blade']: blade_ids.append(blade['id']) blades[blade['id']] = blade @@ -49,4 +61,12 @@ class DeploymentEnvironmentAdapter(object): def is_compute_host(self, shelf_id, blade_id): blade = self.blades[shelf_id][blade_id] - return True if 'role' not in blade else False \ No newline at end of file + return True if 'role' not in blade else False + + def collect_network_info(self): + self.networks = {} + for network in self.dea_struct['network']: + self.networks[network['name']] = network + + def get_networks(self): + return self.networks \ No newline at end of file diff --git a/fuel/deploy/dea.yaml b/fuel/deploy/dea.yaml index 5ade83f..420dae7 100644 --- a/fuel/deploy/dea.yaml +++ b/fuel/deploy/dea.yaml @@ -1,6 +1,10 @@ --- name: ENV-1 -server_type: esxi +server: + type: hp + mgmt_ip: 10.118.32.197 + username: opnfv + password: E///@work shelf: - id: 1 blade: @@ -12,15 +16,22 @@ shelf: - id: 4 - id: 5 - id: 6 -networking: - switch_type: esxi - switch_mgmt_ip: 192.168.0.1/24 - vlan: - - name: traffic - tag: 100 - - name: storage - tag: 102 - - name: control - tag: 101 +network: - name: management -... \ No newline at end of file + cidr: 192.168.0.0/24 + start: 192.168.0.1 + end: 192.168.0.253 + - name: private + vlan: + cidr: 192.168.11.0/24 + start: 192.168.11.1 + end: 192.168.11.253 + - name: storage + vlan: + cidr: 192.168.12.0/24 + start: 192.168.12.1 + end: 192.168.12.253 + - name: public + vlan: +... + diff --git a/fuel/deploy/deploy.py b/fuel/deploy/deploy.py index 4df4f36..4037c1d 100644 --- a/fuel/deploy/deploy.py +++ b/fuel/deploy/deploy.py @@ -1,43 +1,28 @@ -import subprocess -import sys import time import os +import sys + +import common from dha import DeploymentHardwareAdapter from dea import DeploymentEnvironmentAdapter +from configure_environment import ConfigureEnvironment -SUPPORTED_RELEASE = 'Juno on CentOS 6.5' -N = {'id': 0, 'status': 1, 'name': 2, 'cluster': 3, 'ip': 4, 'mac': 5, - 'roles': 6, 'pending_roles': 7, 'online': 8} -E = {'id': 0, 'status': 1, 'name': 2, 'mode': 3, 'release_id': 4, - 'changes': 5, 'pending_release_id': 6} -R = {'id': 0, 'name': 1, 'state': 2, 'operating_system': 3, 'version': 4} -RO = {'name': 0, 'conflicts': 1} - -def exec_cmd(cmd): - process = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=True) - return process.communicate()[0] - -def parse(printout): - parsed_list = [] - lines = printout.splitlines() - for l in lines[2:]: - parsed = [e.strip() for e in l.split('|')] - parsed_list.append(parsed) - return parsed_list - -def err(error_message): - sys.stderr.write(error_message) - sys.exit(1) +SUPPORTED_RELEASE = 'Juno on CentOS 6.5' +N = common.N +E = common.E +R = common.R +RO = common.RO +exec_cmd = common.exec_cmd +parse = common.parse +err = common.err class Deploy(object): - def __init__(self): + def __init__(self, yaml_config_dir): self.supported_release = None + self.yaml_config_dir = yaml_config_dir def get_id_list(self, list): return [l[0] for l in list] @@ -96,6 +81,18 @@ class Deploy(object): self.check_role_definitions() self.check_previous_installation() + def power_off_blades(self, dha, shelf_blades_dict): + for shelf, blade_list in shelf_blades_dict.iteritems(): + dha.power_off_blades(shelf, blade_list) + + def power_on_blades(self, dha, shelf_blades_dict): + for shelf, blade_list in shelf_blades_dict.iteritems(): + dha.power_on_blades(shelf, blade_list) + + def set_boot_order(self, dha, shelf_blades_dict): + for shelf, blade_list in shelf_blades_dict.iteritems(): + dha.set_boot_order_blades(shelf, blade_list) + def count_discovered_nodes(self, node_list): discovered_nodes = 0 for node in node_list: @@ -122,7 +119,7 @@ class Deploy(object): def assign_cluster_node_ids(self, dha, dea, controllers, compute_hosts): node_list= parse(exec_cmd('fuel node list')) for shelf_id in dea.get_shelf_ids(): - for blade_id in dea.get_blade_ids(shelf_id): + for blade_id in dea.get_blade_ids_per_shelf(shelf_id): blade_mac_list = dha.get_blade_mac_addresses( shelf_id, blade_id) @@ -142,27 +139,36 @@ class Deploy(object): "with MACs %s or blade is not in " "discover status\n" % blade_mac_list) - def env_exists(self, env_name): - env_list = parse(exec_cmd('fuel env --list')) - for env in env_list: - if env[E['name']] == env_name and env[E['status']] == 'new': - return True - return False def configure_environment(self, dea): - env_name = dea.get_environment_name() - exec_cmd('fuel env -c --name %s --release %s --mode ha --net neutron ' - '--nst vlan' % (env_name, self.supported_release[R['id']])) + config_env = ConfigureEnvironment(dea, self.yaml_config_dir) + + + + def provision(self): + + + + def fix_power_address(self): + + + + + def deploy(self): + + if id in self.get_id_list(parse(exec_cmd('fuel env list'))): + + self.fix_power_address() - if not self.env_exists(env_name): - err("Failed to create environment %s" % env_name) def main(): yaml_path = exec_cmd('pwd').strip() + '/dea.yaml' - deploy = Deploy() + yaml_config_dir = '/var/lib/opnfv/pre_deploy' + + deploy = Deploy(yaml_config_dir) dea = DeploymentEnvironmentAdapter() @@ -172,30 +178,34 @@ def main(): dea.parse_yaml(yaml_path) - dha = DeploymentHardwareAdapter(dea.get_server_type()) + server_type, mgmt_ip, username, password = dea.get_server_info() + shelf_blades_dict = dea.get_blade_ids_per_shelves() - deploy.check_prerequisites() - - dha.power_off_blades() + dha = DeploymentHardwareAdapter(server_type, mgmt_ip, username, password) - dha.configure_networking() + deploy.check_prerequisites() - dha.reset_to_factory_defaults() + deploy.power_off_blades(dha, shelf_blades_dict) - dha.set_boot_order() + deploy.set_boot_order(dha, shelf_blades_dict) - dha.power_on_blades() + deploy.power_on_blades(dha, shelf_blades_dict) - dha.get_blade_mac_addresses() + macs = dha.get_blade_mac_addresses() deploy.wait_for_discovered_blades(dea.get_no_of_blades()) + controllers = [] compute_hosts = [] deploy.assign_cluster_node_ids(dha, dea, controllers, compute_hosts) + + deploy.configure_environment(dea) + deploy.deploy(dea) + if __name__ == '__main__': diff --git a/fuel/deploy/deploy_fuel.sh b/fuel/deploy/deploy_fuel.sh new file mode 100755 index 0000000..8cb72b7 --- /dev/null +++ b/fuel/deploy/deploy_fuel.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# Deploy in deployFuel has the "configure host-network, +# install fuel, configure vm and start it" meaning +set -o xtrace +set -o errexit +set -o nounset +set -o pipefail + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +readonly iso_file=$1 +readonly interface=$2 +readonly vm_name="fuel_opnfv" +readonly ssh_fuel_vm="sshpass -p r00tme + ssh -o UserKnownHostsFile=/dev/null + -o StrictHostKeyChecking=no + -q + root@192.168.0.11" +readonly RUN_INSTALL="${RUN_INSTALL:-false}" +readonly DEV="${DEV:-false}" + +# poll is not real timeout, commands can take some undefined time to execute +# it is a count of how many times to try while sleeping shortly +# in between checks +readonly poll_virtinstall=1800 +readonly poll_fuel_startup=1200 +readonly poll_deployment=2150 +readonly fuel_logfile="/var/log/puppet/bootstrap_admin_node.log" + +cat >$interface.xml < + $interface + + + + +EOF + +cleanup_previous_run() { + echo "Cleaning up previous run" + set +eu + virsh net-destroy $interface > /dev/null 2>&1 + virsh net-undefine $interface > /dev/null 2>&1 + virsh destroy $vm_name > /dev/null 2>&1 + virsh undefine $vm_name > /dev/null 2>&1 + set -eu +} + +create_disk_and_install() { + rm -rf $vm_name.qcow2 + qemu-img create -f qcow2 -o preallocation=metadata $vm_name.qcow2 60G + virt-install --connect=qemu:///system \ + --name=$vm_name \ + --network=network:$interface \ + --ram 2048 --vcpus=4,cores=2 --check-cpu --hvm \ + --disk path=$vm_name.qcow2,format=qcow2,device=disk,bus=virtio \ + --noautoconsole --vnc \ + --cdrom $iso_file +} + +wait_for_virtinstall() { + # Workaround for virt-install --wait which restarts vm + # too fast too attach disk + echo "Waiting for virt-install to finish..." + set +eu + stopped=false + for i in $(seq 0 $poll_virtinstall); do + virsh_out=`virsh list | grep "$vm_name"` + if [ -z "$virsh_out" ]; then + stopped=true + break + fi + sleep 2 + done + set -eu +} + +wait_for_fuel_startup() { + echo "Wait for fuel to start up..." + for i in $(seq 0 $poll_fuel_startup); do + sleep 2 && echo -n "$i " + $ssh_fuel_vm grep complete $fuel_logfile && + echo "Fuel bootstrap is done, deployment should have started now" && + return 0 + done + return 1 +} + + +cleanup_previous_run +virsh net-define $interface.xml +virsh net-start $interface +create_disk_and_install +wait_for_virtinstall + +echo "Starting $vm_name after installation in 6s..." && sleep 6s +set +eu + +virsh start $vm_name +if ! wait_for_fuel_startup; then + echo "Fuel failed to start up" + exit 1 +fi diff --git a/fuel/deploy/dha.py b/fuel/deploy/dha.py index f78686b..87ac6e2 100644 --- a/fuel/deploy/dha.py +++ b/fuel/deploy/dha.py @@ -1,26 +1,36 @@ +from hardware_adapters.hp.hp_adapter import HpAdapter class DeploymentHardwareAdapter(object): - def __new__(cls, server_type): + def __new__(cls, server_type, *args): if cls is DeploymentHardwareAdapter: - if server_type == 'esxi': return EsxiAdapter() - if server_type == 'hp': return HpAdapter() - if server_type == 'dell': return DellAdapter() - if server_type == 'libvirt': return LibvirtAdapter() + if server_type == 'esxi': return EsxiAdapter(*args) + if server_type == 'hp': return HpAdapter(*args) + if server_type == 'dell': return DellAdapter(*args) + if server_type == 'libvirt': return LibvirtAdapter(*args) return super(DeploymentHardwareAdapter, cls).__new__(cls) class HardwareAdapter(object): - def power_off_blades(self): + def power_off_blades(self, shelf, blade_list): raise NotImplementedError - def power_on_blades(self): + def power_off_blade(self, shelf, blade): + raise NotImplementedError + + def power_on_blades(self, shelf, blade_list): + raise NotImplementedError + + def power_on_blade(self, shelf, blade): raise NotImplementedError def power_cycle_blade(self): raise NotImplementedError - def set_boot_order(self): + def set_boot_order_blades(self, shelf, blade_list): + raise NotImplementedError + + def set_boot_order_blade(self, shelf, blade): raise NotImplementedError def reset_to_factory_defaults(self): @@ -29,20 +39,23 @@ class HardwareAdapter(object): def configure_networking(self): raise NotImplementedError - def get_blade_mac_addresses(self, shelf_id, blade_id): + def get_blade_mac_addresses(self, shelf, blade): raise NotImplementedError - def get_blade_hardware_info(self, shelf_id, blade_id): + def get_hardware_info(self, shelf, blade): raise NotImplementedError class EsxiAdapter(HardwareAdapter): - pass -class LibvirtAdapter(HardwareAdapter): - pass + def __init__(self): + self.environment = {1: {1: {'mac': ['00:50:56:8c:05:85']}, + 2: {'mac': ['00:50:56:8c:21:92']}}} -class HpAdapter(HardwareAdapter): + def get_blade_mac_addresses(self, shelf, blade): + return self.environment[shelf][blade]['mac'] + +class LibvirtAdapter(HardwareAdapter): pass class DellAdapter(HardwareAdapter): diff --git a/fuel/deploy/hardware_adapters/hp/hp_adapter.py b/fuel/deploy/hardware_adapters/hp/hp_adapter.py new file mode 100644 index 0000000..7ce0dc9 --- /dev/null +++ b/fuel/deploy/hardware_adapters/hp/hp_adapter.py @@ -0,0 +1,411 @@ +import re +import time +from netaddr import EUI, mac_unix +import logging + +from run_oa_command import RunOACommand + + +LOG = logging.getLogger(__name__) +out_hdlr = logging.FileHandler(__file__.split('.')[0] + '.log', mode='w') +out_hdlr.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) +LOG.addHandler(out_hdlr) +LOG.setLevel(logging.DEBUG) + +class HpAdapter(object): + + # Exception thrown at any kind of failure to get the requested + # information. + class NoInfoFoundError(Exception): + pass + + # Totally failed to connect so a re-try with other HW should + # be done. This exception should never escape this class. + class InternalConnectError(Exception): + pass + + # Format MAC so leading zeroes are displayed + class mac_dhcp(mac_unix): + word_fmt = "%.2x" + + def __init__(self, mgmt_ip, username, password): + self.mgmt_ip = mgmt_ip + self.username = username + self.password = password + self.oa_error_message = '' + + def get_blade_mac_addresses(self, shelf, blade): + + LOG.debug("Entering: get_mac_addr_hp(%d,%d)" % (shelf, blade)) + self.oa_error_message = '' + oa = RunOACommand(self.mgmt_ip, self.username, self.password) + + LOG.debug("Connect to active OA for shelf %d" % shelf) + try: + res = oa.connect_to_active() + except: + raise self.InternalConnectError(oa.error_message) + if res is None: + raise self.InternalConnectError(oa.error_message) + if not oa.connected(): + raise self.NoInfoFoundError(oa.error_message) + + cmd = ("show server info " + str(blade)) + + LOG.debug("Send command to OA: %s" % cmd) + try: + serverinfo = oa.send_command(cmd) + except: + raise self.NoInfoFoundError(oa.error_message) + finally: + oa.close() + + (left, right) = self.find_mac(serverinfo, shelf, blade) + + left = EUI(left, dialect=self.mac_dhcp) + right = EUI(right, dialect=self.mac_dhcp) + return [str(left), str(right)] + + def get_blade_hardware_info(self, shelf, blade=None): + + if blade: + LOG.debug("Entering: get_hp_info(%d,%d)" % (shelf, blade)) + else: + LOG.debug("Entering: get_hp_info(%d)" % shelf) + + self.oa_error_message = '' + oa = RunOACommand(self.mgmt_ip, self.username, self.password) + + LOG.debug("Connect to active OA for shelf %d" % shelf) + + try: + res = oa.connect_to_active() + except: + self.oa_error_message = oa.error_message + return None + if res is None: + self.oa_error_message = oa.error_message + return None + if not oa.connected(): + self.oa_error_message = oa.error_message + return None + + # If no blade specified we're done we know this is an HP at this point + if not blade: + oa.close() + return "HP" + + check = "show server info %d" % blade + LOG.debug("Send command to OA: %s" % check) + output = oa.send_command("%s" % check) + oa.close() + + match = r"Product Name:\s+(.+)\Z" + if re.search(match, str(output[:])) is None: + self.oa_error_message = ("Blade %d in shelf %d does not exist\n" + % (blade, shelf)) + return None + + for line in output: + seobj = re.search(match, line) + if seobj: + return "HP %s" % seobj.group(1) + return False + + def power_off_blades(self, shelf, blade_list): + return self.set_state(shelf, 'locked', blade_list=blade_list) + + def power_on_blades(self, shelf, blade_list): + return self.set_state(shelf, 'unlocked', blade_list=blade_list) + + def power_off_blade(self, shelf, blade): + return self.set_state(shelf, 'locked', one_blade=blade) + + def power_on_blade(self, shelf, blade): + return self.set_state(shelf, 'unlocked', one_blade=blade) + + def set_boot_order_blade(self, shelf, blade): + return self.set_boot_order(shelf, one_blade=blade) + + def set_boot_order_blades(self, shelf, blade_list): + return self.set_boot_order(shelf, blade_list=blade_list) + + + + # Search HP's OA server info for MAC for left and right control + def find_mac(self, serverinfo, shelf, blade): + left = False + right = False + for line in serverinfo: + if ("No Server Blade Installed" in line or + "Invalid Arguments" in line): + raise self.NoInfoFoundError("Blade %d in shelf %d " + "does not exist." % (blade, shelf)) + seobj = re.search(r"LOM1:1-a\s+([0-9A-F:]+)", line, re.I) + if seobj: + left = seobj.group(1) + else: + seobj = re.search(r"LOM1:2-a\s+([0-9A-F:]+)", line, re.I) + if seobj: + right = seobj.group(1) + if left and right: + return left, right + raise self.NoInfoFoundError("Could not find MAC for blade %d " + "in shelf %d." % (blade, shelf)) + + # Do power on or off on all configured blades in shelf + # Return None to indicate that no connection do OA succeeded, + # Return False to indicate some connection to OA succeeded, + # or config error + # Return True to indicate that power state succesfully updated + # state: locked, unlocked + def set_state(self, shelf, state, one_blade=None, blade_list=None): + + if state not in ['locked', 'unlocked']: + return None + + if one_blade: + LOG.debug("Entering: set_state_hp(%d,%s,%d)" % + (shelf, state, one_blade)) + else: + LOG.debug("Entering: set_state_hp(%d,%s)" % (shelf, state)) + + self.oa_error_message = '' + + oa = RunOACommand(self.mgmt_ip, self.username, self.password) + + LOG.debug("Connect to active OA for shelf %d" % shelf) + + try: + res = oa.connect_to_active() + except: + self.oa_error_message = oa.error_message + return None + if res is None: + self.oa_error_message = oa.error_message + return None + if not oa.connected(): + self.oa_error_message = oa.error_message + return False + + if one_blade: + blades = [one_blade] + else: + blades = sorted(blade_list) + + LOG.debug("Check if blades are present") + + check = "show server list" + + LOG.debug("Send command to OA: %s" % check) + output = oa.send_command(check) + first = True + bladelist = '' + for blade in blades: + prog = re.compile(r"\s+" + str(blade) + r"\s+\[Absent\]", + re.MULTILINE) + if prog.search(str(output[:])) is not None: + oa.close() + self.oa_error_message = ("Blade %d in shelf %d " + % (blade, shelf)) + if one_blade: + self.oa_error_message += ("does not exist.\n" + "Set state %s not performed.\n" + % state) + else: + self.oa_error_message += ( + "specified but does not exist.\nSet " + "state %s not performed on shelf %d\n" + % (state, shelf)) + return False + if not first: + bladelist += "," + else: + first = False + bladelist += str(blade) + + if blade_list: + LOG.debug("All blades present") + + # Use leading upper case on On/Off so it can be reused in match + extra = "" + if state == "locked": + powerstate = "Off" + extra = "force" + else: + powerstate = "On" + + cmd = "power%s server %s" % (powerstate, bladelist) + + if extra != "": + cmd += " %s" % extra + + LOG.debug("Send command to OA: %s" % cmd) + + try: + oa.send_command(cmd) + except: + self.oa_error_message = oa.error_message + oa.close() + return False + + # Check that all blades reach the state which can take some time, + # so re-try a couple of times + LOG.debug("Check if state %s successfully set" % state) + recheck = 2 + while True: + LOG.debug("Send command to OA: %s" % check) + try: + output = oa.send_command(check) + except: + self.oa_error_message = oa.error_message + oa.close() + return False + for blade in blades: + match = (r"\s+" + str(blade) + + r"\s+\w+\s+\w+.\w+.\w+.\w+\s+\w+\s+%s" % + powerstate) + prog = re.compile(match, re.MULTILINE) + if prog.search(str(output[:])) is None: + recheck -= 1 + if recheck >= 0: + # Re-try + time.sleep(3) + break + oa.close() + self.oa_error_message = ( + "Could not set state %s on blade %d in shelf %d\n" + % (state, one_blade, shelf)) + for line in output: + self.oa_error_message += line + return False + else: + # state reached for all blades, exit the infinite loop + break + + if one_blade: + LOG.debug("State %s successfully set on blade %d in shelf %d" + % (state, one_blade, shelf)) + else: + LOG.debug("State %s successfully set on blades %s in shelf %d" + % (state, blade_list, shelf)) + oa.close() + return True + + # Change boot order on all blades in shelf + # Return None to indicate that no connection do OA succeeded, + # Return False to indicate some connection to OA succeeded, + # or config error, + # Return True to indicate that boot order succesfully changed + def set_boot_order(self, shelf, one_blade=None, blade_list=None): + + if one_blade: + LOG.debug("Entering: set_bootorder_hp(%d,%d)" % (shelf, one_blade)) + else: + LOG.debug("Entering: set_bootorder_hp(%d)" % shelf) + + self.oa_error_message = '' + + oa = RunOACommand(self.mgmt_ip, self.username, self.password) + + LOG.debug("Connect to active OA for shelf %d" % shelf) + + try: + res = oa.connect_to_active() + except: + self.oa_error_message = oa.error_message + return None + if res is None: + self.oa_error_message = oa.error_message + return None + if not oa.connected(): + self.oa_error_message = oa.error_message + return False + + if one_blade: + blades = [one_blade] + else: + blades = sorted(blade_list) + + LOG.debug("Check if blades are present") + + check = "show server list" + + LOG.debug("Send command to OA: %s" % check) + + output = oa.send_command(check) + first = True + bladelist = '' + for blade in blades: + prog = re.compile(r"\s+" + str(blade) + r"\s+\[Absent\]", + re.MULTILINE) + if prog.search(str(output[:])) is not None: + oa.close() + self.oa_error_message = ("Blade %d in shelf %d " + % (blade, shelf)) + if one_blade: + self.oa_error_message += ( + "does not exist.\nChange boot order not performed.\n") + else: + self.oa_error_message += ( + "specified but does not exist.\n" + "Change boot order not performed on shelf %d\n" + % shelf) + return False + if not first: + bladelist += ',' + else: + first = False + bladelist += str(blade) + + if blade_list: + LOG.debug("All blades present") + + # Boot origins are pushed so first set boot from hard disk, then PXE + # NB! If we want to support boot from SD we must add USB to the "stack" + cmd1 = "set server boot first hdd %s" % bladelist + cmd2 = "set server boot first pxe %s" % bladelist + for cmd in [cmd1, cmd2]: + + LOG.debug("Send command to OA: %s" % cmd) + try: + output = oa.send_command(cmd) + except: + self.oa_error_message = oa.error_message + for line in output: + self.oa_error_message += line + oa.close() + return False + + # Check that all blades got the correct boot order + # Needs updating if USB is added + LOG.debug("Check if boot order successfully set") + match = (r"^.*Boot Order\):\',\s*\'(\\t)+PXE NIC 1\',\s*\'(\\t)" + r"+Hard Drive") + prog = re.compile(match) + for blade in blades: + + check = "show server boot %d" % blade + + LOG.debug("Send command to OA: %s" % check) + try: + output = oa.send_command(check) + except: + self.oa_error_message = oa.error_message + oa.close() + return False + if prog.search(str(output[:])) is None: + oa.close() + self.oa_error_message = ("Failed to set boot order on blade " + "%d in shelf %d\n" % (blade, shelf)) + for line in output: + self.oa_error_message += line + return False + LOG.debug("Boot order successfully set on blade %d in shelf %d" + % (blade, shelf)) + + if blade_list: + LOG.debug("Boot order successfully set on all configured blades " + "in shelf %d" % (shelf)) + oa.close() + return True diff --git a/fuel/deploy/hardware_adapters/hp/run_oa_command.py b/fuel/deploy/hardware_adapters/hp/run_oa_command.py new file mode 100644 index 0000000..32135c3 --- /dev/null +++ b/fuel/deploy/hardware_adapters/hp/run_oa_command.py @@ -0,0 +1,113 @@ +import socket +import paramiko +import logging + +LOG = logging.getLogger(__name__) +out_hdlr = logging.FileHandler(__file__.split('.')[0] + '.log', mode='w') +out_hdlr.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) +LOG.addHandler(out_hdlr) +LOG.setLevel(logging.DEBUG) + +class RunOACommand: + + def __init__(self, mgmt_ip, username, password): + self.ssh = None + self.mgmt_ip = mgmt_ip + self.username = username + self.password = password + self.error_message = "" + + def connected(self): + return self.ssh is not None + + def close(self): + if self.connected(): + self.ssh.close() + self.ssh = None + self.error_message = "" + + def connect(self): + LOG.info("Trying to connect to OA at %s" % self.mgmt_ip) + try: + self.ssh.connect(self.mgmt_ip, + username=self.username, + password=self.password, + look_for_keys=False, + allow_agent=False) + return True + except socket.error, (err, message): + self.error_message += ("Can not talk to OA %s: %s\n" % + (self.mgmt_ip, message)) + except Exception as e: + self.error_message += ("Can not talk to OA %s: %s\n" % + (self.mgmt_ip, e.args)) + LOG.error("Failed to connect to OA at %s" % self.mgmt_ip) + return False + + # Return None if this most likely is not an OA + # False if we failed to connect to an active OA + # True if connected + def connect_to_active(self): + self.error_message = "OA connect failed with these errors:\n" + + self.ssh = paramiko.SSHClient() + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + initial_mgmt_ip = self.mgmt_ip + if not self.connect(self.mgmt_ip, self.username, self.password): + octets = self.mgmt_ip.split(".") + self.mgmt_ip = "%s.%s.%s.%s" % (octets[0], + octets[1], + octets[2], + str(int(octets[3]) + 1)) + if not self.connect(self.mgmt_ip, self.username, self.password): + self.ssh = None + LOG.error("Failed to connect to OA at %s (and %s)" % + (initial_mgmt_ip, self.mgmt_ip)) + return None + + output = self.send_command("show oa status") + for line in output: + if "Standby" in line: + self.ssh.close() + self.error_message += ( + "%s is the standby OA, trying next OA\n" % self.mgmt_ip) + LOG.info("%s is the standby OA" % self.mgmt_ip) + if self.mgmt_ip != initial_mgmt_ip: + self.error_message += ( + "Can only talk to OA %s which is the standby OA\n" % + self.mgmt_ip) + self.ssh = None + return False + else: + octets = self.mgmt_ip.split(".") + self.mgmt_ip = "%s.%s.%s.%s" % (octets[0], + octets[1], + octets[2], + str(int(octets[3]) + 1)) + if not self.connect(self.mgmt_ip, self.username, + self.password): + self.ssh = None + return False + LOG.info("Connected to active OA at %s" % self.mgmt_ip) + self.error_message = "" + return True + + def send_command(self, cmd): + if not self.connected(): + self.error_message = ( + "Not connected, cannot send command %s\n" % (cmd)) + raise + + LOG.info('Sending "%s" to %s' % (cmd, self.mgmt_ip)) + stdin, stdout, stderr = self.ssh.exec_command(cmd) + output = [] + for line in stdout.read().splitlines(): + if line != '': + output.append(line) + return output + + def __exit__(self, type, value, traceback): + if self.connected(): + self.close() + self.ssh = None \ No newline at end of file -- 2.16.6