├── .gitignore ├── HP └── ILONetworkBoot.py ├── README.md ├── checkRedundantRouters.py ├── cleanDHCPipaddress.sh ├── cloudstackops ├── __init__.py ├── cloudstackops.py ├── cloudstackopsbase.py ├── cloudstackopsssh.py ├── cloudstacksql.py ├── cloudstackstorage.py ├── hypervisor.py ├── kvm.py ├── vmware.py └── xenserver.py ├── clusterMaintenance.py ├── config.sample ├── email_template ├── migrateRouterVM.txt ├── migrateRouterVM_done.txt ├── migrateVirtualMachine_done.txt ├── migrateVirtualMachine_done_nostart.txt ├── migrateVirtualMachine_start.txt ├── rebootRouterVM.txt ├── rebootRouterVM_done.txt ├── recreateRouterVM_done.txt ├── recreateRouterVM_novms.txt ├── recreateRouterVM_start.txt ├── reportAccounts.txt ├── stopStartVirtualMachine_done.txt └── stopStartVirtualMachine_start.txt ├── empty_host.py ├── empty_host_deprecated.py ├── featureTemplates.py ├── generateMigrationCommand.sh ├── generateXen2KvmMigrationCsv.sh ├── hypervisorMaintenance.py ├── killJobs.py ├── kvm_check_bonds.sh ├── kvm_post_empty_script.sh ├── kvm_post_reboot_script.sh ├── kvm_pre_empty_script.sh ├── kvm_rolling_reboot.py ├── listHAWorkers.py ├── listOrphanedDisks.py ├── listRunningJobs.py ├── listVirtualMachines.py ├── listVolumes.py ├── liveMigrateHVtoPOD.py ├── liveMigrateRouter.py ├── liveMigrateVirtualMachine.py ├── liveMigrateVirtualMachineVolumes.py ├── marvin ├── Marvin-0.1.0.tar.gz └── README.md ├── migrateIsolatedNetworkToVPC.py ├── migrateOfflineVolumes.py ├── migrateRouterVM.py ├── migrateVirtualMachine.py ├── migrateVirtualMachineFromVMwareToKVM.py ├── migrateVirtualMachineFromXenServerToKVM.py ├── migrateVolumeFromXenServerToKVM.py ├── rebalanceOSTypesOnCluster.py ├── rebootRouterVM.py ├── rebootVPC.py ├── reportAccounts.py ├── requirements.txt ├── stopStartVirtualMachine.py ├── updateHostTags.py ├── whoHasThisIp.py ├── whoHasThisMac.py ├── xenserver_check_bonds.py ├── xenserver_create_vlans.sh ├── xenserver_fake_pvtools.sh ├── xenserver_parallel_evacuate.py ├── xenserver_patches_to_install.txt ├── xenserver_post_empty_script.sh ├── xenserver_pre_empty_script.sh ├── xenserver_rolling_reboot.py └── xenserver_upload_patches_to_poolmaster.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | config 4 | *.iml 5 | .idea/ 6 | xenserver_patches/ 7 | virtualenv 8 | output 9 | -------------------------------------------------------------------------------- /HP/ILONetworkBoot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse 3 | import ilorest 4 | import logging 5 | import sys 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def converge_file(arguments): 12 | logger.info("Converging over given file.") 13 | f = open(arguments.hosts, 'r') 14 | 15 | for host in f: 16 | host = host.rstrip() # Strip trailing whitespaces and newline. 17 | logger.info("HOST: %s -> Starting converge." % host) 18 | rest_obj = create_rest_object_from_host_and_login(host, arguments.username, arguments.password) 19 | set_temporary_boot_target(rest_obj, arguments.target) 20 | reset_host(rest_obj) 21 | logout_from_host(rest_obj) 22 | f.close() 23 | 24 | 25 | def create_rest_object_from_host_and_login(host, username, password): 26 | logger.info("HOST: %s -> Creating REST object." % host) 27 | rest_obj = ilorest.rest_client(base_url=host, username=username, password=password, default_prefix='/rest/v1') 28 | rest_obj.login(auth="session") 29 | return rest_obj 30 | 31 | 32 | def logout_from_host(rest_obj): 33 | logger.info("HOST: %s -> Logging out." % rest_obj.get_base_url()) 34 | rest_obj.logout() 35 | 36 | 37 | def reset_host(rest_obj): 38 | logger.info("HOST: %s -> Resetting host." % rest_obj.get_base_url()) 39 | body = dict() 40 | body["Action"] = "Reset" 41 | body["ResetType"] = "ForceRestart" 42 | response = rest_obj.post(path="/rest/v1/Systems/1", body=body) 43 | if response.status != 200: 44 | logger.error("HOST: %s -> Unable to reset host. Exiting..." % rest_obj.get_base_url()) 45 | sys.exit(1) 46 | logger.info("HOST: %s -> Reset host successful." % rest_obj.get_base_url()) 47 | 48 | 49 | def set_temporary_boot_target(rest_obj, boot_target): 50 | logger.info("HOST: %s -> Setting %s as temporary boot target." % (rest_obj.get_base_url(), boot_target)) 51 | body = dict() 52 | body["Boot"] = dict() 53 | body["Boot"]["BootSourceOverrideEnabled"] = "Once" 54 | body["Boot"]["BootSourceOverrideTarget"] = boot_target 55 | response = rest_obj.patch(path="/rest/v1/Systems/1", body=body) 56 | if response.status != 200: 57 | logger.error("HOST: %s -> Setting %s as temporary boot target failed. Exiting..." % (rest_obj.get_base_url(), boot_target)) 58 | sys.exit(1) 59 | logger.info("HOST: %s -> Setting %s as temporary boot target successful." % (rest_obj.get_base_url(), boot_target)) 60 | 61 | 62 | if __name__ == "__main__": 63 | logger.info("App: Set temporary boot target and reset host(s).") 64 | parser = argparse.ArgumentParser(description='Set network as temporary boot target and reset host(s).') 65 | parser.add_argument('--hosts', required=True, help='File containing the host(s) in line separated format') 66 | parser.add_argument('--username', required=True, help='Username to login to the ILO api') 67 | parser.add_argument('--password', required=True, help='Password to login to the ILO api') 68 | parser.add_argument('--target', default='Pxe', help='The target device to boot from.') 69 | args = parser.parse_args() 70 | converge_file(args) 71 | 72 | -------------------------------------------------------------------------------- /checkRedundantRouters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to report redundant routers that run on the same pod 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | # Function to handle our arguments 34 | 35 | 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 1 41 | global domainname 42 | domainname = '' 43 | global configProfileName 44 | configProfileName = '' 45 | 46 | # Usage message 47 | help = "Usage: ./" + os.path.basename(__file__) + ' [options]' + \ 48 | '\n --config-profile -c \t\t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 49 | '\n --debug\t\t\t\tEnable debug mode' + \ 50 | '\n --exec\t\t\t\tExecute for real (not needed for check* scripts)' 51 | 52 | try: 53 | opts, args = getopt.getopt( 54 | argv, "hc:d:p:", [ 55 | "config-profile=", "debug", "exec", "is-projectvm"]) 56 | except getopt.GetoptError as e: 57 | print "Error: " + str(e) 58 | print help 59 | sys.exit(2) 60 | 61 | if len(opts) == 0: 62 | print help 63 | sys.exit(2) 64 | 65 | for opt, arg in opts: 66 | if opt == '-h': 67 | print help 68 | sys.exit() 69 | elif opt in ("-c", "--config-profile"): 70 | configProfileName = arg 71 | elif opt in ("--debug"): 72 | DEBUG = 1 73 | elif opt in ("--exec"): 74 | DRYRUN = 0 75 | 76 | # Default to cloudmonkey default config file 77 | if len(configProfileName) == 0: 78 | configProfileName = "config" 79 | 80 | # Parse arguments 81 | if __name__ == "__main__": 82 | handleArguments(sys.argv[1:]) 83 | 84 | # Init our class 85 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 86 | 87 | if DEBUG == 1: 88 | print "Warning: Debug mode is enabled!" 89 | 90 | if DRYRUN == 1: 91 | print "Warning: dry-run mode is enabled, not running any commands!" 92 | 93 | # make credentials file known to our class 94 | c.configProfileName = configProfileName 95 | 96 | # Init the CloudStack API 97 | c.initCloudStackAPI() 98 | 99 | if DEBUG == 1: 100 | print "API address: " + c.apiurl 101 | print "ApiKey: " + c.apikey 102 | print "SecretKey: " + c.secretkey 103 | 104 | # Get the redundant routers 105 | redRouters = c.getRedundantRouters('{}') 106 | 107 | # Look for routers on the same POD 108 | if redRouters is not None and redRouters is not 1: 109 | for routerData in redRouters.itervalues(): 110 | if routerData is None or routerData == 1: 111 | continue 112 | if routerData['router'].podid == routerData['routerPeer'].podid: 113 | print "Warning: Router pair " + routerData['router'].name + " and " + routerData['routerPeer'].name + " run on same POD!" + " (" + routerData['router'].podid + " / " + routerData['routerPeer'].podid + ")" 114 | if DEBUG == 1: 115 | print "DEBUG: " + routerData['router'].name + " has peer " + routerData['routerPeer'].name 116 | -------------------------------------------------------------------------------- /cleanDHCPipaddress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to clean old DHCP ip address config 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | # Config files 26 | DHCP_LEASES="/var/lib/misc/dnsmasq.leases" 27 | DHCP_HOSTS="/etc/dhcphosts.txt" 28 | HOSTS="/etc/hosts" 29 | 30 | # Ip address is required argument 31 | ipv4=$1 32 | if [ ! $ipv4 ] 33 | then 34 | echo "Usage: $0 1.2.3.4 [0|1]" 35 | echo "first arg: ip address, second force no/yes" 36 | exit 1 37 | fi 38 | 39 | # Be friendly, or use force 40 | FORCE=$2 41 | if [ ! $FORCE ] 42 | then 43 | FORCE=0 44 | fi 45 | 46 | # Debug info 47 | echo "Cleaning $ipv4, force=$FORCE" 48 | 49 | # Try to find mac address and hostname 50 | MAC=$(grep $ipv4 $DHCP_LEASES | awk '{print $2}') 51 | HOST=$(grep $ipv4 $DHCP_HOSTS | cut -d, -f4) 52 | 53 | # Find mac address, alternative version 54 | if [ ! $MAC ] 55 | then 56 | MAC=$(grep $ipv4 $DHCP_HOSTS | cut -d, -f1) 57 | fi 58 | 59 | # Need some force 60 | if [ $FORCE -eq 1 ] 61 | then 62 | # Clean ip address 63 | echo "Forcing removal of $ipv4 from $DHCP_HOSTS" 64 | sed -i /$ipv4,/d $DHCP_HOSTS 65 | 66 | # Clean hosts file 67 | echo "Forcing removal of $ipv4 from $HOSTS" 68 | sed -i /"$ipv4 "/d $HOSTS 69 | 70 | # Clean old mac 71 | if [ $MAC ] 72 | then 73 | echo "Forcing removal of $MAC from $DHCP_HOSTS" 74 | sed -i /$MAC/d $DHCP_HOSTS 75 | fi 76 | exit 0 77 | fi 78 | 79 | # No mac found 80 | echo $MAC 81 | if [ ! $MAC ] 82 | then 83 | echo "Error: Could not find Mac address in $DHCP_LEASES" 84 | exit 1 85 | fi 86 | 87 | echo $HOST 88 | if [ ! $HOST ] 89 | then 90 | echo "Error: Could not find hostname in $DHCP_HOSTS" 91 | exit 1 92 | fi 93 | 94 | # Run the clean script with its required arguments 95 | echo "Running /opt/cloud/bin/edithosts.sh -m $MAC -4 $ipv4 -h $HOST to clean it up. If it does not work, run with force parameter:" 96 | echo "Force like this: $0 $ipv4 1" 97 | /opt/cloud/bin/edithosts.sh -m $MAC -4 $ipv4 -h $HOST 98 | 99 | -------------------------------------------------------------------------------- /cloudstackops/__init__.py: -------------------------------------------------------------------------------- 1 | # Empty for now 2 | -------------------------------------------------------------------------------- /cloudstackops/cloudstackopsbase.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Schuberg Philis BV 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | 20 | # CloudStackOps Base Class 21 | # Remi Bergsma - rbergsma@schubergphilis.com 22 | 23 | import ConfigParser 24 | import os 25 | import pprint 26 | import signal 27 | import sys 28 | 29 | # Slackweb 30 | try: 31 | import slackweb 32 | except Exception as e: 33 | print "Error: Please install slackweb library to support Slack messaging: %s" % e 34 | print " pip install slackweb" 35 | sys.exit(1) 36 | 37 | # Colored terminals 38 | try: 39 | from clint.textui import colored 40 | except Exception as e: 41 | print "Error: Please install clint library to support color in the terminal: %s" % e 42 | print " pip install clint" 43 | sys.exit(1) 44 | 45 | 46 | class Timeout: 47 | """Timeout class using ALARM signal.""" 48 | 49 | class Timeout(Exception): 50 | pass 51 | 52 | def __init__(self, sec): 53 | self.sec = sec 54 | 55 | def __enter__(self): 56 | signal.signal(signal.SIGALRM, self.raise_timeout) 57 | signal.alarm(self.sec) 58 | 59 | def __exit__(self, *args): 60 | signal.alarm(0) # disable alarm 61 | 62 | def raise_timeout(self, *args): 63 | raise Timeout.Timeout() 64 | 65 | 66 | class CloudStackOpsBase(object): 67 | # Init function 68 | def __init__(self, debug=0, dryrun=0, force=0): 69 | self.DEBUG = debug 70 | self.DRYRUN = dryrun 71 | self.FORCE = force 72 | self.configProfileNameFullPath = '' 73 | self.organization = '' 74 | self.smtpserver = 'localhost' 75 | self.mail_from = '' 76 | self.errors_to = '' 77 | self.configfile = os.getcwd() + '/config' 78 | self.pp = pprint.PrettyPrinter(depth=6) 79 | self.slack = None 80 | self.slack_custom_title = "Undefined" 81 | self.slack_custom_value = "Undefined" 82 | self.cluster = "Undefined" 83 | self.instance_name = "Undefined" 84 | self.task = "Undefined" 85 | self.vm_name = "Undefined" 86 | self.zone_name = "Undefined" 87 | 88 | self.printWelcome() 89 | self.configure_slack() 90 | 91 | signal.signal(signal.SIGINT, self.catch_ctrl_C) 92 | 93 | def printWelcome(self): 94 | pass 95 | 96 | def configure_slack(self): 97 | slack_url = "" 98 | try: 99 | self.configfile = os.getcwd() + '/config' 100 | config = ConfigParser.RawConfigParser() 101 | config.read(self.configfile) 102 | slack_url = config.get('slack', 'hookurl') 103 | 104 | except: 105 | print "Warning: No Slack integration found, so not using. See config file to setup." 106 | 107 | self.slack = None 108 | if len(slack_url) > 0: 109 | self.slack = slackweb.Slack(url=slack_url) 110 | 111 | def print_message(self, message, message_type="Note", to_slack=False): 112 | if message_type != "Plain": 113 | print "%s: %s" % (message_type.title(), message) 114 | 115 | if to_slack and self.slack is not None: 116 | color = "good" 117 | if message_type.lower() == "error": 118 | color = "danger" 119 | if message_type.lower() == "warning": 120 | color = "warning" 121 | self.send_slack_message(message, color) 122 | 123 | def send_slack_message(self, message, color="good"): 124 | 125 | attachments = [] 126 | attachment = {"text": message, "color": color, "mrkdwn_in": ["text", "pretext", "fields"], "mrkdwn": "true", 127 | "fields": [ 128 | { 129 | "title": str(self.slack_custom_title), 130 | "value": str(self.slack_custom_value), 131 | "short": "true" 132 | }, 133 | { 134 | "title": "Task", 135 | "value": self.task, 136 | "short": "true" 137 | }, 138 | { 139 | "title": "Cluster", 140 | "value": self.cluster, 141 | "short": "true" 142 | }, 143 | { 144 | "title": "Instance ID", 145 | "value": self.instance_name, 146 | "short": "true" 147 | }, 148 | { 149 | "title": "VM name", 150 | "value": self.vm_name, 151 | "short": "true" 152 | }, 153 | { 154 | "title": "Zone", 155 | "value": self.zone_name, 156 | "short": "true" 157 | } 158 | ]} 159 | 160 | try: 161 | attachments.append(attachment) 162 | self.slack.notify(attachments=attachments, icon_emoji=":robot_face:", username="cloudstackOps") 163 | except: 164 | print "Warning: Slack said NO." 165 | 166 | # Handle unwanted CTRL+C presses 167 | def catch_ctrl_C(self, sig, frame): 168 | print "Warning: do not interrupt! If you really want to quit, use kill -9." 169 | 170 | # Read config files 171 | def readConfigFile(self): 172 | # Read our own config file for some more settings 173 | config = ConfigParser.RawConfigParser() 174 | config.read(self.configfile) 175 | try: 176 | self.organization = config.get('cloudstackOps', 'organization') 177 | self.smtpserver = config.get('mail', 'smtpserver') 178 | self.mail_from = config.get('mail', 'mail_from') 179 | self.errors_to = config.get('mail', 'errors_to') 180 | except: 181 | print "Error: Cannot read or parse CloudStackOps config file '" + self.configfile + "'" 182 | print "Hint: Setup the local config file 'config', using 'config.sample' as a starting point. See documentation." 183 | sys.exit(1) 184 | -------------------------------------------------------------------------------- /cloudstackops/cloudstackopsssh.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Schuberg Philis BV 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | 20 | # Class to talk to hypervisors using SSH 21 | # Remi Bergsma - rbergsma@schubergphilis.com 22 | 23 | # Import the class we depend on 24 | from cloudstackopsbase import * 25 | # Import our dependencies 26 | import subprocess 27 | from subprocess import Popen, PIPE 28 | import getpass 29 | 30 | 31 | class CloudStackOpsSSH(CloudStackOpsBase): 32 | 33 | # Run SSH remoteCmd 34 | def runSSHCommand(self, hostname, remoteCmd): 35 | 36 | ssh_user=getpass.getuser() 37 | 38 | if self.DEBUG == 1: 39 | print "Debug: Running SSH command on " + hostname + " :" + remoteCmd 40 | p = subprocess.Popen(['ssh', 41 | '-oStrictHostKeyChecking=no', 42 | '-oUserKnownHostsFile=/dev/null', 43 | '-q', 44 | ssh_user + '@' + hostname, 45 | remoteCmd], 46 | stdout=subprocess.PIPE) 47 | output = "" 48 | try: 49 | output = p.stdout.read().strip() 50 | while p.poll() is None: 51 | time.sleep(0.5) 52 | except: 53 | pass 54 | retcode = self.__parseReturnCode(p.returncode, hostname) 55 | return retcode, output 56 | 57 | def __parseReturnCode(self, retcode, hostname): 58 | if retcode != 0: 59 | print "Error: SSH connection to '" + hostname + "' returned code " + str(retcode) 60 | print "Note: Please make sure 'ssh " + hostname + "' works key-based and try again." 61 | elif self.DEBUG == 1: 62 | print "Note: SSH remoteCmd executed OK." 63 | return retcode 64 | 65 | # Test SSH connection 66 | def testSSHConnection(self, hostname): 67 | remoteCmd = 'echo Note: Testing SSH to $HOSTNAME' 68 | return self.runSSHCommand(hostname, remoteCmd) 69 | 70 | # Fake PV tools 71 | def fakePVTools(self, hostname): 72 | print "Note: We're faking the presence of PV tools of all vm's reporting no tools on hypervisor '" + hostname + "'. Migration will not work otherwise." 73 | remoteCmd = "xe vm-list PV-drivers-up-to-date='' is-control-domain=false resident-on=$(xe host-list name-label=$HOSTNAME --minimal) params=uuid --minimal |tr ', ' '\n'| grep \"-\" | awk {'print \"/opt/tools/sysadmin/bin/fakepv.sh \" $1'} | sh" 74 | return self.runSSHCommand(hostname, remoteCmd) 75 | 76 | # Look for poolmaster 77 | def getPoolmaster(self, hostname): 78 | remoteCmd = "for i in $(xe pool-list --minimal | sed 's/\, /\\n/g'); do poolMaster=$(xe pool-list uuid=$i --minimal params=master | xargs -i xe host-list uuid={} params=name-label --minimal); echo $poolMaster; done" 79 | return self.runSSHCommand(hostname, remoteCmd) 80 | 81 | # Get bond status 82 | def getBondStatus(self, hostname): 83 | remoteCmd = "/opt/tools/nrpe/bin/check_xenbond.py | awk {'print $1'} | tr -d \":\"" 84 | return self.runSSHCommand(hostname, remoteCmd) 85 | 86 | # Get heartbeat status 87 | def getHeartbeatStatus(self, hostname): 88 | remoteCmd = "/opt/tools/nrpe/bin/check_heartbeat.sh | awk {'print $2'}" 89 | return self.runSSHCommand(hostname, remoteCmd) 90 | 91 | # Get xapi vm count 92 | def getXapiVmCount(self, hostname): 93 | remoteCmd = "xe vm-list resident-on=$(xe host-list params=uuid \ 94 | name-label=$HOSTNAME --minimal) \ 95 | params=name-label,memory-static-max is-control-domain=false | \ 96 | tr '\\n' ' ' | sed 's/name-label/\\n/g' | \ 97 | awk {'print $4 \",\" $8'} | sed '/^,$/d'| wc -l" 98 | return self.runSSHCommand(hostname, remoteCmd) 99 | 100 | # Migrate vm via xapi 101 | def migrateVirtualMachineViaXapi(self, args): 102 | 103 | # Handle arguments 104 | hostname = (args['hostname']) if 'hostname' in args else '' 105 | desthostname = (args['desthostname']) if 'desthostname' in args else '' 106 | vmname = (args['vmname']) if 'vmname' in args else '' 107 | 108 | if len(vmname) > 0 and len(desthostname) > 0 and len(hostname): 109 | remoteCmd = "xe vm-migrate vm=" + vmname + " host=" + desthostname 110 | if self.DEBUG == 1: 111 | print "Debug: Running SSH command on " + hostname + " :" + remoteCmd 112 | p = subprocess.Popen(['ssh', 113 | '-oStrictHostKeyChecking=no', 114 | '-oUserKnownHostsFile=/dev/null', 115 | '-q', 116 | 'root@' + hostname, 117 | remoteCmd], 118 | stdout=subprocess.PIPE) 119 | output = "" 120 | try: 121 | output = p.stdout.read().strip() 122 | while p.poll() is None: 123 | time.sleep(0.5) 124 | except: 125 | pass 126 | retcode = p.returncode 127 | if retcode != 0: 128 | print "Error: something went wrong on host " + hostname + ". Got return code " + str(retcode) 129 | elif self.DEBUG == 1: 130 | print "Note: Output: " + output 131 | return retcode, output 132 | return false 133 | -------------------------------------------------------------------------------- /cloudstackops/cloudstackstorage.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Schuberg Philis BV 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | 20 | # Class with Hypervisor storage methods 21 | 22 | import sys 23 | import time 24 | import os 25 | 26 | # Import the class we depend on 27 | from cloudstackops import * 28 | from cloudstackopsssh import * 29 | from cloudstackopsbase import * 30 | 31 | # Fabric 32 | from fabric import * 33 | from fabric import api as fab 34 | from fabric.api import run, env, prefix, output, settings 35 | 36 | 37 | class StorageHelper(): 38 | 39 | def __init__(self, remote_ssh_key='~/.ssh/id_rsa.pub', remote_ssh_user='root', remote_ssh_password='password', remote_timeout=10, debug=0): 40 | 41 | self.debug = debug 42 | 43 | # fabric settings 44 | env.user = remote_ssh_user 45 | env.password = remote_ssh_password 46 | env.forward_agent = True 47 | env.disable_known_hosts = True 48 | env.parallel = False 49 | env.pool_size = 1 50 | env.timeout = remote_timeout 51 | env.abort_on_prompts = True 52 | 53 | # prevent warnings on remote host 54 | os.environ['LC_CTYPE'] = 'C' 55 | 56 | # Supress Fabric output by default, unless debug level has been set 57 | if self.debug > 0: 58 | output['debug'] = True 59 | output['running'] = True 60 | output['stdout'] = True 61 | output['stdin'] = True 62 | output['output'] = True 63 | output['warnings'] = True 64 | else: 65 | output['debug'] = False 66 | output['running'] = False 67 | output['stdout'] = False 68 | output['stdin'] = False 69 | output['output'] = False 70 | output['warnings'] = False 71 | 72 | # generic method to run remote commands via fabric 73 | def _remote_cmd(self, hostname, cmd): 74 | 75 | returncode = '0' 76 | result = '' 77 | output = '' 78 | errormsg = '' 79 | 80 | try: 81 | if self.debug > 0: 82 | print "[DEBUG]: Running remote command: ", cmd, " on", env.user + "@" + hostname 83 | 84 | with settings(host_string=env.user + "@" + hostname, warn_only=True, capture=False): 85 | result = fab.run(command=cmd) 86 | 87 | except Exception as error: 88 | errormsg = error 89 | returncode = '-1' 90 | 91 | finally: 92 | 93 | if result: 94 | if self.debug > 0: 95 | print "[DEBUG]: command success:", result.succeeded, "command failed:", result.failed, "command returncode:", result.return_code, "command error:", result.stderr 96 | 97 | returncode = result.return_code 98 | output = result.stdout 99 | 100 | if result.failed: 101 | errormsg = result.stdout 102 | 103 | return (returncode, output, errormsg) 104 | 105 | # returns a dict of mounts on remote host 106 | # dict is structured : 107 | def list_mounts(self, hostname): 108 | mount_file = '/proc/mounts' 109 | remote_cmd = "cat " + mount_file 110 | mount_list = {} 111 | 112 | returncode, output, errmsg = self._remote_cmd(hostname, remote_cmd) 113 | 114 | if returncode == 0: 115 | 116 | for mount in output.split('\r\n'): 117 | mount = mount.split(' ') 118 | 119 | mount_device = mount[0] 120 | mount_path = mount[1] 121 | mount_list[mount_path] = mount_device 122 | 123 | else: 124 | print "[ERROR]: Failed to retrieve list of mounts on " + hostname + " due to: ", errmsg 125 | 126 | return mount_list 127 | 128 | # returns a remote mountpoint for a given devicepath 129 | def get_mountpoint(self, hostname, device_path): 130 | 131 | mount_list = self.list_mounts(hostname) 132 | mountpoint = None 133 | 134 | if device_path.endswith('/'): 135 | # strip the slash 136 | device_path = device_path[:-1] 137 | 138 | if len(mount_list) > 0: 139 | for path, device in mount_list.iteritems(): 140 | 141 | if device.endswith('/'): 142 | # strip the slash 143 | device = device[:-1] 144 | 145 | if device == device_path: 146 | 147 | mountpoint = path 148 | 149 | return mountpoint 150 | 151 | # returns a dict of remote files and size (mb) for a given hostname and 152 | # path 153 | def list_files(self, hostname, path): 154 | 155 | file_list = {} 156 | 157 | if path is not '': 158 | remote_cmd = "find -H " + path + " -type f -exec du -sm {} \;" 159 | returncode, output, errmsg = self._remote_cmd(hostname, remote_cmd) 160 | 161 | if returncode == 0: 162 | 163 | for line in output.split('\r\n'): 164 | line = line.split('\t') 165 | 166 | file_size = line[0] 167 | file_path = line[1] 168 | 169 | file_list[file_path] = file_size 170 | 171 | else: 172 | print "[ERROR]: Failed to retrieve list from " + hostname + "of file due to: ", output, errmsg 173 | 174 | return file_list 175 | -------------------------------------------------------------------------------- /cloudstackops/hypervisor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import time 6 | 7 | from fabric import api as fab 8 | from fabric.api import * 9 | 10 | # Set user/passwd for fabric ssh 11 | env.user = 'root' 12 | env.password = 'password' 13 | env.forward_agent = True 14 | env.disable_known_hosts = True 15 | env.parallel = False 16 | env.pool_size = 1 17 | 18 | # Supress Fabric output by default, we will enable when needed 19 | output['debug'] = False 20 | output['running'] = False 21 | output['stdout'] = False 22 | output['stdin'] = False 23 | output['output'] = False 24 | output['warnings'] = False 25 | 26 | 27 | # Class to talk to hypervisors 28 | class hypervisor(object): 29 | 30 | def __init__(self, ssh_user='root', threads=5): 31 | self.ssh_user = ssh_user 32 | self.threads = threads 33 | self.DEBUG = 0 34 | 35 | # Check if we are really offline 36 | def check_offline(self, host): 37 | print "Note: Waiting for " + host.name + " to go offline" 38 | while os.system("ping -c 1 " + host.ipaddress + " 2>&1 >/dev/null") == 0: 39 | # Progress indication 40 | sys.stdout.write(".") 41 | sys.stdout.flush() 42 | time.sleep(5) 43 | # Remove progress indication 44 | sys.stdout.write("\033[F") 45 | print "Note: Host " + host.name + " is now offline! " 46 | time.sleep(120) 47 | 48 | # Execute script on hypervisor 49 | def exec_script_on_hypervisor(self, host, script): 50 | script = script.split('/')[-1] 51 | print "Note: Executing script '%s' on host %s.." % (script, host.name) 52 | try: 53 | with settings(show('output'), host_string=self.ssh_user + "@" + host.ipaddress): 54 | return fab.run("bash /tmp/" + script) 55 | except: 56 | return False 57 | -------------------------------------------------------------------------------- /cloudstackops/vmware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from fabric import api as fab 4 | from fabric.api import * 5 | 6 | import hypervisor 7 | 8 | # Set user/passwd for fabric ssh 9 | env.user = 'root' 10 | env.password = 'password' 11 | env.forward_agent = True 12 | env.disable_known_hosts = True 13 | env.parallel = False 14 | env.pool_size = 1 15 | env.keepalive = 1 16 | 17 | # Supress Fabric output by default, we will enable when needed 18 | output['debug'] = False 19 | output['running'] = False 20 | output['stdout'] = False 21 | output['stdin'] = False 22 | output['output'] = False 23 | output['warnings'] = False 24 | 25 | 26 | class vmware(hypervisor.hypervisor): 27 | 28 | def __init__(self, ssh_user='root', threads=5): 29 | hypervisor.__init__(ssh_user, threads) 30 | self.ssh_user = ssh_user 31 | self.threads = threads 32 | self.mountpoint = None 33 | self.migration_path = None 34 | 35 | # Execute script on hypervisor 36 | def exec_script_on_hypervisor(self, host, script): 37 | script = script.split('/')[-1] 38 | print "Note: Executing script '%s' on host %s.." % (script, host.name) 39 | try: 40 | with settings(show('output'), host_string=self.ssh_user + "@" + host.ipaddress): 41 | return fab.run("bash /tmp/" + script) 42 | except: 43 | return False 44 | 45 | def find_nfs_mountpoint(self, host): 46 | print "Note: Looking for Datastores on VMware host %s" % host.name 47 | if self.mountpoint is not None: 48 | print "Note: Found " + str(self.mountpoint) 49 | return self.mountpoint 50 | try: 51 | with settings(host_string=self.ssh_user + "@" + host.ipaddress): 52 | command = "mount | grep sr-mount | grep \"type nfs\" | awk {'print $3'}" 53 | self.mountpoint = fab.run(command) 54 | print "Note: Found " + str(self.mountpoint) 55 | return self.mountpoint 56 | except: 57 | return False 58 | -------------------------------------------------------------------------------- /clusterMaintenance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to put a cluster in enable/disable and manage/unmanage state 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | from cloudstackops import cloudstackopsssh 30 | from cloudstackops import xenserver 31 | import os.path 32 | from random import choice 33 | from prettytable import PrettyTable 34 | 35 | # Function to handle our arguments 36 | 37 | 38 | def handleArguments(argv): 39 | global DEBUG 40 | DEBUG = 0 41 | global DRYRUN 42 | DRYRUN = 1 43 | global clustername 44 | clustername = '' 45 | global configProfileName 46 | configProfileName = '' 47 | global managedstate 48 | managedstate = "" 49 | global allocationstate 50 | allocationstate = "" 51 | global force 52 | force = 0 53 | global checkBonds 54 | checkBonds = True 55 | 56 | # Usage message 57 | help = "Usage: ./" + os.path.basename(__file__) + ' [options]' + \ 58 | '\n --config-profile -c \t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 59 | '\n --clustername -n \t\tName of the cluster to work with' + \ 60 | '\n --managedstate -m \tSet managed state to Managed or Unmanaged' + \ 61 | '\n --allocationstate -a \tSet allocated state to Enabled or Disabled' + \ 62 | '\n --no-bond-check\t\t\t\tSkip the bond check' + \ 63 | '\n --debug\t\t\t\t\tEnable debug mode' + \ 64 | '\n --exec\t\t\t\t\tExecute for real' 65 | 66 | try: 67 | opts, args = getopt.getopt( 68 | argv, "hc:n:a:m:", [ 69 | "credentials-file=", "clustername=", "managedstate=", "allocationstate=", "no-bond-check", "debug", "exec", "force"]) 70 | except getopt.GetoptError as e: 71 | print "Error: " + str(e) 72 | print help 73 | sys.exit(2) 74 | for opt, arg in opts: 75 | if opt == '-h': 76 | print help 77 | sys.exit() 78 | elif opt in ("-c", "--config-profile"): 79 | configProfileName = arg 80 | elif opt in ("-n", "--clustername"): 81 | clustername = arg 82 | elif opt in ("-m", "--managedstate"): 83 | checkBonds = False 84 | managedstate = arg 85 | elif opt in ("-a", "--allocationstate"): 86 | checkBonds = False 87 | allocationstate = arg 88 | elif opt in ("--debug"): 89 | DEBUG = 1 90 | elif opt in ("--exec"): 91 | DRYRUN = 0 92 | elif opt in ("--force"): 93 | force = 1 94 | elif opt in ("--no-bond-check"): 95 | checkBonds = False 96 | 97 | # Default to cloudmonkey default config file 98 | if len(configProfileName) == 0: 99 | configProfileName = "config" 100 | 101 | # We need at least these vars 102 | if len(clustername) == 0: 103 | print help 104 | sys.exit() 105 | if len(managedstate) > 0 and len(allocationstate) > 0: 106 | print "Error: please specify either 'allocationstate' or 'managedstate' and not both." 107 | print help 108 | sys.exit() 109 | 110 | # Check argument sanity 111 | allocationstate_set = ('Enabled', 'Disabled') 112 | if len(allocationstate) > 0 and allocationstate not in allocationstate_set: 113 | print "Error: 'allocationstate' can only contain " + str(allocationstate_set) 114 | print help 115 | sys.exit(1) 116 | 117 | managedstate_set = ('Managed', 'Unmanaged') 118 | if len(managedstate) > 0 and managedstate not in managedstate_set: 119 | print "Error: 'managedstate' can only contain " + str(managedstate_set) 120 | print help 121 | sys.exit(1) 122 | 123 | # Parse arguments 124 | if __name__ == "__main__": 125 | handleArguments(sys.argv[1:]) 126 | 127 | # Init our class 128 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 129 | ssh = cloudstackopsssh.CloudStackOpsSSH(DEBUG, DRYRUN) 130 | c.ssh = ssh 131 | x = xenserver.xenserver() 132 | c.xenserver = x 133 | 134 | if DEBUG == 1: 135 | print "Warning: Debug mode is enabled!" 136 | 137 | if DRYRUN == 1: 138 | print "Warning: dry-run mode is enabled, not running any commands!" 139 | 140 | # make credentials file known to our class 141 | c.configProfileName = configProfileName 142 | 143 | # Init the CloudStack API 144 | c.initCloudStackAPI() 145 | 146 | if DEBUG == 1: 147 | print "API address: " + c.apiurl 148 | print "ApiKey: " + c.apikey 149 | print "SecretKey: " + c.secretkey 150 | 151 | # Check cloudstack IDs 152 | if DEBUG == 1: 153 | print "Note: Checking CloudStack IDs of provided input.." 154 | clusterID = c.checkCloudStackName( 155 | {'csname': clustername, 'csApiCall': 'listClusters'}) 156 | if clusterID == 1: 157 | print "Error: Could not find cluster '" + clustername + "'." 158 | sys.exit(1) 159 | 160 | print "Note: Looking for other hosts in this cluster and checking their health.." 161 | 162 | # Print cluster info 163 | print "Note: Some info about cluster '" + clustername + "':" 164 | c.printCluster(clusterID) 165 | 166 | # Update Cluster 167 | if DRYRUN == 1 and len(managedstate) > 0: 168 | print "Note: Would have set the 'Managed State' of cluster '" + clustername + "' to '" + managedstate + "'" 169 | elif DRYRUN == 1 and len(allocationstate) > 0: 170 | print "Note: Would have set the 'Allocation State' of cluster '" + clustername + "' to '" + allocationstate + "'" 171 | elif DRYRUN == 1: 172 | print "Warning: no command specified, just listing info" 173 | else: 174 | clusterUpdateReturn = c.updateCluster( 175 | {'clusterid': clusterID, 'managedstate': managedstate, 'allocationstate': allocationstate}) 176 | 177 | if clusterUpdateReturn == 1 or clusterUpdateReturn is None: 178 | print "Error: update failed." 179 | else: 180 | cluster = clusterUpdateReturn.cluster 181 | print "Note: Updated OK!" 182 | t = PrettyTable(["Cluster name", 183 | "Allocation state", 184 | "Managed state", 185 | "Hypervisortype", 186 | "Pod name", 187 | "Zone name"]) 188 | t.align["Cluster name"] = "l" 189 | t.add_row([cluster.name, 190 | cluster.allocationstate, 191 | cluster.managedstate, 192 | cluster.hypervisortype, 193 | cluster.podname, 194 | cluster.zonename]) 195 | print t 196 | print "Note: Displaying the hosts of this cluster:" 197 | c.printHypervisors(clusterID, False, checkBonds) 198 | -------------------------------------------------------------------------------- /config.sample: -------------------------------------------------------------------------------- 1 | # Config file for CloudStack Operations scripts 2 | 3 | [cloudstackOps] 4 | organization = The Iaas Team 5 | 6 | [core] 7 | profile = config 8 | 9 | [mail] 10 | smtpserver = localhost 11 | mail_from = cloudstackOps@invalid 12 | errors_to = cloudstackOps@invalid 13 | 14 | [config] 15 | username = admin 16 | apikey = whMTYFZh3n7C4M8VCSpwEhpqZjYkzhYTufcpaLPH9hInYGTx4fOnrJ3dgL-3AZC_STMBUeTFQgqlETPEile4_A 17 | url = http://127.0.0.1:8080/client/api 18 | expires = 600 19 | secretkey = 9Z0S5-ryeoCworyp2x_tuhw5E4bAJ4JTRrpNaftTiAl488q5rvUt8_pG7LxAeg3m_VY-AafXQj-tVhkn9tFv1Q 20 | timeout = 3600 21 | password = password 22 | 23 | [slack] 24 | hookurl = 25 | 26 | [mysql-alias] 27 | mysqlhostname = mysqlhostname 28 | mysqlpassword = password 29 | mysqluser = username 30 | -------------------------------------------------------------------------------- /email_template/migrateRouterVM.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We will now migrate the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | If your router is a redundant pair, you will not experience any down time. If not, your VMs will have no 6 | network connectivity during maintenance. Expected downtime is ~5 minutes. 7 |

8 | Please contact us in case of any problems. 9 |

10 | Kind Regards, 11 |
12 |
ORGANIZATION 13 | -------------------------------------------------------------------------------- /email_template/migrateRouterVM_done.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We have migrated the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | Please contact us in case of any problems. 6 |

7 | Kind Regards, 8 |
9 |
ORGANIZATION 10 | -------------------------------------------------------------------------------- /email_template/migrateVirtualMachine_done.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We completed migrating the VM 'VMNAME' that belongs to your domain 'DOMAIN' to cluster 'TOCLUSTER'. 4 |
The instance id of this VM is 'INSTANCENAME' and before maintenance the state was 'STATE'. 5 |

6 | Your VM has been started on the new cluster and should be up again soon. 7 |

8 | Please contact us in case of any problems. 9 |

10 | Kind Regards, 11 |
12 |
ORGANIZATION 13 | -------------------------------------------------------------------------------- /email_template/migrateVirtualMachine_done_nostart.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We completed migrating the VM 'VMNAME' that belongs to your domain 'DOMAIN' to cluster 'TOCLUSTER'. 4 |
The instance id of this VM is 'INSTANCENAME' and before maintenance the state was 'STATE'. 5 |

6 | Note: your VM has NOT been started because it was not running before maintenance. 7 |

8 | Do NOT start it via the CloudStack GUI or else it will be migrated back to the old storage and start on the last hypervisor it was running on. 9 |

10 | Start the VM using CloudMonkey: 11 |

12 | CLOUDMONKEYCMD 13 |

14 | If you're unsure how to proceed, please contact us and we'll start the VM for you. 15 |

16 | Kind Regards, 17 |
18 |
ORGANIZATION 19 | -------------------------------------------------------------------------------- /email_template/migrateVirtualMachine_start.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We are about to migrate the VM 'VMNAME' that belongs to your domain 'DOMAIN' to cluster 'TOCLUSTER'. 4 |
The instance id of this VM is 'INSTANCENAME' and before maintenance the state was 'STATE'. 5 |

6 | You will receive another e-mail when migration is complete. If the VM was in 'Running' state, we will start it on the new cluster. 7 |

8 | Please contact us in case of any problems. 9 |

10 | Kind Regards, 11 |
12 |
ORGANIZATION 13 | -------------------------------------------------------------------------------- /email_template/rebootRouterVM.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We will now reboot the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | If this router is actually routing traffic (instead of just DHCP/DNS), then during this reboot your VMs will have no network connectivity. Expected downtime is 1~3 minutes. 6 |

7 | Please contact us in case of any problems. 8 |

9 | Kind Regards, 10 |
11 |
The IaaS Team. 12 | -------------------------------------------------------------------------------- /email_template/rebootRouterVM_done.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We have rebooted the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | Please contact us in case of any problems. 6 |

7 | Kind Regards, 8 |
9 |
ORGANIZATION 10 | -------------------------------------------------------------------------------- /email_template/recreateRouterVM_done.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We have recreated the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | Please contact us in case of any problems. 6 |

7 | Kind Regards, 8 |
9 |
ORGANIZATION 10 | -------------------------------------------------------------------------------- /email_template/recreateRouterVM_novms.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We have succesfully destroyed your router, but because there were no VMs running we did not create a new one. 4 |

5 | A new router will be created automatically the next time you start a VM in your domain 'ROUTERDOMAIN'. 6 |

7 | Please contact us in case of any problems. 8 |

9 | Kind Regards, 10 |
11 |
ORGANIZATION 12 | -------------------------------------------------------------------------------- /email_template/recreateRouterVM_start.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We are about to migrate/recreate the virtual router VM that belongs to your domain 'ROUTERDOMAIN' (ROUTERNAME). 4 |

5 | During this your VMs will not have network connectivity. To trigger creation of a new router, we will deploy a new VM in your network, and then delete it again. 6 |

7 | You will receive another e-mail when maintenance is finished. 8 |

9 | Please contact us in case of any problems. 10 |

11 | Kind Regards, 12 |
13 |
ORGANIZATION 14 | -------------------------------------------------------------------------------- /email_template/reportAccounts.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | This is an overview of the enabled admin users in your domain DOMAIN: 4 |

5 | USERTABLE 6 |

7 | The domain path in CloudStack is PATH. 8 |

9 | Please review and adjust where applicable. 10 | In case of issues: please contact us via EMAILADRESS. 11 |

12 | Kind Regards, 13 |
14 |
ORGANIZATION 15 | -------------------------------------------------------------------------------- /email_template/stopStartVirtualMachine_done.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We completed stop/starting the VM 'VMNAME' that belongs to your domain 'DOMAIN'. 4 |
The instance id of this VM is 'INSTANCENAME' and before maintenance the state was 'STATE'. 5 |

6 | Your VM has been started and should be up again soon. 7 |

8 | Please contact us in case of any problems. 9 |

10 | Kind Regards, 11 |
12 |
ORGANIZATION 13 | -------------------------------------------------------------------------------- /email_template/stopStartVirtualMachine_start.txt: -------------------------------------------------------------------------------- 1 | Dear FIRSTNAME LASTNAME, 2 |

3 | We are about to stop/start the VM 'VMNAME' that belongs to your domain 'DOMAIN'. 4 |
The instance id of this VM is 'INSTANCENAME' and before maintenance the state was 'STATE'. 5 |

6 | You will receive another e-mail when stop/start is complete. 7 |

8 | Please contact us in case of any problems. 9 |

10 | Kind Regards, 11 |
12 |
ORGANIZATION 13 | -------------------------------------------------------------------------------- /empty_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2017, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to empty/live migrate a HV to other HV in the same cluster 23 | # Daan de Goede 24 | 25 | import sys 26 | import getopt 27 | from cloudstackops import cloudstackops 28 | from cloudstackops import cloudstacksql 29 | from cloudstackops import kvm 30 | import os.path 31 | from datetime import datetime 32 | import time 33 | import liveMigrateVirtualMachine as lmvm 34 | 35 | # Function to handle our arguments 36 | 37 | 38 | def handleArguments(argv): 39 | global DEBUG 40 | DEBUG = 0 41 | global DRYRUN 42 | DRYRUN = 1 43 | global fromHV 44 | fromHV = '' 45 | global configProfileName 46 | configProfileName = '' 47 | global force 48 | force = 0 49 | 50 | # Usage message 51 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 52 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 53 | '\n --hypervisor -h \t\tHypervisor to migrate' + \ 54 | '\n --debug\t\t\t\tEnable debug mode' + \ 55 | '\n --exec\t\t\t\tExecute for real' 56 | 57 | try: 58 | opts, args = getopt.getopt( 59 | argv, "c:h:", [ 60 | "config-profile=", "hypervisor=", "debug", "exec", "force"]) 61 | except getopt.GetoptError as e: 62 | print "Error: " + str(e) 63 | print help 64 | sys.exit(2) 65 | for opt, arg in opts: 66 | if opt == '--help': 67 | print help 68 | sys.exit() 69 | elif opt in ("-c", "--config-profile"): 70 | configProfileName = arg 71 | elif opt in ("-h", "--hypervisor"): 72 | fromHV = arg 73 | elif opt in ("--debug"): 74 | DEBUG = 1 75 | elif opt in ("--exec"): 76 | DRYRUN = 0 77 | elif opt in ("--force"): 78 | force = 1 79 | 80 | # Default to cloudmonkey default config file 81 | if len(configProfileName) == 0: 82 | configProfileName = "config" 83 | 84 | # We need at least these vars 85 | if len(fromHV) == 0: 86 | print help 87 | sys.exit() 88 | 89 | def emptyHV(DEBUG=0, DRYRUN=1, fromHV='', configProfileName='', force=0): 90 | # Init our class 91 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 92 | c.task = "empty HV" 93 | c.slack_custom_title = "Domain" 94 | c.slack_custom_value = "" 95 | 96 | # Start time 97 | print "Note: Starting @ %s" % time.strftime("%Y-%m-%d %H:%M") 98 | start_time = datetime.now() 99 | 100 | if DEBUG == 1: 101 | print "Warning: Debug mode is enabled!" 102 | 103 | if DRYRUN == 1: 104 | print "Warning: dry-run mode is enabled, not running any commands!" 105 | 106 | # make credentials file known to our class 107 | c.configProfileName = configProfileName 108 | 109 | # Init the CloudStack API 110 | c.initCloudStackAPI() 111 | 112 | if DEBUG == 1: 113 | print "API address: " + c.apiurl 114 | print "ApiKey: " + c.apikey 115 | print "SecretKey: " + c.secretkey 116 | 117 | # Check hypervisor parameter 118 | if DEBUG == 1: 119 | print "Note: Checking host ID of provided hypervisor.." 120 | 121 | host = c.getHostByName(name=fromHV) 122 | if not host or len(host) == 0 or host['count'] != 1: 123 | print "hypervisor parameter ('" + fromHV + "') resulted in zero or more than one hypervisors!" 124 | sys.exit(1) 125 | 126 | if DEBUG == 1: 127 | print host 128 | 129 | hostid = host['host'][0]['id'] 130 | listVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true'}) 131 | listProjectVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true', 'projectid': -1}) 132 | listRouterVMs = c.listRouters({'hostid': hostid, 'listall': 'true'}) 133 | 134 | userVMs = {} 135 | userVMs['count'] = listVMs.get('count',0) + listProjectVMs.get('count',0) 136 | userVMs['virtualmachine'] = listVMs.get('virtualmachine',[]) + listProjectVMs.get('virtualmachine',[]) 137 | if DEBUG == 1: 138 | print userVMs 139 | print listRouterVMs 140 | vmCount = 1 141 | vmTotal = userVMs.get('count',0) 142 | print "found " + str(vmTotal) + " virtualmachines and " + str(listRouterVMs.get('count',0)) + " routerVMs on hypervisor: " + fromHV 143 | c.emptyHypervisor(hostid) 144 | 145 | result = True 146 | listVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true'}) 147 | listProjectVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true', 'projectid': -1}) 148 | listRouterVMs = c.listRouters({'hostid': hostid, 'listall': 'true'}) 149 | 150 | vmCount = listVMs.get('count',0) + listProjectVMs.get('count',0) + listRouterVMs.get('count',0) 151 | if vmCount > 0: 152 | result = False 153 | 154 | # End time 155 | message = "Finished @ " + time.strftime("%Y-%m-%d %H:%M") 156 | c.print_message(message=message, message_type="Note", to_slack=False) 157 | elapsed_time = datetime.now() - start_time 158 | 159 | if result: 160 | print "HV %s is successfully migrated in %s seconds" % (fromHV, elapsed_time.total_seconds()) 161 | else: 162 | print "HV %s has failed to migrate in %s seconds, %s of %s remaining." % (fromHV, elapsed_time.total_seconds(), vmCount, vmTotal) 163 | 164 | # Parse arguments 165 | if __name__ == "__main__": 166 | handleArguments(sys.argv[1:]) 167 | emptyHV(DEBUG, DRYRUN, fromHV, configProfileName, force) -------------------------------------------------------------------------------- /featureTemplates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to manage / (un)feature custom build templates 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | from datetime import date 33 | import re 34 | 35 | # Function to handle our arguments 36 | 37 | 38 | def handleArguments(argv): 39 | global DEBUG 40 | DEBUG = 0 41 | global DRYRUN 42 | DRYRUN = 1 43 | global configProfileName 44 | configProfileName = '' 45 | global zoneName 46 | zoneName = '' 47 | 48 | # Usage message 49 | help = "Usage: ./" + os.path.basename(__file__) + ' [options]' + \ 50 | '\n --config-profile -c \t\t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 51 | '\n --zone -z \t\t\tLimit to this zone' + \ 52 | '\n --debug\t\t\t\tEnable debug mode' + \ 53 | '\n --exec\t\t\t\tExecute for real' 54 | 55 | try: 56 | opts, args = getopt.getopt( 57 | argv, "hc:z:", [ 58 | "config-profile=", "zone", "debug", "exec"]) 59 | except getopt.GetoptError as e: 60 | print "Error: " + str(e) 61 | print help 62 | sys.exit(2) 63 | 64 | if len(opts) == 0: 65 | print help 66 | sys.exit(2) 67 | 68 | for opt, arg in opts: 69 | if opt == '-h': 70 | print help 71 | sys.exit() 72 | elif opt in ("-c", "--config-profile"): 73 | configProfileName = arg 74 | elif opt in ("-z", "--zone"): 75 | zoneName = arg 76 | elif opt in ("--debug"): 77 | DEBUG = 1 78 | elif opt in ("--exec"): 79 | DRYRUN = 0 80 | 81 | # Default to cloudmonkey default config file 82 | if len(configProfileName) == 0: 83 | configProfileName = "config" 84 | 85 | # Parse arguments 86 | if __name__ == "__main__": 87 | handleArguments(sys.argv[1:]) 88 | 89 | # Init our class 90 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 91 | 92 | if DEBUG == 1: 93 | print "Warning: Debug mode is enabled!" 94 | 95 | if DRYRUN == 1: 96 | print "Warning: dry-run mode is enabled, not running any commands!" 97 | 98 | # make credentials file known to our class 99 | c.configProfileName = configProfileName 100 | 101 | # Init the CloudStack API 102 | c.initCloudStackAPI() 103 | 104 | if len(zoneName) > 1: 105 | zoneID = c.checkCloudStackName( 106 | {'csname': zoneName, 'csApiCall': 'listZones'}) 107 | print "Note: Only processing templates in zone " + zoneName 108 | 109 | print "Warning: We only manage XenServer templates!" 110 | 111 | if DEBUG == 1: 112 | print "API address: " + c.apiurl 113 | print "ApiKey: " + c.apikey 114 | print "SecretKey: " + c.secretkey 115 | 116 | # get templates from CloudStack 117 | if len(zoneName) > 1: 118 | templateData = c.listTemplates({'templatefilter': 'all', 'zoneid': zoneID}) 119 | else: 120 | templateData = c.listTemplates({'templatefilter': 'all'}) 121 | # Sort by creation date, newest first 122 | templateData.sort(key=lambda x: x.created, reverse=True) 123 | 124 | if DEBUG == 1: 125 | c.pp.pprint(templateData) 126 | 127 | # We need this later 128 | featureTemplates = {} 129 | unfeatureTemplates = {} 130 | deleteTemplates = {} 131 | keepCount = {} 132 | 133 | # Keep this many templates per zone per OStype 134 | keepNr = 4 135 | 136 | # Let's see what we currently have 137 | for template in templateData: 138 | if template is None: 139 | continue 140 | # Skip templates that are not usable anyway 141 | if template.isready == False: 142 | continue 143 | # Don't mess with BUILTIN and SYSTEM templates, just for sure 144 | if template.templatetype.upper() != "USER": 145 | continue 146 | # Our build templates are XenServer only, don't touch others 147 | if template.hypervisor != "XenServer": 148 | continue 149 | 150 | # Delete cross-zone templates as they only bring trouble. But keep 151 | # systemvm templates. 152 | if template.crossZones and "systemvm" not in template.name.lower(): 153 | deleteTemplates[template.id] = template 154 | if template.isfeatured: 155 | unfeatureTemplates[template.id] = template 156 | continue 157 | 158 | # Find templates that match our build pattern: like m2015-02 or w2015-01 159 | m = re.findall('[mw]\d{4}-\d{2}', template.name) 160 | # Skip all others 161 | if len(m) == 0: 162 | # Only our build templates should be featured. Unfeature the others. 163 | if template.isfeatured: 164 | unfeatureTemplates[template.id] = template 165 | continue 166 | 167 | # Extract week / month number 168 | parts = m[0].split('-') 169 | for p in parts: 170 | if p.isdigit(): 171 | buildNr = int(p) 172 | elif p.startswith('w'): 173 | buildType = "Week" 174 | elif p.startswith('m'): 175 | buildType = "Month" 176 | 177 | # Figure out OS names 178 | osName = template.name[:9].replace("_", "").replace(" ", "_") 179 | 180 | # Feature the latest ostypename + zonename combi 181 | key = osName.lower() + template.zonename 182 | if osName.lower().startswith( 183 | ("rhel", 184 | "centos_6", 185 | "centos_7", 186 | "ubuntu", 187 | "win")): 188 | if key in featureTemplates: 189 | if featureTemplates[key].created < template.created: 190 | featureTemplates[key] = template 191 | elif template.isfeatured: 192 | unfeatureTemplates[template.id] = template 193 | else: 194 | featureTemplates[key] = template 195 | elif template.isfeatured: 196 | unfeatureTemplates[key] = template 197 | 198 | # Mark the ones we will keep and delete 199 | countkey = osName + template.zonename 200 | if countkey in keepCount: 201 | keepCount[countkey] += 1 202 | else: 203 | keepCount[countkey] = 0 204 | 205 | if DEBUG == 1: 206 | print "Counter " + countkey + " " + str(keepCount[countkey]) 207 | 208 | keepkey = countkey + str(keepCount[countkey]) 209 | if keepCount[countkey] > keepNr: 210 | deleteTemplates[keepkey] = template 211 | 212 | # This is the work we need to process 213 | loopWork = ['featureTemplates', 'unfeatureTemplates', 'deleteTemplates'] 214 | 215 | # Just print what we would do 216 | if DRYRUN == 1: 217 | for work in loopWork: 218 | t = PrettyTable(["Name", 219 | "Displaytext", 220 | "ostypename", 221 | "zonename", 222 | "isfeatured", 223 | "isready", 224 | "Created", 225 | "CrossZones", 226 | "Type"]) 227 | print work + ":" 228 | # Generate table 229 | for templatekey, template in eval(work).iteritems(): 230 | t.add_row([template.name, 231 | template.displaytext, 232 | template.ostypename, 233 | template.zonename, 234 | template.isfeatured, 235 | template.isready, 236 | template.created, 237 | template.crossZones, 238 | template.templatetype]) 239 | print t 240 | 241 | # Make changes to the templates 242 | elif DRYRUN == 0: 243 | for work in loopWork: 244 | print "Processing " + work + ".." 245 | if work == "featureTemplates": 246 | for templatekey, template in eval(work).iteritems(): 247 | if DEBUG == 1: 248 | print "DEBUG: Setting feature flag for " + template.name + " .." 249 | if template.isfeatured == False: 250 | result = c.updateTemplatePermissins( 251 | {'templateid': template.id, 'isfeatured': 'true'}) 252 | if result == 1: 253 | print "ERROR: Something went wrong!" 254 | else: 255 | print "Feature flag set OK for " + template.name 256 | else: 257 | print "Note: Template " + template.name + " is alreay featured, ignoring." 258 | elif work == "unfeatureTemplates": 259 | for templatekey, template in eval(work).iteritems(): 260 | if DEBUG == 1: 261 | print "DEBUG: Unsetting feature flag for " + template.name + " .." 262 | if template.isfeatured: 263 | result = c.updateTemplatePermissins( 264 | {'templateid': template.id, 'isfeatured': 'false'}) 265 | if result == 1: 266 | print "ERROR: Something went wrong!" 267 | else: 268 | print "Feature flag removed OK for " + template.name 269 | else: 270 | print "Note: Template " + template.name + " is not featured, ignoring." 271 | elif work == "deleteTemplates": 272 | for templatekey, template in eval(work).iteritems(): 273 | if DEBUG == 1: 274 | print "DEBUG: Deleting template " + template.name + " .." 275 | result = c.deleteTemplate({'id': template.id}) 276 | if result == 1: 277 | print "ERROR: Something went wrong!" 278 | else: 279 | print "Template " + template.name + " removed OK!" 280 | 281 | if DEBUG == 1: 282 | print "Note: We're done!" 283 | -------------------------------------------------------------------------------- /generateMigrationCommand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ZONE=$1 4 | VMNAME=$2 5 | 6 | cloudmonkey set display table 7 | 8 | HOSTNAME=$(cloudmonkey -p $ZONE list virtualmachines hypervisor=XenServer name=$VMNAME listall=true filter=hostname | grep -i mccp | awk '{ print $2 }') 9 | 10 | if [ -z "$HOSTNAME" ] 11 | then 12 | # Couldn't find VM / host 13 | exit 1 14 | fi 15 | 16 | PODNAME=$(cloudmonkey -p $ZONE list hosts name=$HOSTNAME filter=podname | grep -i mccp | awk '{ print $2 }') 17 | 18 | if [ "$ZONE" == "admin-nl1" ]; then 19 | if echo $PODNAME | grep -q -i SBP; then 20 | echo "./migrateVirtualMachineFromXenServerToKVM.py -c $ZONE -t mccppod052-cs01 -s $ZONE --helper-scripts-path /home/mcc_stenley/shared/cosmic/xen2kvm/ -i $VMNAME --exec" 21 | else 22 | echo "./migrateVirtualMachineFromXenServerToKVM.py -c $ZONE -t mccppod062-cs01 -s $ZONE --helper-scripts-path /home/mcc_stenley/shared/cosmic/xen2kvm/ -i $VMNAME --exec" 23 | fi 24 | elif [ "$ZONE" == "nl1" ]; then 25 | if echo $PODNAME | grep -q -i SBP; then 26 | echo "./migrateVirtualMachineFromXenServerToKVM.py -c $ZONE -t mccppod053-cs01 -s $ZONE --helper-scripts-path /home/mcc_stenley/shared/cosmic/xen2kvm/ -i $VMNAME --exec" 27 | else 28 | echo "./migrateVirtualMachineFromXenServerToKVM.py -c $ZONE -t mccppod063-cs01 -s $ZONE --helper-scripts-path /home/mcc_stenley/shared/cosmic/xen2kvm/ -i $VMNAME --exec" 29 | fi 30 | fi 31 | -------------------------------------------------------------------------------- /generateXen2KvmMigrationCsv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROFILE="xxx" 4 | OUTPUT_DIR="output" 5 | 6 | cloudmonkey set display table 7 | cloudmonkey set profile $PROFILE 8 | 9 | UUIDs=$(cloudmonkey list domains filter=id,name | grep -v id | grep -v '+') 10 | 11 | rm -rf $OUTPUT_DIR 12 | mkdir -p $OUTPUT_DIR 13 | 14 | cloudmonkey set display csv 15 | 16 | IFS=$'\n' 17 | for line in $UUIDs; do 18 | NAME=$(echo $line | awk '{ print $4 }') 19 | UUID=$(echo $line | awk '{ print $2 }') 20 | 21 | if [ -z "$NAME" ]; then 22 | continue 23 | fi 24 | 25 | OUTPUT=$(cloudmonkey list virtualmachines listall=true domainid=$UUID hypervisor=XenServer filter=domain,templatename,state,name,created,serviceofferingname) 26 | 27 | if [ ! -z "$OUTPUT" ]; then 28 | echo "$OUTPUT" > $OUTPUT_DIR/$NAME.csv 29 | fi 30 | done 31 | 32 | cloudmonkey set display table 33 | -------------------------------------------------------------------------------- /hypervisorMaintenance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to put a host in maintenance, and if that fails migrate vm's one by one 23 | # so that it will enter maintenance after all 24 | # Remi Bergsma - rbergsma@schubergphilis.com 25 | 26 | import time 27 | import sys 28 | import getopt 29 | from cloudstackops import cloudstackops 30 | from cloudstackops import cloudstackopsssh 31 | from cloudstackops import xenserver 32 | from cloudstackops import kvm 33 | import os.path 34 | import getpass 35 | 36 | # Function to handle our arguments 37 | 38 | 39 | def handleArguments(argv): 40 | global DEBUG 41 | DEBUG = 0 42 | global DRYRUN 43 | DRYRUN = 1 44 | global hostname 45 | hostname = '' 46 | global configProfileName 47 | configProfileName = '' 48 | global cancelmaintenance 49 | cancelmaintenance = 0 50 | global force 51 | force = 0 52 | global checkBonds 53 | checkBonds = True 54 | 55 | # Usage message 56 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 57 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 58 | '\n --hostname|-n \t\tWork with this hypervisor' + \ 59 | '\n --cancel-maintenance\t\t\tCancel maintenance for this hypervisor' + \ 60 | '\n --no-bond-check\t\t\tSkip the bond check' + \ 61 | '\n --force\t\t\t\tForce put in maintenance, even when there is already a hypervisor in maintenance' + \ 62 | '\n --debug\t\t\t\tEnable debug mode' + \ 63 | '\n --exec\t\t\t\tExecute for real' 64 | 65 | try: 66 | opts, args = getopt.getopt( 67 | argv, "hc:n:", [ 68 | "credentials-file=", "hostname=", "debug", "exec", "cancel-maintenance", "force", "no-bond-check"]) 69 | except getopt.GetoptError as e: 70 | print "Error: " + str(e) 71 | print help 72 | sys.exit(2) 73 | for opt, arg in opts: 74 | if opt == '-h': 75 | print help 76 | sys.exit() 77 | elif opt in ("-c", "--config-profile"): 78 | configProfileName = arg 79 | elif opt in ("-n", "--hostname"): 80 | hostname = arg 81 | elif opt in ("--cancel-maintenance"): 82 | cancelmaintenance = 1 83 | elif opt in ("--debug"): 84 | DEBUG = 1 85 | elif opt in ("--exec"): 86 | DRYRUN = 0 87 | elif opt in ("--force"): 88 | force = 1 89 | elif opt in ("--no-bond-check"): 90 | checkBonds = False 91 | 92 | # Default to cloudmonkey default config file 93 | if len(configProfileName) == 0: 94 | configProfileName = "config" 95 | 96 | # We need at least these vars 97 | if len(hostname) == 0: 98 | print help 99 | sys.exit() 100 | 101 | # Parse arguments 102 | if __name__ == "__main__": 103 | handleArguments(sys.argv[1:]) 104 | 105 | # Init our classes 106 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 107 | ssh = cloudstackopsssh.CloudStackOpsSSH(DEBUG, DRYRUN) 108 | c.ssh = ssh 109 | 110 | # Init XenServer class 111 | x = xenserver.xenserver('root') 112 | x.DEBUG = DEBUG 113 | x.DRYRUN = DRYRUN 114 | c.xenserver = x 115 | 116 | # Init KVM class 117 | k = kvm.Kvm(ssh_user=getpass.getuser()) 118 | k.DEBUG = DEBUG 119 | k.DRYRUN = DRYRUN 120 | c.kvm = k 121 | 122 | if DEBUG == 1: 123 | print "Warning: Debug mode is enabled!" 124 | 125 | if DRYRUN == 1: 126 | print "Warning: dry-run mode is enabled, not running any commands!" 127 | 128 | # make credentials file known to our class 129 | c.configProfileName = configProfileName 130 | 131 | # Init the CloudStack API 132 | c.initCloudStackAPI() 133 | 134 | if DEBUG == 1: 135 | print "API address: " + c.apiurl 136 | print "ApiKey: " + c.apikey 137 | print "SecretKey: " + c.secretkey 138 | 139 | # Check cloudstack IDs 140 | if DEBUG == 1: 141 | print "Note: Checking CloudStack IDs of provided input.." 142 | hostID = c.checkCloudStackName({'csname': hostname, 'csApiCall': 'listHosts'}) 143 | if hostID == 1: 144 | print "Error: Host " + hostname + " could not be found." 145 | sys.exit(1) 146 | 147 | # Get hosts data 148 | hostData = c.getHostData({'hostname': hostname}) 149 | for host in hostData: 150 | if host.name == hostname: 151 | foundHostData = host 152 | 153 | # Get hosts that belong to toCluster 154 | clusterHostsData = c.getAllHostsFromCluster(foundHostData.clusterid) 155 | print "Note: Host '" + hostname + "' belongs to cluster '" + foundHostData.clustername + "'" 156 | 157 | if foundHostData.hypervisor not in ("XenServer", "KVM"): 158 | print "Error: This is only tested for KVM and XenServer at the moment!" 159 | sys.exit(1) 160 | 161 | # Test SSH connection 162 | retcode, output = ssh.testSSHConnection(foundHostData.ipaddress) 163 | if retcode != 0: 164 | sys.exit(1) 165 | 166 | # Poolmaster 167 | poolmaster = "n/a" 168 | if foundHostData.hypervisor == "XenServer": 169 | retcode, poolmaster = ssh.getPoolmaster(foundHostData.ipaddress) 170 | print "Note: Poolmaster is: '" + poolmaster + "'" 171 | 172 | print "Note: Looking for other hosts in this cluster and checking their health.." 173 | 174 | # Print overview 175 | c.printHypervisors(foundHostData.clusterid, poolmaster, checkBonds, foundHostData.hypervisor) 176 | 177 | # Look for hosts without XenTools 178 | if foundHostData.hypervisor == "XenServer": 179 | retcode, output = ssh.fakePVTools(foundHostData.ipaddress) 180 | if retcode != 0: 181 | print "Error: something went wrong. Got return code " + str(retcode) 182 | else: 183 | print "Note: Command executed OK." 184 | 185 | # Cancel maintenance 186 | if cancelmaintenance == 1 and DRYRUN == 0: 187 | print "Note: You want to cancel maintenance for host '" + hostname + "'" 188 | # Does it make sense? 189 | if foundHostData.resourcestate != "Maintenance" and foundHostData.resourcestate != "PrepareForMaintenance": 190 | print "Error: Host '" + hostname + "' is not in maintenance, so can not cancel. Halting." 191 | sys.exit(1) 192 | # Cancel maintenance 193 | cancelresult = c.cancelHostMaintenance(hostID) 194 | if cancelresult is None or cancelresult == 1: 195 | print "Error: Cancel maintenance failed. Please investigate manually. Halting." 196 | # Check result 197 | while True: 198 | hostData = c.getHostData({'hostname': hostname}) 199 | for host in hostData: 200 | if host.name == hostname: 201 | foundHostData = host 202 | 203 | if foundHostData.resourcestate != "Enabled": 204 | print "Note: Resource state currently is '" + foundHostData.resourcestate + "', waiting some more.." 205 | time.sleep(5) 206 | else: 207 | print "Note: Resource state currently is '" + foundHostData.resourcestate + "', returning" 208 | break 209 | print "Note: Cancel maintenance succeeded for host '" + hostname + "'" 210 | # Print overview 211 | c.printHypervisors(foundHostData.clusterid, poolmaster, checkBonds, foundHostData.hypervisor) 212 | print "Note: We're done!" 213 | sys.exit(0) 214 | elif cancelmaintenance == 1 and DRYRUN == 1: 215 | print "Note: Would have cancelled maintenance for host '" + hostname + "'." 216 | sys.exit(0) 217 | elif DRYRUN == 1: 218 | print "Note: Would have enabled maintenance for host '" + hostname + "'." 219 | 220 | # Check if we are safe to put a hypervisor in Maintenance 221 | safe = c.safeToPutInMaintenance(foundHostData.clusterid) 222 | if safe == False and force == 0: 223 | print "Error: All hosts should be in resouce state 'Enabled' before putting a host to maintenance. " \ 224 | "Use --force to to ignore WARNING states. Halting." 225 | sys.exit(1) 226 | elif safe == False and force == 1: 227 | print "Warning: To be safe, all hosts should be in resouce state 'Enabled' before putting a host to maintenance" 228 | print "Warning: You used --force to to ignore WARNING states. Assuming you know what you are doing.." 229 | else: 230 | print "Note: All resource states are 'Enabled', we can safely put one to maintenance" 231 | 232 | if DEBUG == 1: 233 | print "Debug: Host to put in maintenance: " + hostID 234 | 235 | # Migrate all vm's and empty hypervisor 236 | c.emptyHypervisor(hostID) 237 | 238 | # Put host in CloudStack Maintenance 239 | maintenanceresult = c.startMaintenance(hostID, hostname) 240 | if maintenanceresult: 241 | # Print overview 242 | c.printHypervisors(foundHostData.clusterid, poolmaster, checkBonds, foundHostData.hypervisor) 243 | print "Note: We're done!" 244 | sys.exit(0) 245 | elif DRYRUN == 0: 246 | print "Error: Could not enable Maintenance for host '" + hostname + "'. Please investigate manually. Halting." 247 | elif DRYRUN == 1: 248 | print "Note: We're done!" 249 | 250 | if DRYRUN == 0: 251 | # Print overview 252 | c.printHypervisors(foundHostData.clusterid, poolmaster, checkBonds, foundHostData.hypervisor) 253 | -------------------------------------------------------------------------------- /killJobs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2018, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to kill jobs related to instance_id 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstacksql 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | 34 | # Function to handle our arguments 35 | def handleArguments(argv): 36 | global DEBUG 37 | DEBUG = 0 38 | global DRYRUN 39 | DRYRUN = 1 40 | global mysqlHost 41 | mysqlHost = '' 42 | global mysqlPasswd 43 | mysqlPasswd = '' 44 | global hypervisorName 45 | hypervisorName = '' 46 | global plainDisplay 47 | plainDisplay = 0 48 | global onlyNonRunning 49 | onlyNonRunning = 0 50 | global instance_id 51 | instance_id = "" 52 | 53 | # Usage message 54 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 55 | '\n --mysqlserver -s \tSpecify MySQL server to ' \ 56 | 'read HA worker table from' + \ 57 | '\n --mysqlpassword \t\tSpecify password to cloud ' + \ 58 | 'MySQL user' + \ 59 | '\n --hostname -n \t\tLimit search to this hypervisor ' + \ 60 | 'hostname' + \ 61 | '\n --instance \t\t\tVM name' + \ 62 | '\n --debug\t\t\t\tEnable debug mode' + \ 63 | '\n --exec\t\t\t\tExecute for real (not needed for list* scripts)' 64 | 65 | try: 66 | opts, args = getopt.getopt(argv, "hs:i:", [ 67 | "mysqlserver=", 68 | "mysqlpassword=", 69 | "instance=", 70 | "debug", 71 | "exec" 72 | ]) 73 | except getopt.GetoptError as e: 74 | print "Error: " + str(e) 75 | print help 76 | sys.exit(2) 77 | for opt, arg in opts: 78 | if opt == '-h': 79 | print help 80 | sys.exit() 81 | elif opt in ("-s", "--mysqlserver"): 82 | mysqlHost = arg 83 | elif opt in ("-p", "--mysqlpassword"): 84 | mysqlPasswd = arg 85 | elif opt in ("-i", "--instance"): 86 | instance_id = arg 87 | elif opt in ("--debug"): 88 | DEBUG = 1 89 | elif opt in ("--exec"): 90 | DRYRUN = 0 91 | 92 | # We need at least these vars 93 | if len(mysqlHost) == 0: 94 | print help 95 | sys.exit() 96 | 97 | 98 | # Parse arguments 99 | if __name__ == "__main__": 100 | handleArguments(sys.argv[1:]) 101 | 102 | # Init our class 103 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 104 | 105 | if DEBUG == 1: 106 | print "Warning: Debug mode is enabled!" 107 | 108 | if DRYRUN == 1: 109 | print "Warning: dry-run mode is enabled, not running any commands!" 110 | 111 | # Connect MySQL 112 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 113 | if result > 0: 114 | print "Error: MySQL connection failed" 115 | sys.exit(1) 116 | elif DEBUG == 1: 117 | print "DEBUG: MySQL connection successful" 118 | print s.conn 119 | 120 | killJobResponse = s.kill_jobs_of_instance(instance_id=instance_id) 121 | print "Done" 122 | 123 | # Disconnect MySQL 124 | s.disconnectMySQL() 125 | -------------------------------------------------------------------------------- /kvm_check_bonds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bonds="$@" 4 | 5 | ERRORS=( ) 6 | GOOD=( ) 7 | 8 | for bond in $bonds; do 9 | state=$(sudo /bin/ovs-appctl bond/show ${bond}) 10 | 11 | if [ -z "${state}" ]; then 12 | ERRORS+=( "${bond} is missing" ) 13 | fi 14 | 15 | lacp_status=$(echo "${state}" | grep ^lacp_status | cut -f2 -d\ ) 16 | 17 | # check slaves 18 | slave_count=0 19 | while read slave_state; do 20 | slave=${slave_state//slave } 21 | slave=${slave//: *} 22 | slave_status=${slave_state//* } 23 | 24 | if [ "${slave_status}" == "enabled" ]; then 25 | ((slave_count++)) 26 | else 27 | ERRORS+=( "${bond}: ${slave} status ${slave_status}" ) 28 | fi 29 | done < <(echo "${state}" | grep ^slave) 30 | 31 | if [ "${lacp_status}" == "negotiated" ]; then 32 | if [ ${slave_count} -ge 2 ]; then 33 | GOOD+=( "${bond} ${slave_count} slaves enabled" ) 34 | else 35 | ERRORS+=( "${bond} only ${slave_count} slaves enabled" ) 36 | fi 37 | else 38 | ERRORS+=( "${bond} LACP status '${lacp_status}'" ) 39 | fi 40 | done 41 | 42 | if [ -z "${ERRORS}" -a -z "${GOOD}" ]; then 43 | echo "UNKNOWN: no bond found" 44 | exit 3 45 | elif [ -z "${ERRORS}" ]; then 46 | echo "OK: ${GOOD}" 47 | exit 0 48 | else 49 | echo "CRITICAL: ${ERRORS}" 50 | exit 2 51 | fi 52 | -------------------------------------------------------------------------------- /kvm_post_empty_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the post_empty_script you can customise." 4 | virsh list 5 | -------------------------------------------------------------------------------- /kvm_post_reboot_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the post_reboot_script you can customise." 4 | virsh list 5 | -------------------------------------------------------------------------------- /kvm_pre_empty_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This is the pre_empty_script you can customise." 4 | virsh list 5 | -------------------------------------------------------------------------------- /listHAWorkers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to list all VMs in a given cluster 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstacksql 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | 34 | # Function to handle our arguments 35 | def handleArguments(argv): 36 | global DEBUG 37 | DEBUG = 0 38 | global DRYRUN 39 | DRYRUN = 0 40 | global mysqlHost 41 | mysqlHost = '' 42 | global mysqlPasswd 43 | mysqlPasswd = '' 44 | global hypervisorName 45 | hypervisorName = '' 46 | global plainDisplay 47 | plainDisplay = 0 48 | global onlyNonRunning 49 | onlyNonRunning = 0 50 | global vmnameFilter 51 | vmnameFilter = "" 52 | 53 | # Usage message 54 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 55 | '\n --mysqlserver -s \tSpecify MySQL server to ' \ 56 | 'read HA worker table from' + \ 57 | '\n --mysqlpassword \t\tSpecify password to cloud ' + \ 58 | 'MySQL user' + \ 59 | '\n --hostname -n \t\tLimit search to this hypervisor ' + \ 60 | 'hostname' + \ 61 | '\n --name-filter \t\t\tFilter on this VM name' + \ 62 | '\n --non-running\t\t\t\tOnly show HA entries of VMs that ' + \ 63 | 'have non-running current state' + \ 64 | '\n --plain-display\t\t\tEnable plain display, no pretty tables' + \ 65 | '\n --debug\t\t\t\tEnable debug mode' + \ 66 | '\n --exec\t\t\t\tExecute for real (not needed for list* scripts)' 67 | 68 | try: 69 | opts, args = getopt.getopt(argv, "hs:n:", [ 70 | "mysqlserver=", 71 | "mysqlpassword=", 72 | "hostname=", 73 | "name-filter=", 74 | "non-running", 75 | "plain-display", 76 | "debug", 77 | "exec" 78 | ]) 79 | except getopt.GetoptError as e: 80 | print "Error: " + str(e) 81 | print help 82 | sys.exit(2) 83 | for opt, arg in opts: 84 | if opt == '-h': 85 | print help 86 | sys.exit() 87 | elif opt in ("-s", "--mysqlserver"): 88 | mysqlHost = arg 89 | elif opt in ("-p", "--mysqlpassword"): 90 | mysqlPasswd = arg 91 | elif opt in ("-n", "--hostname"): 92 | hypervisorName = arg 93 | elif opt in ("--name-filter"): 94 | vmnameFilter = arg 95 | elif opt in ("--plain-display"): 96 | plainDisplay = 1 97 | elif opt in ("--non-running"): 98 | onlyNonRunning = 1 99 | elif opt in ("--debug"): 100 | DEBUG = 1 101 | elif opt in ("--exec"): 102 | DRYRUN = 0 103 | 104 | # We need at least these vars 105 | if len(mysqlHost) == 0: 106 | print help 107 | sys.exit() 108 | 109 | # Parse arguments 110 | if __name__ == "__main__": 111 | handleArguments(sys.argv[1:]) 112 | 113 | # Init our class 114 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 115 | 116 | if DEBUG == 1: 117 | print "Warning: Debug mode is enabled!" 118 | 119 | if DRYRUN == 1: 120 | print "Warning: dry-run mode is enabled, not running any commands!" 121 | 122 | # Connect MySQL 123 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 124 | if result > 0: 125 | print "Error: MySQL connection failed" 126 | sys.exit(1) 127 | elif DEBUG == 1: 128 | print "DEBUG: MySQL connection successful" 129 | print s.conn 130 | 131 | haworkers = s.getHAWorkerData(hypervisorName) 132 | counter = 0 133 | t = PrettyTable([ 134 | "Domain", 135 | "VM", 136 | "Type", 137 | "VM state", 138 | "Created (-2H)", 139 | "HAworker step taken", 140 | "Step", 141 | "Hypervisor", 142 | "Mgt server" 143 | ]) 144 | t.align["VM"] = "l" 145 | 146 | if plainDisplay == 1: 147 | t.border = False 148 | t.header = False 149 | t.padding_width = 1 150 | 151 | for (domain,vmname, vm_type, state, created, taken, step, hypervisor, mgtname, 152 | hastate) in haworkers: 153 | if onlyNonRunning == 1 and state == "Running": 154 | continue 155 | if len(vmnameFilter) > 0 and vmname.find(vmnameFilter) < 0: 156 | continue 157 | if vmname == None: 158 | continue 159 | counter = counter + 1 160 | displayname = (vmname[:28] + '..') if len(vmname) >= 31 else vmname 161 | if mgtname is not None: 162 | mgtname = mgtname.split(".")[0] 163 | hvname = hypervisor.split(".")[0] 164 | t.add_row([ 165 | domain, 166 | displayname, 167 | vm_type, 168 | state, 169 | created, 170 | taken, 171 | step, 172 | hvname, 173 | mgtname 174 | ]) 175 | 176 | # Disconnect MySQL 177 | s.disconnectMySQL() 178 | print t.get_string(sortby="VM") 179 | 180 | if plainDisplay == 0: 181 | print "Note: Found " + str(counter) + " HA workers." 182 | -------------------------------------------------------------------------------- /listOrphanedDisks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to search primary storage pools for 'orphaned disks' and remove 23 | # them to free up space 24 | 25 | import sys 26 | import getopt 27 | import math 28 | import os.path 29 | 30 | from cloudstackops import cloudstackops 31 | from cloudstackops import cloudstackopsssh 32 | from cloudstackops.cloudstackstorage import StorageHelper 33 | 34 | from prettytable import PrettyTable 35 | 36 | 37 | def get_volume_filesize(file_uuid_in_cloudstack, *filelist): 38 | filelist, = filelist 39 | size = None 40 | for filepath in filelist.keys(): 41 | file_uuid_on_storagepool = filepath.split('/')[-1].split('.')[:1][0] 42 | 43 | if file_uuid_in_cloudstack == file_uuid_on_storagepool: 44 | size = int(filelist[filepath]) 45 | return size 46 | 47 | # Function to handle our arguments 48 | 49 | 50 | def handle_arguments(argv): 51 | global DEBUG 52 | DEBUG = 0 53 | global DRYRUN 54 | DRYRUN = 1 55 | global FORCE 56 | FORCE = 0 57 | global zone 58 | zone = '' 59 | global clusterarg 60 | clusterarg = '' 61 | global configProfileName 62 | configProfileName = '' 63 | 64 | # Usage message 65 | help = "Usage: " + os.path.basename(__file__) + ' [options] ' + \ 66 | '\n --config-profile -c \t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file) [required]' + \ 67 | '\n --zone -z \t\t\t\tZone Name [required]\t' + \ 68 | '\n --cluster -t \t\t\tCluster Name [optional]\t' + \ 69 | '\n --debug\t\t\t\t\tEnable debug mode [optional]' 70 | try: 71 | opts, args = getopt.getopt( 72 | argv, "hc:z:t:", ["config-profile=", "zone=", "clusterarg=", "debug"]) 73 | 74 | except getopt.GetoptError as e: 75 | print "Error: " + str(e) 76 | sys.exit(2) 77 | 78 | for opt, arg in opts: 79 | if opt == '-h': 80 | print help 81 | sys.exit() 82 | elif opt in ("-c", "--config-profile"): 83 | configProfileName = arg 84 | elif opt in ("--debug"): 85 | DEBUG = 1 86 | elif opt in ("-z", "--zone"): 87 | zone = arg 88 | elif opt in ("-t", "--cluster"): 89 | clusterarg = arg 90 | 91 | # Print help if required options not provided 92 | if len(configProfileName) == 0 or len(zone) == 0: 93 | print help 94 | exit(1) 95 | 96 | ## MAIN ## 97 | 98 | # Parse arguments 99 | if __name__ == "__main__": 100 | handle_arguments(sys.argv[1:]) 101 | 102 | # Init our classes 103 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN, FORCE) 104 | cs = cloudstackopsssh.CloudStackOpsSSH() 105 | 106 | # make credentials file known to our class 107 | c.configProfileName = configProfileName 108 | 109 | # Init the CloudStack API 110 | c.initCloudStackAPI() 111 | 112 | if DEBUG == 1: 113 | print "DEBUG: API address: " + c.apiurl 114 | print "DEBUG: ApiKey: " + c.apikey 115 | print "DEBUG: SecretKey: " + c.secretkey 116 | 117 | # Check cloudstack IDs 118 | if DEBUG == 1: 119 | print "DEBUG: Checking CloudStack IDs of provided input.." 120 | 121 | zoneid = c.getZoneId(zone) 122 | 123 | 124 | if zoneid is None: 125 | print "Cannot find zone " + zone 126 | exit(1) 127 | 128 | # get all clusters in zone if no cluster is given as input 129 | if clusterarg is None or clusterarg == '': 130 | 131 | clusters = c.listClusters({'zoneid': zoneid, 'listall': 'true'}) 132 | 133 | else: 134 | 135 | clusters = c.listClusters( 136 | {'zoneid': zoneid, 'name': clusterarg, 'listall': 'true'}) 137 | 138 | # die if there are no clusters found (unlikely) 139 | if clusters is None: 140 | print "DEBUG: No clusters found in zone" 141 | exit(1) 142 | 143 | 144 | # get a list of storage pools for each cluster 145 | t_storagepool = PrettyTable( 146 | ["Cluster", "Storage Pool", "Number of Orphaned disks", "Real Space used (GB)"]) 147 | 148 | for cluster in clusters: 149 | storagepools = [] 150 | storagepools.append(c.getStoragePool(cluster.id)) 151 | random_hypervisor = c.getHostsFromCluster(cluster.id).pop() 152 | # flatten storagepool list 153 | storagepools = [y for x in storagepools for y in x] 154 | 155 | # # if there are storage pools (should be) 156 | if len(storagepools) > 0: 157 | 158 | storagehelper = StorageHelper(debug=DEBUG) 159 | 160 | for storagepool in storagepools: 161 | used_space = 0 162 | 163 | # Get list of orphaned cloudstack disks for storagepool 164 | print "[INFO]: Retrieving list of orphans for storage pool", storagepool.name 165 | orphans = c.getDetachedVolumes(storagepool.id) 166 | 167 | storagepool_devicepath = storagepool.ipaddress + \ 168 | ":" + str(storagepool.path) 169 | 170 | # get filelist for storagepool via a 'random' hypervisor from 171 | # cluster 172 | primary_mountpoint = storagehelper.get_mountpoint( 173 | random_hypervisor.ipaddress, storagepool_devicepath) 174 | 175 | if primary_mountpoint is None: 176 | print "[DEBUG]: no physical volume list retrieved for " + storagepool.name + " skipping" 177 | storagepool_filelist = None 178 | 179 | else: 180 | storagepool_filelist = storagehelper.list_files( 181 | random_hypervisor.ipaddress, primary_mountpoint) 182 | 183 | t = PrettyTable(["Domain", "Account", "Name", "Cluster", "Storagepool", "Path", 184 | "Allocated Size (GB)", "Real Size (GB)", "Orphaned"]) 185 | 186 | for orphan in orphans: 187 | isorphaned = '' 188 | 189 | orphan_allocated_sizeGB = (orphan.size / math.pow(1024, 3)) 190 | 191 | if storagepool_filelist is None: 192 | orphan_real_sizeGB = 'n/a' 193 | isorphaned = '?' 194 | 195 | else: 196 | orphan_real_sizeGB = get_volume_filesize( 197 | orphan.path, storagepool_filelist) 198 | 199 | if orphan_real_sizeGB is not None: 200 | used_space += (orphan_real_sizeGB / 1024) 201 | orphan_real_sizeGB = format( 202 | (orphan_real_sizeGB / 1024), '.2f') 203 | isorphaned = 'Y' 204 | 205 | else: 206 | orphan_real_sizeGB = 0 207 | isorphaned = 'N' 208 | 209 | # add a row with orphan details 210 | t.add_row([orphan.domain, orphan.account, orphan.name, cluster.name, storagepool.name, orphan.path, 211 | orphan_allocated_sizeGB, orphan_real_sizeGB, isorphaned]) 212 | 213 | # Print orphan table 214 | print t.get_string() 215 | t_storagepool.add_row( 216 | [cluster.name, storagepool.name, len(orphans), format(used_space, '.2f')]) 217 | 218 | print "Storagepool Totals" 219 | print t_storagepool.get_string() 220 | -------------------------------------------------------------------------------- /listRunningJobs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to list all VMs in a given cluster 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstacksql 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | 34 | # Function to handle our arguments 35 | def handleArguments(argv): 36 | global DEBUG 37 | DEBUG = 0 38 | global DRYRUN 39 | DRYRUN = 0 40 | global mysqlHost 41 | mysqlHost = '' 42 | global mysqlPasswd 43 | mysqlPasswd = '' 44 | global hypervisorName 45 | hypervisorName = '' 46 | global plainDisplay 47 | plainDisplay = 0 48 | 49 | # Usage message 50 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 51 | '\n --mysqlserver -s \tSpecify MySQL server to ' + \ 52 | 'read HA worker table from' + \ 53 | '\n --mysqlpassword \t\tSpecify password to cloud ' \ 54 | 'MySQL user' + \ 55 | '\n --plain-display\t\t\tEnable plain display, no pretty tables' + \ 56 | '\n --debug\t\t\t\tEnable debug mode' + \ 57 | '\n --exec\t\t\t\tExecute for real (not needed for list* scripts)' 58 | 59 | try: 60 | opts, args = getopt.getopt(argv, "hs:n:", [ 61 | "mysqlserver=", 62 | "mysqlpassword=", 63 | "plain-display", 64 | "debug", 65 | "exec" 66 | ]) 67 | except getopt.GetoptError as e: 68 | print "Error: " + str(e) 69 | print help 70 | sys.exit(2) 71 | for opt, arg in opts: 72 | if opt == '-h': 73 | print help 74 | sys.exit() 75 | elif opt in ("-s", "--mysqlserver"): 76 | mysqlHost = arg 77 | elif opt in ("-p", "--mysqlpassword"): 78 | mysqlPasswd = arg 79 | elif opt in ("--plain-display"): 80 | plainDisplay = 1 81 | elif opt in ("--debug"): 82 | DEBUG = 1 83 | elif opt in ("--exec"): 84 | DRYRUN = 0 85 | 86 | # We need at least these vars 87 | if len(mysqlHost) == 0: 88 | print help 89 | sys.exit() 90 | 91 | # Parse arguments 92 | if __name__ == "__main__": 93 | handleArguments(sys.argv[1:]) 94 | 95 | # Init our class 96 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 97 | 98 | if DEBUG == 1: 99 | print "Warning: Debug mode is enabled!" 100 | 101 | if DRYRUN == 1: 102 | print "Warning: dry-run mode is enabled, not running any commands!" 103 | 104 | # Connect MySQL 105 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 106 | if result > 0: 107 | print "Error: MySQL connection failed" 108 | sys.exit(1) 109 | elif DEBUG == 1: 110 | print "DEBUG: MySQL connection successful" 111 | print s.conn 112 | 113 | asyncjobs = s.getAsyncJobData() 114 | counter = 0 115 | t = PrettyTable([ 116 | "username", 117 | "account_name", 118 | "instance_name", 119 | "vm_state", 120 | "job_cmd", 121 | "job_dispatcher", 122 | "Job created", 123 | "Mgt Server", 124 | "Job ID", 125 | "Related job ID" 126 | ]) 127 | t.align["instance_name"] = "l" 128 | 129 | if plainDisplay == 1: 130 | t.border = Falsusername, account_name, instance_id, vm_state, job_cmd, \ 131 | job_dispatcher, created, mgtserver, jobid, related 132 | t.header = False 133 | t.padding_width = 1 134 | 135 | for (username, account_name, instance_id, vm_state, job_cmd, job_dispatcher, 136 | created, mgtserver, jobid, related) in asyncjobs: 137 | counter = counter + 1 138 | t.add_row([ 139 | username, 140 | account_name, 141 | instance_id, 142 | vm_state, 143 | job_cmd, 144 | job_dispatcher, 145 | created, 146 | mgtserver, 147 | jobid, 148 | related 149 | ]) 150 | 151 | # Disconnect MySQL 152 | s.disconnectMySQL() 153 | print t.get_string(sortby="instance_name") 154 | 155 | if plainDisplay == 0: 156 | print "Note: Found " + str(counter) + " running jobs." 157 | -------------------------------------------------------------------------------- /listVolumes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to list all volumes in a given storage pool 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | # Function to handle our arguments 34 | 35 | 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 0 41 | global domainname 42 | domainname = '' 43 | global configProfileName 44 | configProfileName = '' 45 | global storagepoolname 46 | storagepoolname = '' 47 | global isProjectVm 48 | isProjectVm = 0 49 | 50 | # Usage message 51 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 52 | '\n --config-profile -c \t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 53 | '\n --storagepoolname -p \tList volumes from this storage pool' + \ 54 | '\n --is-projectvm\t\t\t\tLimit search to volumes that belong to a project' + \ 55 | '\n --debug\t\t\t\t\tEnable debug mode' + \ 56 | '\n --exec\t\t\t\t\tExecute for real (not needed for list* scripts)' 57 | 58 | try: 59 | opts, args = getopt.getopt( 60 | argv, "hc:p:", [ 61 | "config-profile=", "storagepoolname=", "debug", "exec", "is-projectvm"]) 62 | except getopt.GetoptError as e: 63 | print "Error: " + str(e) 64 | print help 65 | sys.exit(2) 66 | 67 | if len(opts) == 0: 68 | print help 69 | sys.exit(2) 70 | 71 | for opt, arg in opts: 72 | if opt == '-h': 73 | print help 74 | sys.exit() 75 | elif opt in ("-c", "--config-profile"): 76 | configProfileName = arg 77 | elif opt in ("-p", "--storagepoolname"): 78 | storagepoolname = arg 79 | elif opt in ("--debug"): 80 | DEBUG = 1 81 | elif opt in ("--exec"): 82 | DRYRUN = 0 83 | elif opt in ("--is-projectvm"): 84 | isProjectVm = 1 85 | 86 | # Default to cloudmonkey default config file 87 | if len(configProfileName) == 0: 88 | configProfileName = "config" 89 | 90 | # We need at least these vars 91 | if len(storagepoolname) == 0: 92 | print help 93 | sys.exit() 94 | 95 | # Parse arguments 96 | if __name__ == "__main__": 97 | handleArguments(sys.argv[1:]) 98 | 99 | # Handle project parameter 100 | if isProjectVm == 1: 101 | projectParam = "true" 102 | else: 103 | projectParam = "false" 104 | 105 | # Init our class 106 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 107 | 108 | if DEBUG == 1: 109 | print "# Warning: Debug mode is enabled!" 110 | 111 | if DRYRUN == 1: 112 | print "# Warning: dry-run mode is enabled, not running any commands!" 113 | 114 | # make credentials file known to our class 115 | c.configProfileName = configProfileName 116 | 117 | # Init the CloudStack API 118 | c.initCloudStackAPI() 119 | 120 | if DEBUG == 1: 121 | print "API address: " + c.apiurl 122 | print "ApiKey: " + c.apikey 123 | print "SecretKey: " + c.secretkey 124 | 125 | # Check cloudstack IDs 126 | if DEBUG == 1: 127 | print "Checking CloudStack IDs of provided input.." 128 | if len(storagepoolname) > 1: 129 | storagepoolID = c.checkCloudStackName( 130 | {'csname': storagepoolname, 'csApiCall': 'listStoragePools'}) 131 | 132 | # Get volumes from storage pool 133 | volumesData = c.listVolumes(storagepoolID, projectParam) 134 | 135 | # Empty line 136 | print 137 | t = PrettyTable(["VM name", "Volume name", "Instance name", "Volume path"]) 138 | t.align["VM"] = "l" 139 | 140 | counter = 0 141 | 142 | for volume in volumesData: 143 | counter = counter + 1 144 | 145 | # Attached? 146 | if volume.vmname is None: 147 | vmname = instancename = "NotAttached" 148 | else: 149 | vmname = ( 150 | volume.vmname[:20] + 151 | '..') if len( 152 | volume.vmname) >= 22 else volume.vmname 153 | virtualmachineData = c.getVirtualmachineData(volume.virtualmachineid) 154 | vm = virtualmachineData[0] 155 | instancename = vm.instancename 156 | 157 | # Table 158 | t.add_row([vmname, volume.name, instancename, volume.path + ".vhd"]) 159 | 160 | # Display table 161 | print t 162 | 163 | if DEBUG == 1: 164 | print "Note: We're done!" 165 | -------------------------------------------------------------------------------- /liveMigrateHVtoPOD.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2017, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to empty/live migrate a HV to another cluster 23 | # Daan de Goede 24 | 25 | import sys 26 | import getopt 27 | from cloudstackops import cloudstackops 28 | from cloudstackops import cloudstacksql 29 | from cloudstackops import kvm 30 | import os.path 31 | from datetime import datetime 32 | import time 33 | import liveMigrateVirtualMachine as lmvm 34 | 35 | # Function to handle our arguments 36 | 37 | 38 | def handleArguments(argv): 39 | global DEBUG 40 | DEBUG = 0 41 | global DRYRUN 42 | DRYRUN = 1 43 | global fromHV 44 | fromHV = '' 45 | global toCluster 46 | toCluster = '' 47 | global configProfileName 48 | configProfileName = '' 49 | global force 50 | force = 0 51 | global zwps2cwps 52 | zwps2cwps = False 53 | global affinityGroupToAdd 54 | affinityGroupToAdd = '' 55 | global destination_dc_name 56 | destination_dc_name = '' 57 | 58 | # Usage message 59 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 60 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 61 | '\n --hypervisor -h \t\tHypervisor to migrate' + \ 62 | '\n --tocluster -t \t\tMigrate router to this cluster' + \ 63 | '\n --destinationdc -d \t\tSpecify name of DC to migrate to' + \ 64 | '\n --debug\t\t\t\tEnable debug mode' + \ 65 | '\n --exec\t\t\t\tExecute for real' 66 | 67 | try: 68 | opts, args = getopt.getopt( 69 | argv, "c:h:t:d:", [ 70 | "config-profile=", "hypervisor=", "tocluster=", "debug", "exec", "force", "destinationdc="]) 71 | except getopt.GetoptError as e: 72 | print "Error: " + str(e) 73 | print help 74 | sys.exit(2) 75 | for opt, arg in opts: 76 | if opt == '--help': 77 | print help 78 | sys.exit() 79 | elif opt in ("-c", "--config-profile"): 80 | configProfileName = arg 81 | elif opt in ("-h", "--hypervisor"): 82 | fromHV = arg 83 | elif opt in ("-t", "--tocluster"): 84 | toCluster = arg 85 | elif opt in ("--debug"): 86 | DEBUG = 1 87 | elif opt in ("--exec"): 88 | DRYRUN = 0 89 | elif opt in ("--force"): 90 | force = 1 91 | elif opt in ("-d", "--destinationdc"): 92 | destination_dc_name = arg 93 | 94 | # Default to cloudmonkey default config file 95 | if len(configProfileName) == 0: 96 | configProfileName = "config" 97 | 98 | # We need at least these vars 99 | if len(fromHV) == 0 or len(toCluster) == 0: 100 | print help 101 | sys.exit() 102 | 103 | def liveMigrateHVtoPOD(DEBUG=0, DRYRUN=1, fromHV='', toCluster='', configProfileName='', force=0): 104 | # Init our class 105 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 106 | c.task = "Live Migrate HV to new POD" 107 | c.slack_custom_title = "Domain" 108 | c.slack_custom_value = "" 109 | 110 | # Start time 111 | print "Note: Starting @ %s" % time.strftime("%Y-%m-%d %H:%M migration HV to POD") 112 | start_time = datetime.now() 113 | 114 | if DEBUG == 1: 115 | print "Warning: Debug mode is enabled!" 116 | 117 | if DRYRUN == 1: 118 | print "Warning: dry-run mode is enabled, not running any commands!" 119 | 120 | # make credentials file known to our class 121 | c.configProfileName = configProfileName 122 | 123 | # Init the CloudStack API 124 | c.initCloudStackAPI() 125 | 126 | if DEBUG == 1: 127 | print "API address: " + c.apiurl 128 | print "ApiKey: " + c.apikey 129 | print "SecretKey: " + c.secretkey 130 | 131 | # Check hypervisor parameter 132 | if DEBUG == 1: 133 | print "Note: Checking host ID of provided hypervisor.." 134 | 135 | host = c.getHostByName(name=fromHV) 136 | if not host or len(host) == 0 or host['count'] != 1: 137 | print "hypervisor parameter ('" + fromHV + "') resulted in zero or more than one hypervisors!" 138 | sys.exit(1) 139 | 140 | if DEBUG == 1: 141 | print host 142 | 143 | hostid = host['host'][0]['id'] 144 | listVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true'}) 145 | listProjectVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true', 'projectid': -1}) 146 | 147 | VMs = {} 148 | VMs['count'] = listVMs.get('count',0) + listProjectVMs.get('count',0) 149 | VMs['virtualmachine'] = listVMs.get('virtualmachine',[]) + listProjectVMs.get('virtualmachine',[]) 150 | if DEBUG == 1: 151 | print VMs 152 | vmCount = 1 153 | vmTotal = VMs.get('count',0) 154 | print "found " + str(vmTotal) + " virtualmachines on hypervisor: " + fromHV 155 | for vm in VMs.get('virtualmachine',[]): 156 | print "=================================================== Migrating vm %s of %s ===" % (vmCount, vmTotal) 157 | print "Virtualmachine: " + vm['name'] 158 | if DEBUG == 1: 159 | print vm 160 | isProjectVm = 0 161 | if 'projectid' in vm.keys(): 162 | isProjectVm = 1 163 | # perform the actual migration of a VM to the new cluster 164 | lmvm.liveMigrateVirtualMachine(c, DEBUG, DRYRUN, vm['instancename'], toCluster, configProfileName, isProjectVm, force, zwps2cwps, destination_dc_name, affinityGroupToAdd, multirun=True) 165 | vmCount += 1 166 | 167 | result = True 168 | listVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true'}) 169 | listProjectVMs = c.listVirtualMachines({'hostid': hostid, 'listall': 'true', 'projectid': -1}) 170 | 171 | vmCount = listVMs.get('count',0) + listProjectVMs.get('count',0) 172 | if DEBUG == 1: 173 | print VMs 174 | if vmCount > 0: 175 | result = False 176 | 177 | # End time 178 | message = "Finished @ " + time.strftime("%Y-%m-%d %H:%M migration HV to POD") 179 | c.print_message(message=message, message_type="Note", to_slack=False) 180 | elapsed_time = datetime.now() - start_time 181 | 182 | if result: 183 | print "HV %s is successfully migrated to cluster %s in %s seconds" % (fromHV, toCluster, elapsed_time.total_seconds()) 184 | else: 185 | print "HV %s has failed to migrate to cluster %s in %s seconds, %s of %s remaining." % (fromHV, toCluster, elapsed_time.total_seconds(), vmCount, vmTotal) 186 | 187 | # Parse arguments 188 | if __name__ == "__main__": 189 | handleArguments(sys.argv[1:]) 190 | liveMigrateHVtoPOD(DEBUG, DRYRUN, fromHV, toCluster, configProfileName, force) 191 | -------------------------------------------------------------------------------- /liveMigrateRouter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to live migrate riuter VM to another host in the same cluster 23 | # Mainly used to work-around an old XenServer 6.2 bug 24 | # Remi Bergsma - rbergsma@schubergphilis.com 25 | 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | # Function to handle our arguments 31 | 32 | 33 | def handleArguments(argv): 34 | global DEBUG 35 | DEBUG = 0 36 | global DRYRUN 37 | DRYRUN = 1 38 | global vmname 39 | vmname = '' 40 | global toCluster 41 | toCluster = '' 42 | global configProfileName 43 | configProfileName = '' 44 | global isProjectVm 45 | isProjectVm = 0 46 | 47 | # Usage message 48 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 49 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 50 | '\n --routerinstance-name -r \tWork with this router (r-12345-VM)' + \ 51 | '\n --is-projectrouter\t\t\tThe specified router belongs to a project' + \ 52 | '\n --debug\t\t\t\tEnable debug mode' + \ 53 | '\n --exec\t\t\t\tExecute for real' 54 | 55 | try: 56 | opts, args = getopt.getopt( 57 | argv, "hc:r:p", [ 58 | "config-profile=", "routerinstance-name=", "debug", "exec", "is-projectrouter", "only-when-required"]) 59 | except getopt.GetoptError: 60 | print help 61 | sys.exit(2) 62 | for opt, arg in opts: 63 | if opt == '-h': 64 | print help 65 | sys.exit() 66 | elif opt in ("-c", "--config-profile"): 67 | configProfileName = arg 68 | elif opt in ("-r", "--routerinstance-name"): 69 | vmname = arg 70 | elif opt in ("--debug"): 71 | DEBUG = 1 72 | elif opt in ("--exec"): 73 | DRYRUN = 0 74 | elif opt in ("--is-projectrouter"): 75 | isProjectVm = 1 76 | 77 | # Default to cloudmonkey default config file 78 | if len(configProfileName) == 0: 79 | configProfileName = "config" 80 | 81 | # We need at least these vars 82 | if len(vmname) == 0: 83 | print help 84 | sys.exit() 85 | 86 | # Parse arguments 87 | if __name__ == "__main__": 88 | handleArguments(sys.argv[1:]) 89 | 90 | # Init our class 91 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 92 | 93 | if DEBUG == 1: 94 | print "Warning: Debug mode is enabled!" 95 | 96 | if DRYRUN == 1: 97 | print "Warning: dry-run mode is enabled, not running any commands!" 98 | 99 | # make credentials file known to our class 100 | c.configProfileName = configProfileName 101 | 102 | # Init the CloudStack API 103 | c.initCloudStackAPI() 104 | 105 | if DEBUG == 1: 106 | print "DEBUG: API address: " + c.apiurl 107 | print "DEBUG: ApiKey: " + c.apikey 108 | print "DEBUG: SecretKey: " + c.secretkey 109 | 110 | # Check cloudstack IDs 111 | if DEBUG == 1: 112 | print "DEBUG: Checking CloudStack IDs of provided input.." 113 | 114 | if isProjectVm == 1: 115 | projectParam = "true" 116 | else: 117 | projectParam = "false" 118 | 119 | # check routerID 120 | routerID = c.checkCloudStackName({'csname': vmname, 121 | 'csApiCall': 'listRouters', 122 | 'listAll': 'true', 123 | 'isProjectVm': projectParam}) 124 | 125 | # get router data 126 | routerData = c.getRouterData({'name': vmname, 'isProjectVm': projectParam}) 127 | router = routerData[0] 128 | 129 | if DEBUG == 1: 130 | print routerData 131 | 132 | print "Note: Found router " + router.name + " that belongs to account " + str(router.account) + " with router ID " + router.id 133 | print "Note: This router has " + str(len(router.nic)) + " nics." 134 | 135 | print "Note: Let's live migrate the router VM.." 136 | 137 | # Reboot router 138 | if DRYRUN == 1: 139 | print "Note: Would have live migrated router " + router.name + " (" + router.id + ")" 140 | else: 141 | print "Executing: live migrate router " + router.name + " (" + router.id + ")" 142 | 143 | result = c.migrateSystemVm({'vmid': router.id, 'projectParam': projectParam}) 144 | if result == 1: 145 | print "Live migrating failed, will try again!" 146 | result = c.migrateSystemVm({'vmid': router.id, 'projectParam': projectParam}) 147 | if result == 1: 148 | print "live migrating failed again -- exiting." 149 | print "Error: investigate manually!" 150 | 151 | print "Note: We're done!" 152 | -------------------------------------------------------------------------------- /marvin/Marvin-0.1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MissionCriticalCloud/cloudstackOps/cc21338b8f40437e52c309b2ea4d49e4bd3262ba/marvin/Marvin-0.1.0.tar.gz -------------------------------------------------------------------------------- /marvin/README.md: -------------------------------------------------------------------------------- 1 | Setup Marvin 2 | === 3 | To talk to the CloudStack API, these scripts use `Marvin` that comes with `Apache CloudStack`. Unfortunately, Marvin has changed quite a few times. Without backwards compatibility that is. Therefore, use the one in this repository and you're fine. Support for the latest version is being worked on, but that takes some time. 4 | 5 | Install the `tar.gz` using `pip` 6 | 7 | `pip install -Iv marvin/Marvin-0.1.0.tar.gz` 8 | 9 | -------------------------------------------------------------------------------- /migrateIsolatedNetworkToVPC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import getopt 4 | import os.path 5 | import sys 6 | import time 7 | from datetime import datetime 8 | 9 | from cloudstackops import cloudstackops 10 | from cloudstackops import cloudstacksql 11 | 12 | 13 | # Function to handle our arguments 14 | 15 | 16 | def handleArguments(argv): 17 | global DEBUG 18 | DEBUG = 0 19 | global DRYRUN 20 | DRYRUN = 1 21 | global networkname 22 | networkname = '' 23 | global networkuuid 24 | networkuuid = '' 25 | global vpcofferingname 26 | vpcofferingname = '' 27 | global networkofferingname 28 | networkofferingname = '' 29 | global configProfileName 30 | configProfileName = '' 31 | global force 32 | force = 0 33 | global threads 34 | threads = 5 35 | global mysqlHost 36 | mysqlHost = '' 37 | global mysqlPasswd 38 | mysqlPasswd = '' 39 | 40 | # Usage message 41 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 42 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from ' \ 43 | '(or specify in ./config file)' + \ 44 | '\n --network-name -n \tMigrate Isolated network with this name.' \ 45 | '\n --uuid -u \t\t\tThe UUID of the network. When provided, the network name will be ignored.' \ 46 | '\n --vpc-offering -v \tThe name of the VPC offering.' \ 47 | '\n --network-offering -o \tThe name of the VPC tier network offering.' \ 48 | '\n --mysqlserver -s \tSpecify MySQL server config section name' + \ 49 | '\n --mysqlpassword \t\tSpecify password to cloud MySQL user' + \ 50 | '\n --debug\t\t\t\tEnable debug mode' + \ 51 | '\n --exec\t\t\t\tExecute for real' 52 | 53 | try: 54 | opts, args = getopt.getopt( 55 | argv, "hc:n:u:v:o:t:p:s:b:", [ 56 | "config-profile=", "network-name=", "uuid=", "vpc-offering=", "network-offering=", "mysqlserver=", 57 | "mysqlpassword=", "debug", "exec", "force" 58 | ]) 59 | except getopt.GetoptError as e: 60 | print "Error: " + str(e) 61 | print help 62 | sys.exit(2) 63 | for opt, arg in opts: 64 | if opt == '-h': 65 | print help 66 | sys.exit() 67 | elif opt in ("-c", "--config-profile"): 68 | configProfileName = arg 69 | elif opt in ("-n", "--network-name"): 70 | networkname = arg 71 | elif opt in ("-u", "--uuid"): 72 | networkuuid = arg 73 | elif opt in ("-v", "--vpc-offering"): 74 | vpcofferingname = arg 75 | elif opt in ("-o", "--network-offering"): 76 | networkofferingname = arg 77 | elif opt in ("-s", "--mysqlserver"): 78 | mysqlHost = arg 79 | elif opt in ("-p", "--mysqlpassword"): 80 | mysqlPasswd = arg 81 | elif opt in ("--debug"): 82 | DEBUG = 1 83 | elif opt in ("--exec"): 84 | DRYRUN = 0 85 | elif opt in ("--force"): 86 | force = 1 87 | 88 | # Default to cloudmonkey default config file 89 | if len(configProfileName) == 0: 90 | configProfileName = "config" 91 | 92 | # We need at least these vars 93 | if (len(networkname) == 0 and len(networkuuid) == 0) or len(mysqlHost) == 0 or len(vpcofferingname) == 0 or \ 94 | len(networkofferingname) == 0: 95 | print "networkname: " + networkname 96 | print "networkuuid: " + networkuuid 97 | print "mysqlHost: " + mysqlHost 98 | print "vpcofferingname: " + vpcofferingname 99 | print "networkofferingname: " + networkofferingname 100 | print "Required parameter not passed!" 101 | print help 102 | sys.exit() 103 | 104 | 105 | def exit_script(message): 106 | print "Fatal Error: %s" % message 107 | sys.exit(1) 108 | 109 | 110 | # Parse arguments 111 | if __name__ == "__main__": 112 | handleArguments(sys.argv[1:]) 113 | 114 | # Start time 115 | print "Note: Starting @ %s" % time.strftime("%Y-%m-%d %H:%M") 116 | start_time = datetime.now() 117 | 118 | if DEBUG == 1: 119 | print "Warning: Debug mode is enabled!" 120 | 121 | if DRYRUN == 1: 122 | print "Warning: dry-run mode is enabled, not running any commands!" 123 | 124 | # Init CloudStackOps class 125 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 126 | c.task = "Isolated network -> VPC migration" 127 | c.slack_custom_title = "Migration details" 128 | 129 | # Init SQL class 130 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 131 | 132 | # Connect MySQL 133 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 134 | if result > 0: 135 | message = "MySQL connection failed" 136 | c.print_message(message=message, message_type="Error", to_slack=True) 137 | sys.exit(1) 138 | elif DEBUG == 1: 139 | print "DEBUG: MySQL connection successful" 140 | print s.conn 141 | 142 | # make credentials file known to our class 143 | c.configProfileName = configProfileName 144 | 145 | # Init the CloudStack API 146 | c.initCloudStackAPI() 147 | 148 | if not networkuuid: 149 | networkuuid = c.checkCloudStackName({ 150 | 'csname': networkname, 151 | 'csApiCall': 'listNetworks', 152 | 'listAll': 'true', 153 | 'isProjectVm': False 154 | }) 155 | 156 | if DEBUG == 1: 157 | print "API address: " + c.apiurl 158 | print "ApiKey: " + c.apikey 159 | print "SecretKey: " + c.secretkey 160 | print "Username: " + c.username 161 | print "Password: " + c.password 162 | 163 | # Check cloudstack IDs 164 | if DEBUG == 1: 165 | print "Debug: Checking CloudStack IDs of provided input.." 166 | print "Network UUID: %s" % networkuuid 167 | 168 | # Get Isolated network details 169 | isolated_network = c.listNetworks(networkuuid)[0] 170 | isolated_network_db_id = s.get_network_db_id(networkuuid) 171 | 172 | # Pretty Slack messages 173 | c.instance_name = isolated_network.name 174 | c.slack_custom_title = "Network" 175 | c.slack_custom_value = isolated_network.name 176 | c.zone_name = isolated_network.zonename 177 | c.task = "Converting legacy network to VPC tier" 178 | 179 | to_slack = True 180 | if DRYRUN == 1: 181 | to_slack = False 182 | 183 | # Pre-flight checks 184 | # 1. Check if network is actually already an VPC tier 185 | if s.check_if_network_is_vpc_tier(isolated_network_db_id): 186 | message = "Network '%s' is already part of a VPC. Nothing to do!" % isolated_network.name 187 | c.print_message(message=message, message_type="Note", to_slack=to_slack) 188 | exit(1) 189 | 190 | # 2 Gather VPC / network offering 191 | vpc_offering_db_id = s.get_vpc_offering_id(vpcofferingname) 192 | vpc_tier_offering_db_id = s.get_network_offering_id(networkofferingname) 193 | 194 | if DRYRUN: 195 | message = "Would have migrated classic network '%s' to a VPC!" % isolated_network.name 196 | c.print_message(message=message, message_type="Note", to_slack=to_slack) 197 | exit(0) 198 | 199 | message = "Starting migration of classic network '%s' to VPC" % isolated_network.name 200 | c.print_message(message=message, message_type="Note", to_slack=to_slack) 201 | 202 | # Migration 203 | # 1. Create the new VPC 204 | vpc_db_id = s.create_vpc(isolated_network_db_id, vpc_offering_db_id) 205 | 206 | # 2. Fill the vpc service map 207 | s.fill_vpc_service_map(vpc_db_id) 208 | 209 | # 3. Update network service map 210 | s.migrate_ntwk_service_map_from_isolated_network_to_vpc(isolated_network_db_id) 211 | 212 | # 4. Create network acl from isolated network egress for vpc 213 | egress_network_acl_db_id = s.create_network_acl_for_vpc(vpc_db_id, networkuuid + '-fwrules') 214 | 215 | # TODO Think about default deny / default allow 216 | # 5. Fill egress network acl 217 | s.convert_isolated_network_egress_rules_to_network_acl(isolated_network_db_id, egress_network_acl_db_id) 218 | 219 | # 6. Update network to become a VPC tier 220 | s.update_isolated_network_to_be_a_vpc_tier(vpc_db_id, egress_network_acl_db_id, vpc_tier_offering_db_id, 221 | isolated_network_db_id) 222 | 223 | # 7. Migrate public ips to vpc 224 | ipadresses = s.get_all_ipaddresses_from_network(isolated_network_db_id) 225 | for ipaddress in ipadresses: 226 | # Create ACL for ingress rules 227 | # ingress_acl_db_id = s.create_network_acl_for_vpc(vpc_db_id, ipaddress[1] + '-ingress_rules') 228 | 229 | # Fill ingress ACL with rules 230 | s.convert_isolated_network_public_ip_rules_to_network_acl(ipaddress[0], egress_network_acl_db_id) 231 | 232 | # Migrate public ip to vpc 233 | s.migrate_public_ip_from_isolated_network_to_vpc(vpc_db_id, 2, ipaddress[0]) 234 | 235 | # HACK Update Egress cidrs to be allow all 236 | s.fix_egress_cidr_allow_all(egress_network_acl_db_id) 237 | 238 | # 8. Migrate routers 239 | s.migrate_routers_from_isolated_network_to_vpc(vpc_db_id, isolated_network_db_id) 240 | 241 | message = "Migration of classic network '%s' to VPC succeeded! Restart+Cleanup needed to complete migration!"\ 242 | % isolated_network.name 243 | c.print_message(message=message, message_type="Note", to_slack=to_slack) 244 | -------------------------------------------------------------------------------- /migrateOfflineVolumes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to migrate offline volumes to a new storage (to a new cluster) 23 | # i.e. from clusterX/randomStorageX to clusterY/RandomStorageY) 24 | # Remi Bergsma - rbergsma@schubergphilis.com 25 | 26 | import time 27 | import sys 28 | import getopt 29 | from cloudstackops import cloudstackops 30 | import os.path 31 | from random import choice 32 | from prettytable import PrettyTable 33 | 34 | # Function to handle our arguments 35 | 36 | 37 | def handleArguments(argv): 38 | global DEBUG 39 | DEBUG = 0 40 | global DRYRUN 41 | DRYRUN = 1 42 | global fromCluster 43 | fromCluster = '' 44 | global toCluster 45 | toCluster = '' 46 | global configProfileName 47 | configProfileName = '' 48 | global isProjectVm 49 | isProjectVm = 0 50 | 51 | # Usage message 52 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 53 | '\n --config-profile -c \t\tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 54 | '\n --oncluster -o \t\t\tMigrate volumes on this cluster' + \ 55 | '\n --tocluster -t \t\t\tMigrate volumes to this cluster' + \ 56 | '\n --is-projectvm\t\t\t\tLimit search to volumes that belong to a project' + \ 57 | '\n --debug\t\t\t\t\tEnable debug mode' + \ 58 | '\n --exec\t\t\t\t\tExecute for real' 59 | 60 | try: 61 | opts, args = getopt.getopt( 62 | argv, "hc:o:t:p", [ 63 | "config-profile=", "oncluster=", "tocluster=", "debug", "exec", "is-projectvm"]) 64 | except getopt.GetoptError as e: 65 | print "Error: " + str(e) 66 | print help 67 | sys.exit(2) 68 | for opt, arg in opts: 69 | if opt == '-h': 70 | print help 71 | sys.exit() 72 | elif opt in ("-c", "--config-profile"): 73 | configProfileName = arg 74 | elif opt in ("-o", "--oncluster"): 75 | fromCluster = arg 76 | elif opt in ("-t", "--tocluster"): 77 | toCluster = arg 78 | elif opt in ("--debug"): 79 | DEBUG = 1 80 | elif opt in ("--exec"): 81 | DRYRUN = 0 82 | elif opt in ("--is-projectvm"): 83 | isProjectVm = 1 84 | 85 | # Default to cloudmonkey default config file 86 | if len(configProfileName) == 0: 87 | configProfileName = "config" 88 | 89 | # We need at least these vars 90 | if len(fromCluster) == 0 or len(toCluster) == 0: 91 | print help 92 | sys.exit() 93 | 94 | # Parse arguments 95 | if __name__ == "__main__": 96 | handleArguments(sys.argv[1:]) 97 | 98 | # Init our class 99 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 100 | 101 | if DEBUG == 1: 102 | print "Warning: Debug mode is enabled!" 103 | 104 | if DRYRUN == 1: 105 | print "Note: dry-run mode is enabled, not running any commands!" 106 | 107 | # make credentials file known to our class 108 | c.configProfileName = configProfileName 109 | 110 | # Init the CloudStack API 111 | c.initCloudStackAPI() 112 | 113 | if DEBUG == 1: 114 | print "API address: " + c.apiurl 115 | print "ApiKey: " + c.apikey 116 | print "SecretKey: " + c.secretkey 117 | 118 | # Check cloudstack IDs 119 | if DEBUG == 1: 120 | print "Note: Checking CloudStack IDs of provided input.." 121 | 122 | if isProjectVm == 1: 123 | projectParam = "true" 124 | else: 125 | projectParam = "false" 126 | 127 | fromClusterID = c.checkCloudStackName( 128 | {'csname': fromCluster, 'csApiCall': 'listClusters'}) 129 | toClusterID = c.checkCloudStackName( 130 | {'csname': toCluster, 'csApiCall': 'listClusters'}) 131 | 132 | # Select storage pool 133 | fromStorageID = c.getRandomStoragePool(fromClusterID) 134 | toStorageID = c.getRandomStoragePool(toClusterID) 135 | 136 | # Get storage pool data 137 | result = c.getStoragePoolData(fromStorageID) 138 | fromStorage = result[0].name 139 | result = c.getStoragePoolData(toStorageID) 140 | toStorage = result[0].name 141 | 142 | # Figure out how many volumes we have 143 | volumes = c.listVolumes(fromStorageID, isProjectVm) 144 | 145 | # Init vars 146 | size = 0 147 | tsize = 0 148 | volumesToMigrate = {} 149 | count = 0 150 | 151 | # Read volumes we should ignore 152 | if os.path.isfile('ignore_volumes.txt'): 153 | ignoreVolumes = [] 154 | ignoreVolumes = [line.strip() for line in open('ignore_volumes.txt')] 155 | if DEBUG == 1: 156 | print "Debug: Ignoring these volumes: %s" % (ignoreVolumes) 157 | else: 158 | print "Note: Ignore file 'ignore_volumes.txt' not found, so no volumes will be ignored." 159 | ignoreVolumes = [] 160 | 161 | # loop volumes 162 | for volume in volumes: 163 | tsize = tsize + (volume.size / 1024 / 1024 / 1024) 164 | # We need a storage attribute to be able to migrate -- otherwise it's 165 | # probably just allocated and not ready yet 166 | if volume.id in ignoreVolumes: 167 | print "Debug: Ignorning volume id %s because it is on the ignore_volumes.txt list!" % (volume.id) 168 | elif hasattr(volume, 'storage'): 169 | # No need to migrate if we're already on target 170 | if volume.storage == toStorage: 171 | if DEBUG == 1: 172 | print "Debug: volume %s with name %s is already on storage %s -- ignoring!" % (volume.id, volume.name, volume.storage) 173 | # Only manage this hypervisor 174 | else: 175 | if volume.state == 'Ready': 176 | if hasattr(volume, 'vmstate') and volume.vmstate == 'Stopped': 177 | # Mark this volume for migration 178 | volumesToMigrate[count] = volume 179 | count = count + 1 180 | if DEBUG == 1: 181 | print "Note: Will migrate because volume %s is attached to non-running VM: %s %s %s" % (volume.id, volume.name, volume.state, volume.storage) 182 | print volume 183 | size = size + (volume.size / 1024 / 1024 / 1024) 184 | # Check if volume is attached to a vm 185 | elif volume.vmstate is not None: 186 | if DEBUG == 1: 187 | print "Debug: volume %s is in attached to %s VM -- ignoring!" % (volume.id, volume.vmstate) 188 | else: 189 | # Mark this volume for migration 190 | volumesToMigrate[count] = volume 191 | count = count + 1 192 | if DEBUG == 1: 193 | print "Note: will migrate because volume %s is not attached to running VM: %s %s %s" % (volume.id, volume.name, volume.state, volume.storage) 194 | print volume 195 | size = size + (volume.size / 1024 / 1024 / 1024) 196 | elif DEBUG == 1: 197 | print "Debug: volume %s is in state %s -- ignoring!" % (volume.id, volume.state) 198 | elif DEBUG == 1: 199 | print "Debug: no storage attribute found for volume id %s with name %s and state %s -- ignoring!" % (volume.id, volume.name, volume.state) 200 | 201 | # Display sizes 202 | if DEBUG == 1: 203 | print size 204 | print tsize 205 | print "Debug: Overview of volumes to migrate:" 206 | print volumesToMigrate 207 | 208 | # Define table 209 | t = PrettyTable(["Volume name", 210 | "Attached to VM", 211 | "Type", 212 | "Volume state", 213 | "Size", 214 | "Account", 215 | "Domain"]) 216 | t.align["Volume name"] = "l" 217 | 218 | # Volumes to migrate 219 | if len(volumesToMigrate) > 0: 220 | print "Note: Overview of volumes to migrate to storage pool " + toStorage + ":" 221 | counter = 0 222 | for x, vol in volumesToMigrate.items(): 223 | counter = counter + 1 224 | if vol.account is not None: 225 | volaccount = vol.account 226 | else: 227 | volaccount = "Unknown" 228 | 229 | if vol.vmname is not None: 230 | volvmname = ( 231 | vol.vmname[:22] + 232 | '..') if len( 233 | vol.vmname) > 24 else vol.vmname 234 | else: 235 | volvmname = "None" 236 | 237 | if vol.name is not None: 238 | volname = ( 239 | vol.name[:22] + 240 | '..') if len( 241 | vol.name) > 24 else vol.name 242 | else: 243 | volname = "None" 244 | 245 | # Print overview table 246 | t.add_row([volname, 247 | volvmname, 248 | vol.type, 249 | vol.state, 250 | str(vol.size / 1024 / 1024 / 1024), 251 | volaccount, 252 | vol.domain]) 253 | 254 | if DRYRUN != 1: 255 | # Execute the commands 256 | print "Executing: migrate volume " + vol.id + " to storage " + toStorageID 257 | result = c.migrateVolume(vol.id, toStorageID) 258 | if result == 1: 259 | print "Migrate failed -- exiting." 260 | print "Error: investegate manually!" 261 | # Notify user 262 | msgSubject = 'Warning: problem with maintenance for volume ' + \ 263 | vol.name + ' / ' + vol.id 264 | emailbody = "Could not migrate volume " + vol.id 265 | c.sendMail(c.mail_from, c.errors_to, msgSubject, emailbody) 266 | continue 267 | 268 | if result.volume.state == "Ready": 269 | print "Note: " + result.volume.name + " is migrated successfully " 270 | else: 271 | warningMsg = "Warning: " + result.volume.name + " is in state " + \ 272 | result.volume.state + " instead of Ready. Please investigate!" 273 | print warningMsg 274 | msgSubject = 'Warning: problem with maintenance for volume ' + \ 275 | vol.name + ' / ' + vol.id 276 | emailbody = warningMsg 277 | c.sendMail(c.mail_from, c.errors_to, msgSubject, emailbody) 278 | 279 | # Display table 280 | print t 281 | 282 | if DRYRUN == 1: 283 | print "Total size of volumes to migrate: " + str(size) + " GB" 284 | 285 | else: 286 | print "Note: Nothing to migrate at this time." 287 | -------------------------------------------------------------------------------- /rebalanceOSTypesOnCluster.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to rebalance OS types on a cluster 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys, getopt 27 | from cloudstackops import cloudstackops 28 | import os.path 29 | from random import choice 30 | from prettytable import PrettyTable 31 | from datetime import date 32 | import re 33 | import operator 34 | 35 | # Function to handle our arguments 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 1 41 | global configProfileName 42 | configProfileName = '' 43 | global isProjectVm 44 | isProjectVm = 0 45 | global clusterName 46 | clusterName = '' 47 | 48 | # Usage message 49 | help = "Usage: " + os.path.basename(__file__) + ' --config-profile|-c -n [--debug --exec --is-projectvm]' 50 | 51 | try: 52 | opts, args = getopt.getopt(argv,"hc:n:p:",["config-profile=","cluster","debug","exec","is-projectvm"]) 53 | except getopt.GetoptError as e: 54 | print "Error: " + str(e) 55 | print help 56 | sys.exit(2) 57 | 58 | if len(opts) == 0: 59 | print help 60 | sys.exit(2) 61 | 62 | for opt, arg in opts: 63 | if opt == '-h': 64 | print help 65 | sys.exit() 66 | elif opt in ("-c", "--config-profile"): 67 | configProfileName = arg 68 | elif opt in ("-n", "--cluster"): 69 | clusterName = arg 70 | elif opt in ("--debug"): 71 | DEBUG = 1 72 | elif opt in ("--exec"): 73 | DRYRUN = 0 74 | elif opt in ("--is-projectvm"): 75 | isProjectVm = 1 76 | 77 | # Default to cloudmonkey default config file 78 | if len(configProfileName) == 0: 79 | configProfileName = "config" 80 | 81 | if len(clusterName) == 0: 82 | print "ERROR: Please provide cluster name" 83 | print help 84 | sys.exit(1) 85 | 86 | # Check available memory 87 | def hostHasEnhoughMemory(h): 88 | # Available memory 89 | memoryavailable = h.memorytotal - h.memoryallocated 90 | print "Host " + h.name + " has available memory: " + str(memoryavailable) 91 | 92 | # Don't try if host has less than 10GB memory left or if vm does not fit at all 93 | # vm.memory is in Mega Bytes 94 | if memoryavailable < (10 * 1024 * 1024 * 1024): 95 | print "Warning: Skipping " + h.name + " as it has not enough free memory (" + str(memoryavailable) + ")." 96 | return False 97 | return True 98 | 99 | # Get host with min/max instances 100 | def sortHostByVmCounter(vmcounter,reverse=False): 101 | return sorted(vmcounter.items(), key=lambda x:x[1], reverse=reverse) 102 | 103 | # Get host with min/max memory 104 | def sortHostByMemory(hosts,reverse=False): 105 | return dict(sorted(hosts.items(), key=lambda x:x[1].memoryallocated, reverse=reverse)) 106 | 107 | # Parse arguments 108 | if __name__ == "__main__": 109 | handleArguments(sys.argv[1:]) 110 | 111 | # Handle project parameter 112 | if isProjectVm == 1: 113 | projectParam = "true" 114 | else: 115 | projectParam = "false" 116 | 117 | # Init our class 118 | c = cloudstackops.CloudStackOps(DEBUG,DRYRUN) 119 | 120 | if DEBUG == 1: 121 | print "Warning: Debug mode is enabled!" 122 | 123 | if DRYRUN == 1: 124 | print "Warning: dry-run mode is enabled, not running any commands!" 125 | 126 | # make credentials file known to our class 127 | c.configProfileName = configProfileName 128 | 129 | # Init the CloudStack API 130 | c.initCloudStackAPI() 131 | 132 | if len(clusterName) > 1: 133 | clusterID = c.checkCloudStackName({'csname': clusterName, 'csApiCall': 'listClusters'}) 134 | 135 | if DEBUG == 1: 136 | print "API address: " + c.apiurl 137 | print "ApiKey: " + c.apikey 138 | print "SecretKey: " + c.secretkey 139 | 140 | # Fetch the list of hosts in a cluster which are not marked dedicated aka shared: 141 | fromClusterHostsData = c.getSharedHostsFromCluster(clusterID) 142 | if fromClusterHostsData == [] or fromClusterHostsData == None: 143 | print 144 | sys.stdout.write("\033[F") 145 | print "No (enabled or non-dedicated) hosts found on cluster " + clusterName 146 | print "Nothing to work on, exiting." 147 | exit (1) 148 | 149 | # Settings 150 | minInstances = 7 151 | maxInstances = 25 152 | osFamilies = [] 153 | osFamilies.append('Windows') 154 | osFamilies.append('RedHat') 155 | osData = {} 156 | 157 | # Build the data for each OS Family 158 | for family in osFamilies: 159 | osData[family] = {} 160 | osData[family]['grandCounter'] = 0 161 | osData[family]['vms'] = {} 162 | osData[family]['vmcounter'] = {} 163 | 164 | # Figure out OStype 165 | osCat = c.listOsCategories({'name': family}) 166 | keyword = 'Red' if family == 'RedHat' else 'Server' 167 | osTypes = c.listOsTypes({'oscategoryid': osCat[0].id, 'keyword': keyword }) 168 | osData[family]['types'] = [] 169 | osData[family]['hosts'] = {} 170 | 171 | if osTypes is None: 172 | print "Warning: No OS Types found for " + family + " skipping.." 173 | continue 174 | 175 | for type in osTypes: 176 | osData[family]['types'].append(type.id) 177 | 178 | # Look at all hosts in the cluster 179 | for fromHostData in fromClusterHostsData: 180 | osData[family]['vmcounter'][fromHostData.name] = 0 181 | osData[family]['vms'][fromHostData.name] = {} 182 | osData[family]['hosts'][fromHostData.name] = fromHostData 183 | 184 | if DEBUG ==1: 185 | print "# Looking for VMS on node " + fromHostData.name 186 | print "# Memory of this host: " + str(fromHostData.memorytotal) 187 | 188 | # Get all vm's: project and non project 189 | vmdata_non_project = c.deprecatedListVirtualMachines({'hostid': fromHostData.id, 'isProjectVm': 'false' }) 190 | vmdata_project = c.deprecatedListVirtualMachines({'hostid': fromHostData.id, 'isProjectVm': 'true' }) 191 | 192 | if vmdata_project is None and vmdata_non_project is None: 193 | print "Note: No vm's of type " + family + " found on " + fromHostData.name 194 | continue 195 | if vmdata_project is None and vmdata_non_project is not None: 196 | vmdata = vmdata_non_project 197 | if vmdata_project is not None and vmdata_non_project is None: 198 | vmdata = vmdata_project 199 | if vmdata_project is not None and vmdata_non_project is not None: 200 | vmdata = vmdata_non_project + vmdata_project 201 | 202 | oscounter = 0 203 | for vm in vmdata: 204 | if DEBUG == 1: 205 | print vm.name + " -> " + str(vm.guestosid) 206 | if vm.guestosid in osData[family]['types']: 207 | osData[family]['vms'][fromHostData.name][vm.id] = vm 208 | osData[family]['vmcounter'][fromHostData.name] += 1 209 | 210 | # Cluster wide counters 211 | osData[family]['grandCounter'] += osData[family]['vmcounter'][fromHostData.name] 212 | 213 | # Sort by most memory free 214 | osData[family]['hosts'] = sortHostByMemory(osData[family]['hosts'], False) 215 | 216 | print "Note: Cluster " + clusterName + " has " + str(osData['RedHat']['grandCounter']) + " Red Hat vm's and " + str(osData['Windows']['grandCounter']) + " Windows Server vm's" 217 | 218 | # Process the generated OS Family data 219 | for family, familyData in osData.iteritems(): 220 | print 221 | print "Note: Processing " + family + " Family" 222 | print "Note: =======================================" 223 | migrateTo = [] 224 | migrateFrom = [] 225 | 226 | if 'vmcounter' not in familyData.keys(): 227 | print "Warning: key vmcounter not found" 228 | if DEBUG == 1: 229 | c.pp.pprint(familyData) 230 | continue 231 | 232 | if DEBUG == 1: 233 | print "DEBUG: Overview: " 234 | c.pp.pprint(familyData['vmcounter']) 235 | print 236 | 237 | for h in familyData['vmcounter']: 238 | if familyData['vmcounter'][h] == 0: 239 | if DEBUG == 1: 240 | print "DEBUG: No VMs on " + h + " running family " + family 241 | continue 242 | 243 | if DEBUG == 1: 244 | print h + " " + str(familyData['vmcounter'][h]) 245 | 246 | if familyData['vmcounter'][h] >= minInstances and familyData['vmcounter'][h] < maxInstances: 247 | if DEBUG == 1: 248 | print h + " is migration-to candidate!" 249 | 250 | # Available memory 251 | if not hostHasEnhoughMemory(familyData['hosts'][h]): 252 | continue 253 | 254 | migrateTo.append(h) 255 | 256 | elif familyData['vmcounter'][h] < minInstances: 257 | if DEBUG == 1: 258 | print h + " is migration-from candidate!" 259 | migrateFrom.append(h) 260 | 261 | if DEBUG == 1: 262 | print "DEBUG: MigrateTo:" 263 | print migrateTo 264 | 265 | # If no host with minCounter vm's, then select the one with the most 266 | if len(migrateTo) == 0: 267 | maxHosts = sortHostByVmCounter(familyData['vmcounter'], True) 268 | print "Note: Hosts in sorted order:" 269 | print maxHosts 270 | maxHost = "" 271 | 272 | # Select the best host to migrate to 273 | for d in maxHosts: 274 | # Get hostname 275 | m = d[0] 276 | # Available memory 277 | if not hostHasEnhoughMemory(familyData['hosts'][m]): 278 | continue 279 | # Too many instances already 280 | if familyData['vmcounter'][m] >= maxInstances: 281 | print "Note: Skipping " + m + " because it has more than maxInstances vm's already " + str(maxInstances) 282 | continue 283 | # Take the next best one 284 | maxHost = m 285 | print "Note: Selecting " + m + " because it already has some instances running." 286 | break 287 | 288 | # If it did not work, halt 289 | if len(maxHost) == 0: 290 | print "Error: Could not select a suitable host. Halting." 291 | sys.exit(1) 292 | 293 | if DEBUG == 1: 294 | print "DEBUG: Selecting the host with max vm's already." 295 | migrateTo.append(maxHost) 296 | 297 | osData[family]['vmcounterafter'] = osData[family]['vmcounter'] 298 | 299 | # Display what we'd do 300 | for h in migrateFrom: 301 | for key,vm in familyData['vms'][h].iteritems(): 302 | to = choice(migrateTo) 303 | if to != h: 304 | print "Note: Would have migrated " + vm.name + " (from " + h + " to " + str(to) + ") " + str(vm.memory) + " mem" 305 | osData[family]['vmcounterafter'][to] +=1 306 | osData[family]['vmcounterafter'][h] -=1 307 | else: 308 | print "Note: Skipping " + vm.name + " (already on " + h + " / " + to + ")" 309 | 310 | print "DEBUG Result after migration:" 311 | c.pp.pprint(osData[family]['vmcounterafter']) 312 | 313 | if DEBUG == 1: 314 | print "Note: We're done!" 315 | -------------------------------------------------------------------------------- /rebootRouterVM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to upgrade a router VM to the new template 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import sys 26 | import getopt 27 | from cloudstackops import cloudstackops 28 | import os.path 29 | 30 | # Function to handle our arguments 31 | 32 | 33 | def handleArguments(argv): 34 | global DEBUG 35 | DEBUG = 0 36 | global DRYRUN 37 | DRYRUN = 1 38 | global vmname 39 | vmname = '' 40 | global toCluster 41 | toCluster = '' 42 | global configProfileName 43 | configProfileName = '' 44 | global isProjectVm 45 | isProjectVm = 0 46 | global onlyRequired 47 | onlyRequired = 0 48 | global CLEANUP 49 | CLEANUP = 0 50 | 51 | # Usage message 52 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 53 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 54 | '\n --routerinstance-name -r \tWork with this router (r-12345-VM)' + \ 55 | '\n --is-projectrouter\t\t\tThe specified router belongs to a project' + \ 56 | '\n --only-when-required\t\t\tOnly reboot when the RequiresUpgrade flag is set' + \ 57 | '\n --cleanup\t\t\t\tRestart router with cleanup' + \ 58 | '\n --debug\t\t\t\tEnable debug mode' + \ 59 | '\n --exec\t\t\t\tExecute for real' 60 | 61 | try: 62 | opts, args = getopt.getopt( 63 | argv, "hc:r:p", [ 64 | "config-profile=", "routerinstance-name=", "debug", "exec", "cleanup", "is-projectrouter", "only-when-required"]) 65 | except getopt.GetoptError: 66 | print help 67 | sys.exit(2) 68 | for opt, arg in opts: 69 | if opt == '-h': 70 | print help 71 | sys.exit() 72 | elif opt in ("-c", "--config-profile"): 73 | configProfileName = arg 74 | elif opt in ("-r", "--routerinstance-name"): 75 | vmname = arg 76 | elif opt in ("--cleanup"): 77 | CLEANUP = 1 78 | elif opt in ("--debug"): 79 | DEBUG = 1 80 | elif opt in ("--exec"): 81 | DRYRUN = 0 82 | elif opt in ("--is-projectrouter"): 83 | isProjectVm = 1 84 | elif opt in ("--only-when-required"): 85 | onlyRequired = 1 86 | 87 | # Default to cloudmonkey default config file 88 | if len(configProfileName) == 0: 89 | configProfileName = "config" 90 | 91 | # We need at least these vars 92 | if len(vmname) == 0: 93 | print help 94 | sys.exit() 95 | 96 | # Parse arguments 97 | if __name__ == "__main__": 98 | handleArguments(sys.argv[1:]) 99 | 100 | # Init CloudStack class 101 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 102 | c.instance_name = "N/A" 103 | 104 | if DEBUG == 1: 105 | print "Warning: Debug mode is enabled!" 106 | 107 | if DRYRUN == 1: 108 | print "Warning: dry-run mode is enabled, not running any commands!" 109 | 110 | # make credentials file known to our class 111 | c.configProfileName = configProfileName 112 | 113 | # Init the CloudStack API 114 | c.initCloudStackAPI() 115 | 116 | if DEBUG == 1: 117 | print "DEBUG: API address: " + c.apiurl 118 | print "DEBUG: ApiKey: " + c.apikey 119 | print "DEBUG: SecretKey: " + c.secretkey 120 | 121 | # Check cloudstack IDs 122 | if DEBUG == 1: 123 | print "DEBUG: Checking CloudStack IDs of provided input.." 124 | 125 | if isProjectVm == 1: 126 | projectParam = "true" 127 | else: 128 | projectParam = "false" 129 | 130 | # check routerID 131 | routerID = c.checkCloudStackName({'csname': vmname, 132 | 'csApiCall': 'listRouters', 133 | 'listAll': 'true', 134 | 'isProjectVm': projectParam}) 135 | 136 | # get router data 137 | routerData = c.getRouterData({'name': vmname, 'isProjectVm': projectParam}) 138 | router = routerData[0] 139 | 140 | if DEBUG == 1: 141 | print routerData 142 | 143 | print "Note: Found router " + router.name + " that belongs to account " + str(router.account) + " with router ID " + router.id 144 | print "Note: This router has " + str(len(router.nic)) + " nics." 145 | 146 | # Pretty Slack messages 147 | c.instance_name = router.name 148 | c.slack_custom_title = "Domain" 149 | c.slack_custom_value = router.domain 150 | hostData = c.getHostData({'hostid': router.hostid})[0] 151 | clusterData = c.listClusters({'id': hostData.clusterid}) 152 | c.cluster = clusterData[0].name 153 | 154 | # Does this router need an upgrade? 155 | if onlyRequired == 1 and router.requiresupgrade == 0: 156 | print "Note: This router does not need to be upgraded. Won't reboot because of --only-when-required flag. When you remove the flag and run the script again it will reboot anyway." 157 | sys.exit(0) 158 | 159 | print "Note: Let's reboot the router VM.." 160 | 161 | if DRYRUN == 1: 162 | print "Note: Would have rebooted router " + router.name + " (" + router.id + ")" 163 | else: 164 | # Restart network with clean up 165 | if CLEANUP == 1: 166 | # If the network is a VPC 167 | if router.vpcid: 168 | c.task = "Restart VPC with clean up" 169 | message = "Restarting router " + router.name + " with clean up (" + router.id + ")" 170 | c.print_message(message=message, message_type="Note", to_slack=True) 171 | result = c.restartVPC(router.vpcid) 172 | if result == 1: 173 | print "Restarting failed, will try again!" 174 | result = c.restartVPC(router.vpcid) 175 | if result == 1: 176 | message = "Restarting (" + router.id + ") with clean up failed.\nError: investigate manually!" 177 | c.print_message(message=message, message_type="Error", to_slack=True) 178 | sys.exit(1) 179 | else: 180 | message = "Successfully restarted " + router.name + " (" + router.id + ")" 181 | c.print_message(message=message, message_type="Note", to_slack=True) 182 | else: 183 | message = "Successfully restarted " + router.name + " (" + router.id + ")" 184 | c.print_message(message=message, message_type="Note", to_slack=True) 185 | # If the network is a Isolated network 186 | else: 187 | c.task = "Restart isolated network with clean up" 188 | message = "Restarting isolated network router " + router.name + " with clean up (" + router.id + ")" 189 | c.print_message(message=message, message_type="Note", to_slack=True) 190 | result = c.restartNetwork(router.guestnetworkid) 191 | if result == 1: 192 | print "Restarting failed, will try again!" 193 | result = c.restartNetwork(router.guestnetworkid) 194 | if result == 1: 195 | message = "Restarting (" + router.id + ") with clean up failed.\nError: investigate manually!" 196 | c.print_message(message=message, message_type="Error", to_slack=True) 197 | sys.exit(1) 198 | else: 199 | message = "Successfully restarted " + router.name + " (" + router.id + ")" 200 | c.print_message(message=message, message_type="Note", to_slack=True) 201 | else: 202 | message = "Successfully restarted " + router.name + " (" + router.id + ")" 203 | c.print_message(message=message, message_type="Note", to_slack=True) 204 | else: 205 | # Reboot router 206 | c.task = "Reboot virtual router" 207 | message = "Rebooting router " + router.name + " (" + router.id + ")" 208 | c.print_message(message=message, message_type="Note", to_slack=True) 209 | result = c.rebootRouter(router.id) 210 | if result == 1: 211 | print "Rebooting failed, will try again!" 212 | result = c.rebootRouter(router.id) 213 | if result == 1: 214 | message = "Rebooting (" + router.id + ") failed.\nError: Investigate manually!" 215 | c.print_message(message=message, message_type="Error", to_slack=True) 216 | sys.exit(1) 217 | else: 218 | message = "Successfully rebooted " + router.name + " (" + router.id + ")" 219 | c.print_message(message=message, message_type="Note", to_slack=True) 220 | 221 | print "Note: We're done!" 222 | -------------------------------------------------------------------------------- /rebootVPC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import getopt 4 | from cloudstackops import cloudstackops 5 | import os.path 6 | 7 | # Function to handle our arguments 8 | 9 | 10 | def handleArguments(argv): 11 | global DEBUG 12 | DEBUG = 0 13 | global DRYRUN 14 | DRYRUN = 1 15 | global vpcname 16 | vpcname = '' 17 | global vpcuuid 18 | vpcuuid = '' 19 | global networkuuid 20 | networkuuid = '' 21 | global toCluster 22 | toCluster = '' 23 | global configProfileName 24 | configProfileName = '' 25 | global isProjectVm 26 | isProjectVm = 0 27 | 28 | # Usage message 29 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 30 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 31 | '\n --vpc-name -v \t\t\tWork with this VPC (r-12345-VM)' + \ 32 | '\n --uuid -u \t\t\tWork with this VPC (UUID)' + \ 33 | '\n --network-uuid -t \t\tWork with this VPC tier (UUID)' + \ 34 | '\n --is-projectrouter\t\t\tThe specified router belongs to a project' + \ 35 | '\n --debug\t\t\t\tEnable debug mode' + \ 36 | '\n --exec\t\t\t\tExecute for real' 37 | 38 | try: 39 | opts, args = getopt.getopt( 40 | argv, "hc:v:u:pt:", [ 41 | "config-profile=", "vpc-name=", "uuid=", "network-uuid=", "debug", "exec", "is-projectrouter"]) 42 | except getopt.GetoptError: 43 | print help 44 | sys.exit(2) 45 | for opt, arg in opts: 46 | if opt == '-h': 47 | print help 48 | sys.exit() 49 | elif opt in ("-c", "--config-profile"): 50 | configProfileName = arg 51 | elif opt in ("-v", "--vpc-name"): 52 | vpcname = arg 53 | elif opt in ("-u", "--uuid"): 54 | vpcuuid = arg 55 | elif opt in ("-t", "--network-uuid"): 56 | networkuuid = arg 57 | elif opt in ("--debug"): 58 | DEBUG = 1 59 | elif opt in ("--exec"): 60 | DRYRUN = 0 61 | elif opt in ("--is-projectrouter"): 62 | isProjectVm = 1 63 | 64 | # Default to cloudmonkey default config file 65 | if len(configProfileName) == 0: 66 | configProfileName = "config" 67 | 68 | # We need at least these vars 69 | if len(vpcname) == 0 and len(vpcuuid) == 0 and len(networkuuid) == 0: 70 | print vpcuuid 71 | print vpcname 72 | print networkuuid 73 | print help 74 | sys.exit() 75 | 76 | # Parse arguments 77 | if __name__ == "__main__": 78 | handleArguments(sys.argv[1:]) 79 | 80 | # Init CloudStack class 81 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 82 | c.instance_name = "N/A" 83 | 84 | if DEBUG == 1: 85 | print "Warning: Debug mode is enabled!" 86 | 87 | if DRYRUN == 1: 88 | print "Warning: dry-run mode is enabled, not running any commands!" 89 | 90 | # make credentials file known to our class 91 | c.configProfileName = configProfileName 92 | 93 | # Init the CloudStack API 94 | c.initCloudStackAPI() 95 | 96 | if DEBUG == 1: 97 | print "DEBUG: API address: " + c.apiurl 98 | print "DEBUG: ApiKey: " + c.apikey 99 | print "DEBUG: SecretKey: " + c.secretkey 100 | 101 | # Check cloudstack IDs 102 | if DEBUG == 1: 103 | print "DEBUG: Checking CloudStack IDs of provided input.." 104 | 105 | if isProjectVm == 1: 106 | projectParam = "true" 107 | else: 108 | projectParam = "false" 109 | 110 | # check routerID 111 | if len(vpcuuid) == 0: 112 | vpcuuid = c.checkCloudStackName({'csname': vpcname, 113 | 'csApiCall': 'listVPCs', 114 | 'listAll': 'true', 115 | 'isProjectVm': projectParam}) 116 | 117 | if len(networkuuid) > 0: 118 | print "Note: Getting VPC id from network uuid %s" % networkuuid 119 | network = c.listNetworks(networkuuid)[0] 120 | vpcuuid = network.vpcid 121 | 122 | if vpcuuid == 1 or vpcuuid == "": 123 | print "Error: VPC cannot be found!" 124 | exit(1) 125 | 126 | vpc = c.listVPCs(vpcuuid)[0] 127 | 128 | print "Note: Found VPC " + vpcname 129 | 130 | # Pretty Slack messages 131 | c.instance_name = vpcname 132 | c.slack_custom_title = "Domain" 133 | c.slack_custom_value = vpc.domain 134 | c.zone_name = vpc.zonename 135 | 136 | print "Note: Let's reboot the VPC.." 137 | 138 | if DRYRUN == 1: 139 | print "Note: Would have rebooted vpc " + vpc.name + " (" + vpcuuid + ")" 140 | else: 141 | # If the network is a VPC 142 | c.task = "Restart VPC with clean up" 143 | message = "Restarting VPC " + vpc.name + " with clean up (" + vpcuuid + ")" 144 | c.print_message(message=message, message_type="Note", to_slack=True) 145 | result = c.restartVPC(vpcuuid) 146 | if result == 1: 147 | print "Restarting failed, will try again!" 148 | result = c.restartVPC(vpcuuid) 149 | if result == 1: 150 | message = "Restarting VPC " + vpc.name + "(" + vpcuuid + ") with clean up failed.\nError: investigate manually!" 151 | c.print_message(message=message, message_type="Error", to_slack=True) 152 | sys.exit(1) 153 | else: 154 | message = "Successfully restarted VPC " + vpc.name + " (" + vpcuuid + ")" 155 | c.print_message(message=message, message_type="Note", to_slack=True) 156 | else: 157 | message = "Successfully restarted VPC " + vpc.name + " (" + vpcuuid + ")" 158 | c.print_message(message=message, message_type="Note", to_slack=True) 159 | 160 | 161 | print "Note: We're done!" 162 | -------------------------------------------------------------------------------- /reportAccounts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to report the users that exist in accounts 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | # Function to handle our arguments 34 | 35 | 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 1 41 | global domainname 42 | domainname = '' 43 | global configProfileName 44 | configProfileName = '' 45 | global display 46 | display = "screen" 47 | 48 | # Usage message 49 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 50 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 51 | '\n --display -d \t\tEmail the result or print on screen (defaults to screen)' + \ 52 | '\n --debug\t\t\t\tEnable debug mode' + \ 53 | '\n --exec\t\t\t\tExecute for real' 54 | 55 | try: 56 | opts, args = getopt.getopt( 57 | argv, "hc:d:p:", [ 58 | "config-profile=", "display=", "debug", "exec"]) 59 | except getopt.GetoptError as e: 60 | print "Error: " + str(e) 61 | print help 62 | sys.exit(2) 63 | 64 | if len(opts) == 0: 65 | print help 66 | sys.exit(2) 67 | 68 | for opt, arg in opts: 69 | if opt == '-h': 70 | print help 71 | sys.exit() 72 | elif opt in ("-c", "--config-profile"): 73 | configProfileName = arg 74 | elif opt in ("-d", "--display"): 75 | display = arg 76 | elif opt in ("--debug"): 77 | DEBUG = 1 78 | elif opt in ("--exec"): 79 | DRYRUN = 0 80 | 81 | # Default to cloudmonkey default config file 82 | if len(configProfileName) == 0: 83 | configProfileName = "config" 84 | 85 | # Sanity check 86 | if display != "screen" and display != "email": 87 | print "Error: invalid display value '" + display + "'" 88 | sys.exit(1) 89 | 90 | # Parse arguments 91 | if __name__ == "__main__": 92 | handleArguments(sys.argv[1:]) 93 | 94 | # Init our class 95 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 96 | 97 | if DEBUG == 1: 98 | print "Warning: Debug mode is enabled!" 99 | 100 | if DRYRUN == 1: 101 | print "Warning: dry-run mode is enabled, not running any commands!" 102 | 103 | # make credentials file known to our class 104 | c.configProfileName = configProfileName 105 | 106 | # Init the CloudStack API 107 | c.initCloudStackAPI() 108 | 109 | if DEBUG == 1: 110 | print "API address: " + c.apiurl 111 | print "ApiKey: " + c.apikey 112 | print "SecretKey: " + c.secretkey 113 | 114 | # get users 115 | userData = c.reportUsers() 116 | 117 | # Empty line 118 | if display == "screen": 119 | print 120 | 121 | counter = 0 122 | 123 | for domainid, domain in userData.iteritems(): 124 | # Get domain data 125 | domainResult = c.listDomains(domainid) 126 | domainData = domainResult[0] 127 | 128 | # Display on screen 129 | if display == "screen": 130 | print "\nOverview for " + domainData.name + " (" + domainData.path + "):" 131 | 132 | # Start table 133 | t = PrettyTable( 134 | ["User Account", "Username", "E-mail", "Firstname", "Lastname"]) 135 | 136 | if domain is None: 137 | continue 138 | 139 | for userArray in domain: 140 | if userArray is None: 141 | continue 142 | 143 | for user in userArray: 144 | # Handle special chars in names 145 | user.firstname = c.removeNonAscii(user.firstname) 146 | user.lastname = c.removeNonAscii(user.lastname) 147 | 148 | # Generate table 149 | t.add_row([user.account, 150 | user.username, 151 | user.email, 152 | user.firstname, 153 | user.lastname]) 154 | 155 | if user.email is None: 156 | user.email = "Unknown" 157 | 158 | if display == "screen": 159 | print t 160 | elif display == "email": 161 | userTable = t.get_html_string(format=True) 162 | 163 | # E-mail the report 164 | if display == "email": 165 | # Get user data to e-mail 166 | adminData = c.getDomainAdminUserData(domainid) 167 | if adminData is None or adminData == 1 or not adminData.email: 168 | print "Warning: no valid e-mail address found, skipping " + domainData.name 169 | continue 170 | 171 | print "Note: Sending report to " + adminData.email 172 | if not adminData.email: 173 | print "Warning: Skipping mailing due to missing e-mail address." 174 | 175 | templatefile = open("email_template/reportAccounts.txt", "r") 176 | emailbody = templatefile.read() 177 | emailbody = emailbody.replace("FIRSTNAME", adminData.firstname) 178 | emailbody = emailbody.replace("LASTNAME", adminData.lastname) 179 | emailbody = emailbody.replace("USERTABLE", userTable) 180 | emailbody = emailbody.replace("DOMAIN", domainData.name) 181 | emailbody = emailbody.replace("PATH", domainData.path) 182 | emailbody = emailbody.replace("ORGANIZATION", c.organization) 183 | emailbody = emailbody.replace("EMAILADRESS", c.mail_from) 184 | templatefile.close() 185 | 186 | if DRYRUN == 1: 187 | print "Note: Not sending notification e-mails due to DRYRUN setting. Would have e-mailed (from " + c.mail_from + ") to " + adminData.email + " for domain " + domainData.name 188 | else: 189 | try: 190 | # Notify user 191 | msgSubject = 'Monthly overview of users in your domain "' + \ 192 | domainData.name + '"' 193 | c.sendMail(c.mail_from, adminData.email, msgSubject, emailbody) 194 | 195 | # Notify admin 196 | c.sendMail(c.mail_from, c.errors_to, msgSubject, emailbody) 197 | except: 198 | print "Warning: failed to send e-mail notification (from " + c.mail_from + ") to " + adminData.email + " for domain " + domainData.name 199 | 200 | if DEBUG == 1: 201 | print emailbody 202 | 203 | if DEBUG == 1: 204 | print "Note: We're done!" 205 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | args==0.1.0 2 | bcrypt==3.1.7 3 | boto==2.49.0 4 | botocore==1.14.9 5 | certifi==2019.11.28 6 | cffi==1.13.2 7 | chardet==3.0.4 8 | clint==0.5.1 9 | colorama==0.4.1 10 | cryptography==2.8 11 | cs==2.7.1 12 | dnspython==1.16.0 13 | docutils==0.15.2 14 | enum34==1.1.6 15 | Fabric==1.13.2 16 | futures==3.3.0 17 | idna==2.8 18 | invoke==1.4.0 19 | ipaddress==1.0.23 20 | jmespath==0.9.4 21 | libvirt-python==4.5.0 22 | Marvin==0.1.0 23 | mysql-connector-python==8.0.19 24 | nose==1.3.7 25 | paramiko==2.7.1 26 | prettytable==0.7.2 27 | protobuf==3.6.1 28 | pyasn1==0.4.8 29 | pycparser==2.19 30 | PyNaCl==1.3.0 31 | python-dateutil==2.8.1 32 | pytz==2019.3 33 | PyYAML==5.2 34 | requests==2.22.0 35 | rsa==3.4.2 36 | s3transfer==0.3.2 37 | six==1.14.0 38 | slackweb==1.0.5 39 | urllib3==1.25.8 40 | -------------------------------------------------------------------------------- /updateHostTags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to update the host tags 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstackops 29 | import os.path 30 | from random import choice 31 | 32 | # Function to handle our arguments 33 | 34 | 35 | def handleArguments(argv): 36 | global DEBUG 37 | DEBUG = 0 38 | global DRYRUN 39 | DRYRUN = 1 40 | global hostname 41 | hostname = '' 42 | global configProfileName 43 | configProfileName = '' 44 | global replace 45 | replace = 0 46 | global newTags 47 | newTags = '' 48 | 49 | # Usage message 50 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 51 | '\n --config-profile -c \tSpecify the CloudMonkey profile name to get the credentials from (or specify in ./config file)' + \ 52 | '\n --hostname -n \t\tWork with this hypervisor' + \ 53 | '\n --tags -t \t\t\tAdd these tags (can be comma separated list, without spaces). Use \' \' to set empty.' + \ 54 | '\n --replace\t\t\t\tReplace all tags with the one(s) specified' + \ 55 | '\n --debug\t\t\t\tEnable debug mode' + \ 56 | '\n --exec\t\t\t\tExecute for real' 57 | 58 | try: 59 | opts, args = getopt.getopt( 60 | argv, "hc:n:t:r", [ 61 | "config-profile=", "hostname=", "tags=", "debug", "exec", "replace"]) 62 | except getopt.GetoptError as e: 63 | print "Error: " + str(e) 64 | print help 65 | sys.exit(2) 66 | for opt, arg in opts: 67 | if opt == '-h': 68 | print help 69 | sys.exit() 70 | elif opt in ("-c", "--config-profile"): 71 | configProfileName = arg 72 | elif opt in ("-n", "--hostname"): 73 | hostname = arg 74 | elif opt in ("-t", "--tags"): 75 | newTags = arg 76 | elif opt in ("--debug"): 77 | DEBUG = 1 78 | elif opt in ("--exec"): 79 | DRYRUN = 0 80 | elif opt in ("--replace"): 81 | replace = 1 82 | 83 | # Default to cloudmonkey default config file 84 | if len(configProfileName) == 0: 85 | configProfileName = "config" 86 | 87 | # We need at least these vars 88 | if len(hostname) == 0 or len(newTags) == 0: 89 | print help 90 | sys.exit() 91 | 92 | # Parse arguments 93 | if __name__ == "__main__": 94 | handleArguments(sys.argv[1:]) 95 | 96 | # Init our class 97 | c = cloudstackops.CloudStackOps(DEBUG, DRYRUN) 98 | 99 | if DEBUG == 1: 100 | print "Warning: Debug mode is enabled!" 101 | 102 | if DRYRUN == 1: 103 | print "Warning: dry-run mode is enabled, not running any commands!" 104 | 105 | # make credentials file known to our class 106 | c.configProfileName = configProfileName 107 | 108 | # Init the CloudStack API 109 | c.initCloudStackAPI() 110 | 111 | if DEBUG == 1: 112 | print "DEBUG: API address: " + c.apiurl 113 | print "DEBUG: ApiKey: " + c.apikey 114 | print "DEBUG: SecretKey: " + c.secretkey 115 | 116 | # Check cloudstack IDs 117 | if DEBUG == 1: 118 | print "DEBUG: Checking CloudStack IDs of provided input.." 119 | 120 | # check hostID 121 | hostID = c.checkCloudStackName({'csname': hostname, 'csApiCall': 'listHosts'}) 122 | 123 | # get router data 124 | if hostID == 1 or hostID is None: 125 | print "Error: could not locate ID." 126 | sys.exit() 127 | if DEBUG == 1: 128 | print "DEBUG: Found host with id " + str(hostID) 129 | 130 | hostData = c.getHostData({'hostid': hostID}) 131 | 132 | if DEBUG == 1: 133 | print hostData 134 | 135 | # current tags 136 | if hostData[0].hosttags is None: 137 | currentTags = '' 138 | comma = '' 139 | print "Note: Hosttags are currently set to '" + currentTags + "'" 140 | else: 141 | currentTags = hostData[0].hosttags 142 | comma = ', ' 143 | print "Note: Hosttags are currently set to '" + currentTags + "'" 144 | 145 | # Construct new tags 146 | if replace == 1: 147 | updatedTags = newTags 148 | else: 149 | updatedTags = currentTags + comma + newTags 150 | 151 | print "Note: Hosttags will be set to '" + updatedTags + "'" 152 | 153 | # Update host tags 154 | if DRYRUN == 1: 155 | print "Note: Would have updated tags to '" + updatedTags + "'" 156 | else: 157 | result = c.updateHostTags(hostID, updatedTags) 158 | if result == 1: 159 | print "Error: updating failed" 160 | else: 161 | print "Note: updating was succesful" 162 | -------------------------------------------------------------------------------- /whoHasThisIp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to display who uses a given ip address 23 | # Remi Bergsma - rbergsma@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstacksql 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | # Function to handle our arguments 34 | 35 | 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 0 41 | global mysqlHost 42 | mysqlHost = '' 43 | global mysqlPasswd 44 | mysqlPasswd = '' 45 | global ipaddress 46 | ipaddress = '' 47 | 48 | # Usage message 49 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 50 | '\n --ip-address -i \t\tSearch for this ip address ' + \ 51 | '(partial is allowed)' + \ 52 | '\n --mysqlserver -s \tSpecify MySQL server ' + \ 53 | 'to read HA worker table from' + \ 54 | '\n\t\t\t\t\tuse any for all databases from config file' + \ 55 | '\n --mysqlpassword \t\tSpecify password to cloud ' + \ 56 | 'MySQL user' + \ 57 | '\n --debug\t\t\t\tEnable debug mode' + \ 58 | '\n --exec\t\t\t\tExecute for real (not needed for list* scripts)' 59 | 60 | try: 61 | opts, args = getopt.getopt( 62 | argv, "hs:i:", [ 63 | "mysqlserver=", 64 | "ip-address=", 65 | "mysqlpassword=", 66 | "debug", 67 | "exec"]) 68 | except getopt.GetoptError as e: 69 | print "Error: " + str(e) 70 | print help 71 | sys.exit(2) 72 | for opt, arg in opts: 73 | if opt == '-h': 74 | print help 75 | sys.exit() 76 | elif opt in ("-s", "--mysqlserver"): 77 | mysqlHost = arg 78 | elif opt in ("-p", "--mysqlpassword"): 79 | mysqlPasswd = arg 80 | elif opt in ("-i", "--ip-address"): 81 | ipaddress = arg 82 | elif opt in ("--debug"): 83 | DEBUG = 1 84 | elif opt in ("--exec"): 85 | DRYRUN = 0 86 | 87 | # We need at least these vars 88 | if len(mysqlHost) == 0 or len(ipaddress) == 0: 89 | print help 90 | sys.exit() 91 | 92 | # Parse arguments 93 | if __name__ == "__main__": 94 | handleArguments(sys.argv[1:]) 95 | 96 | # Init our class 97 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 98 | 99 | if DEBUG == 1: 100 | print "# Warning: Debug mode is enabled!" 101 | 102 | if DRYRUN == 1: 103 | print "# Warning: dry-run mode is enabled, not running any commands!" 104 | 105 | db = [] 106 | dbHost = '' 107 | if mysqlHost == 'any': 108 | db = s.getAllDB() 109 | else: 110 | db.append(mysqlHost) 111 | print "\nConnecting to the following DB's: " + str(db) + "\n" 112 | 113 | for mysqlHost in db: 114 | # Connect MySQL 115 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 116 | if result > 0: 117 | print "Error: MySQL connection failed" 118 | sys.exit(1) 119 | elif DEBUG == 1: 120 | print "DEBUG: MySQL connection successful" 121 | print s.conn 122 | 123 | ipaddresses = s.getIpAddressData(ipaddress) 124 | counter = 0 125 | networknamenone = 0 126 | t = PrettyTable(["VM name", 127 | "Network Name", 128 | "Mac Address", 129 | "Ipv4", 130 | "Netmask", 131 | "Mode", 132 | "State", 133 | "Created"]) 134 | t.align["VM name"] = "l" 135 | t.align["Network Name"] = "l" 136 | 137 | for ( 138 | networkname, 139 | mac_address, 140 | ip4_address, 141 | netmask, 142 | broadcast_uri, 143 | mode, 144 | state, 145 | created, 146 | vmname) in ipaddresses: 147 | counter = counter + 1 148 | dbHost = mysqlHost 149 | if networkname is None: 150 | networknamenone = networknamenone + 1 151 | 152 | vmname = (vmname[:22] + '..') if len(vmname) > 24 else vmname 153 | networkname = ( 154 | networkname[:22] + '..') if networkname is not None \ 155 | and len(networkname) > 24 else networkname 156 | t.add_row([vmname, 157 | networkname, 158 | mac_address, 159 | ip4_address, 160 | netmask, 161 | mode, 162 | state, 163 | created]) 164 | 165 | # When not found a vm name in the VPC query check the bridged networks 166 | if counter == networknamenone: 167 | countera = 0 168 | ipaddresses = s.getIpAddressDataBridge(ipaddress) 169 | r = PrettyTable(["VM name", 170 | "State", 171 | "Ipv4", 172 | "Network Name", 173 | "Created"]) 174 | r.align["VM name"] = "l" 175 | r.align["Network Name"] = "l" 176 | 177 | r = PrettyTable(["VM name", 178 | "State", 179 | "Ipv4", 180 | "Network Name", 181 | "Created"]) 182 | r.align["VM name"] = "l" 183 | r.align["Network Name"] = "l" 184 | 185 | for ( 186 | vmname, 187 | ip4_address, 188 | created, 189 | state, 190 | networkname) in ipaddresses: 191 | countera = countera + 1 192 | dbHost = mysqlHost 193 | 194 | vmname = (vmname[:22] + '..') if len(vmname) > 24 else vmname 195 | networkname = ( 196 | networkname[:22] + '..') if networkname is not None \ 197 | and len(networkname) > 24 else networkname 198 | r.add_row([vmname, 199 | networkname, 200 | ip4_address, 201 | state, 202 | created]) 203 | 204 | # When not found a vm name in the VPC query and the bridged networks check the infra 205 | if counter == networknamenone and countera == networknamenone: 206 | counterb = 0 207 | ipaddresses = s.getIpAddressDataInfra(ipaddress) 208 | u = PrettyTable(["VM name", 209 | "VM Type", 210 | "Ipv4", 211 | "Instance ID", 212 | "State"]) 213 | u.align["VM name"] = "l" 214 | u.align["VM Type"] = "l" 215 | 216 | u = PrettyTable(["VM name", 217 | "VM Type", 218 | "Ipv4", 219 | "Instance ID", 220 | "State"]) 221 | u.align["VM name"] = "l" 222 | u.align["Ipv4"] = "l" 223 | 224 | for ( 225 | vmname, 226 | vmtype, 227 | ip4_address, 228 | instance_id, 229 | state) in ipaddresses: 230 | counterb = counterb + 1 231 | dbHost = mysqlHost 232 | 233 | vmname = (vmname[:22] + '..') if len(vmname) > 24 else vmname 234 | u.add_row([vmname, 235 | vmtype, 236 | ip4_address, 237 | instance_id, 238 | state]) 239 | 240 | # Disconnect MySQL 241 | s.disconnectMySQL() 242 | 243 | if counter <> 0: 244 | print "Results: " + dbHost + "\n" 245 | print t 246 | print "Note: Found " + str(counter) + " results." 247 | elif countera <> 0: 248 | print "Results: " + dbHost + "\n" 249 | print r 250 | print "Note: Found " + str(countera) + " results." 251 | elif counterb <> 0: 252 | print "Results:" + dbHost + "\n" 253 | print u 254 | print "Note: Found " + str(counterb) + " results." 255 | 256 | 257 | -------------------------------------------------------------------------------- /whoHasThisMac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to display who uses a given mac address (based on whoHasThisIp.py) 23 | # Edwin Beekman - ebeekman@schubergphilis.com 24 | 25 | import time 26 | import sys 27 | import getopt 28 | from cloudstackops import cloudstacksql 29 | import os.path 30 | from random import choice 31 | from prettytable import PrettyTable 32 | 33 | # Function to handle our arguments 34 | 35 | 36 | def handleArguments(argv): 37 | global DEBUG 38 | DEBUG = 0 39 | global DRYRUN 40 | DRYRUN = 0 41 | global mysqlHost 42 | mysqlHost = '' 43 | global mysqlPasswd 44 | mysqlPasswd = '' 45 | global macaddress 46 | macaddress = '' 47 | 48 | # Usage message 49 | help = "Usage: ./" + os.path.basename(__file__) + ' [options] ' + \ 50 | '\n --mac-address -m \t\tSearch for this mac address ' + \ 51 | '(partial is allowed)' + \ 52 | '\n --mysqlserver -s \t\tSpecify MySQL server ' + \ 53 | 'to read HA worker table from' + \ 54 | '\n --mysqlpassword \t\t\tSpecify password to cloud ' + \ 55 | 'MySQL user' + \ 56 | '\n --debug\t\t\t\t\tEnable debug mode' + \ 57 | '\n --exec\t\t\t\t\tExecute for real (not needed for list* scripts)' 58 | 59 | try: 60 | opts, args = getopt.getopt( 61 | argv, "hs:m:", [ 62 | "mysqlserver=", 63 | "mac-address=", 64 | "mysqlpassword=", 65 | "debug", 66 | "exec"]) 67 | except getopt.GetoptError as e: 68 | print "Error: " + str(e) 69 | print help 70 | sys.exit(2) 71 | for opt, arg in opts: 72 | if opt == '-h': 73 | print help 74 | sys.exit() 75 | elif opt in ("-s", "--mysqlserver"): 76 | mysqlHost = arg 77 | elif opt in ("-p", "--mysqlpassword"): 78 | mysqlPasswd = arg 79 | elif opt in ("-m", "--mac-address"): 80 | macaddress = arg 81 | elif opt in ("--debug"): 82 | DEBUG = 1 83 | elif opt in ("--exec"): 84 | DRYRUN = 0 85 | 86 | # We need at least these vars 87 | if len(mysqlHost) == 0 or len(macaddress) == 0: 88 | print help 89 | sys.exit() 90 | 91 | # Parse arguments 92 | if __name__ == "__main__": 93 | handleArguments(sys.argv[1:]) 94 | 95 | # Init our class 96 | s = cloudstacksql.CloudStackSQL(DEBUG, DRYRUN) 97 | 98 | if DEBUG == 1: 99 | print "# Warning: Debug mode is enabled!" 100 | 101 | if DRYRUN == 1: 102 | print "# Warning: dry-run mode is enabled, not running any commands!" 103 | 104 | # Connect MySQL 105 | result = s.connectMySQL(mysqlHost, mysqlPasswd) 106 | if result > 0: 107 | print "Error: MySQL connection failed" 108 | sys.exit(1) 109 | elif DEBUG == 1: 110 | print "DEBUG: MySQL connection successful" 111 | print s.conn 112 | 113 | macaddresses = s.getMacAddressData(macaddress) 114 | counter = 0 115 | t = PrettyTable(["VM name", 116 | "Network Name", 117 | "Mac Address", 118 | "Ipv4", 119 | "Netmask", 120 | "Mode", 121 | "State", 122 | "Created"]) 123 | t.align["VM name"] = "l" 124 | t.align["Network Name"] = "l" 125 | 126 | for ( 127 | networkname, 128 | mac_address, 129 | ip4_address, 130 | netmask, 131 | broadcast_uri, 132 | mode, 133 | state, 134 | created, 135 | vmname) in macaddresses: 136 | counter = counter + 1 137 | 138 | vmname = (vmname[:22] + '..') if len(vmname) > 24 else vmname 139 | networkname = ( 140 | networkname[:22] + '..') if networkname is not None \ 141 | and len(networkname) > 24 else networkname 142 | t.add_row([vmname, 143 | networkname, 144 | mac_address, 145 | ip4_address, 146 | netmask, 147 | mode, 148 | state, 149 | created]) 150 | 151 | # Disconnect MySQL 152 | s.disconnectMySQL() 153 | 154 | print t 155 | print "Note: Found " + str(counter) + " results." 156 | -------------------------------------------------------------------------------- /xenserver_check_bonds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | 23 | import sys 24 | import socket 25 | import time 26 | import re 27 | import os 28 | import XenAPI 29 | 30 | # check xen bond status 31 | 32 | DEBUG = False 33 | 34 | 35 | def log(s): 36 | if DEBUG: 37 | print s 38 | 39 | 40 | def get_bonds(session, host): 41 | bonds = {} 42 | slaves = {} 43 | 44 | for (b, brec) in session.xenapi.PIF.get_all_records().items(): 45 | # only local interfaces 46 | if brec['host'] != host: 47 | continue 48 | # save masters and slaves 49 | if brec['bond_master_of']: 50 | bonds[b] = brec 51 | if brec['bond_slave_of']: 52 | slaves[b] = brec 53 | 54 | for (m, mrec) in session.xenapi.PIF_metrics.get_all_records().items(): 55 | for srec in slaves.values(): 56 | if srec['metrics'] != m: 57 | continue 58 | srec['carrier'] = mrec['carrier'] 59 | 60 | for (n, nrec) in session.xenapi.network.get_all_records().items(): 61 | for brec in bonds.values(): 62 | if brec['network'] != n: 63 | continue 64 | brec['name_label'] = nrec['name_label'] 65 | 66 | return bonds, slaves 67 | 68 | 69 | def get_bond_status(session, host): 70 | status = {} 71 | for (b, brec) in session.xenapi.Bond.get_all_records().items(): 72 | status[b] = brec 73 | 74 | return status 75 | 76 | 77 | def main(): 78 | 79 | # First acquire a valid session by logging in: 80 | session = XenAPI.xapi_local() 81 | session.xenapi.login_with_password("root", "") 82 | 83 | hostname = socket.gethostname() 84 | host = (session.xenapi.host.get_by_name_label(hostname))[0] 85 | 86 | # warning when host not found... 87 | if not host: 88 | print "failed to detect XAPI host for '%s'" % hostname 89 | sys.exit(1) 90 | 91 | bonds, slaves = get_bonds(session, host) 92 | bond_status = get_bond_status(session, host) 93 | 94 | clist = [] 95 | olist = [] 96 | 97 | # iterate over the bonds 98 | for b in bonds: 99 | net = bonds[b]['name_label'] 100 | ref = bonds[b]['bond_master_of'][0] 101 | status = bond_status[ref] 102 | 103 | # On XenServer 6.0 we manually build links_up by checking carrier 104 | if 'links_up' not in status: 105 | 106 | status['links_up'] = 0 107 | 108 | for slave in status['slaves']: 109 | if slaves[slave]['carrier']: 110 | status['links_up'] += 1 111 | 112 | if len(status['slaves']) != int(status['links_up']): 113 | clist.append("%s has only %s links up (%s slaves)" 114 | % (net, status['links_up'], len(status['slaves']))) 115 | else: 116 | olist.append("%s %s links up" % (net, status['links_up'])) 117 | 118 | if len(clist): 119 | print "CRITICAL:", ", ".join(clist) 120 | return 2 121 | elif len(olist): 122 | print "OK:", ", ".join(olist) 123 | return 0 124 | else: 125 | print "OK: no bonds found" 126 | return 0 127 | 128 | if __name__ == "__main__": 129 | sys.exit(main()) 130 | -------------------------------------------------------------------------------- /xenserver_create_vlans.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Script to create the missing VLANs on a XEN host 23 | # Ferenc Born - fborn@schubergphilis.com 24 | 25 | networks=$(xe network-list params=name-label | grep "\ VLAN" | awk -F': ' '{ print $2}') 26 | 27 | for network in $networks 28 | do 29 | if [[ -z $(xe pif-list network-name-label=$network host-name-label=${HOSTNAME} --minimal) ]]; then 30 | vlan=${network##*-} 31 | networkuuid=$(xe network-list name-label=$network --minimal) 32 | pifuuid=$(xe pif-list device=bond0 VLAN=-1 host-name-label=${HOSTNAME} --minimal) 33 | echo "Note: Creating VLAN $vlan on hypervisor `hostname`" 34 | xe vlan-create network-uuid=$networkuuid pif-uuid=$pifuuid vlan=$vlan 35 | result=$? 36 | if [[ $result != 0 ]]; then 37 | echo "Error: Creating VLAN $vlan on hypervisor `hostname` failed with exit code $result" 38 | exit 1 39 | fi 40 | fi 41 | done 42 | -------------------------------------------------------------------------------- /xenserver_fake_pvtools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | usage() { 6 | echo "$0 " 7 | echo " -- fakes the presence of PV tools in " 8 | echo " -- NB the VM must be either paused or running on localhost" 9 | } 10 | 11 | fake(){ 12 | local uuid=$1 13 | domid=$(xe vm-list uuid=${uuid} params=dom-id --minimal) 14 | xenstore-write /local/domain/${domid}/attr/PVAddons/MajorVersion ${major} \ 15 | /local/domain/${domid}/attr/PVAddons/MinorVersion ${minor} \ 16 | /local/domain/${domid}/attr/PVAddons/MicroVersion ${micro} \ 17 | /local/domain/${domid}/data/updated 1 18 | } 19 | 20 | uuid=$(xe vm-list uuid=$1 params=uuid --minimal) 21 | if [ $? -ne 0 ]; then 22 | echo "Argument should be a VM uuid" 23 | usage 24 | exit 1 25 | fi 26 | 27 | # use the PRODUCT_VERSION from here: 28 | . /etc/xensource-inventory 29 | 30 | major=$(echo ${PRODUCT_VERSION} | cut -f 1 -d .) 31 | minor=$(echo ${PRODUCT_VERSION} | cut -f 2 -d .) 32 | micro=$(echo ${PRODUCT_VERSION} | cut -f 3 -d .) 33 | 34 | # Check the VM is running on this host 35 | resident_on=$(xe vm-list uuid=${uuid} params=resident-on --minimal) 36 | if [ "${resident_on}" != "${INSTALLATION_UUID}" ]; then 37 | echo "VM must be resident on this host" 38 | exit 2 39 | fi 40 | 41 | power_state=$(xe vm-list uuid=${uuid} params=power-state --minimal) 42 | case "${power_state}" in 43 | running) 44 | fake $uuid 45 | ;; 46 | paused) 47 | fake $uuid 48 | ;; 49 | *) 50 | echo "VM must be either running or paused" 51 | exit 3 52 | ;; 53 | esac 54 | -------------------------------------------------------------------------------- /xenserver_parallel_evacuate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015, Schuberg Philis BV 4 | # 5 | # Licensed to the Apache Software Foundation (ASF) under one 6 | # or more contributor license agreements. See the NOTICE file 7 | # distributed with this work for additional information 8 | # regarding copyright ownership. The ASF licenses this file 9 | # to you under the Apache License, Version 2.0 (the 10 | # "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, 16 | # software distributed under the License is distributed on an 17 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 | # KIND, either express or implied. See the License for the 19 | # specific language governing permissions and limitations 20 | # under the License. 21 | 22 | # Evacuate a XenServer using multiple threads in parallel 23 | # Remi Bergsma, rbergsma@schubergphilis.com 24 | 25 | # We depend on these modules 26 | import sys 27 | import os 28 | import getopt 29 | 30 | 31 | # Argument handling Class 32 | class handleArguments(object): 33 | def handleArguments(self,argv): 34 | self.DEBUG = 0 35 | self.DRYRUN = 1 36 | self.threads = 5 37 | self.skip_checks = False 38 | 39 | # Usage message 40 | help = "Usage: " + os.path.basename(__file__) + ' --threads [--debug --exec --skip-checks]' 41 | 42 | try: 43 | opts, args = getopt.getopt(argv,"ht:",["threads=","debug","exec","skip-checks"]) 44 | except getopt.GetoptError: 45 | print help 46 | sys.exit(2) 47 | for opt, arg in opts: 48 | if opt == '-h': 49 | print help 50 | sys.exit() 51 | elif opt in ("-t","--threads"): 52 | self.threads = arg 53 | elif opt in ("--debug"): 54 | self.DEBUG = 1 55 | elif opt in ("--exec"): 56 | self.DRYRUN = 0 57 | elif opt in ("--skip-checks"): 58 | self.skip_checks = True 59 | 60 | # Class to handle XenServer parallel evactation 61 | class xenserver_parallel_evacuation(object): 62 | 63 | def __init__(self, arg): 64 | self.DEBUG = arg.DEBUG 65 | self.DRYRUN = arg.DRYRUN 66 | self.threads = arg.threads 67 | self.poolmember = False 68 | self.vmlist = False 69 | self.hvlist = False 70 | self.skip_checks = arg.skip_checks 71 | 72 | # Run local command 73 | def run_local_command(self, command): 74 | if self.DEBUG == 1: 75 | print "Debug: Running command:" + command 76 | 77 | # XenServer 6.2 runs an ancient python 2.4 78 | # and the subprocess module did not work 79 | # properly. That's why I used ancient os.* 80 | p = os.popen(command) 81 | lines = "" 82 | while 1: 83 | line = p.readline() 84 | if not line: break 85 | lines += line 86 | 87 | return lines 88 | 89 | # Get overview of free memory on enabled hosts 90 | def get_hypervisor_free_memory(self): 91 | return self.run_local_command("for h in $(xe host-list params=name-label \ 92 | enabled=true --minimal | tr ',' ' '); \ 93 | do echo -n \"$h,\"; \ 94 | xe host-compute-free-memory host=$h;done") 95 | 96 | # Get overview of VMs and their memory 97 | def get_vms_with_memory_from_hypervisor(self, grep_for=None): 98 | try: 99 | grep_command = "" 100 | if grep_for is not None: 101 | grep_command = "| grep %s" % grep_for 102 | return self.run_local_command("xe vm-list resident-on=$(xe host-list params=uuid \ 103 | name-label=$HOSTNAME --minimal) \ 104 | params=name-label,memory-static-max is-control-domain=false |\ 105 | tr '\\n' ' ' | sed 's/name-label/\\n/g' | \ 106 | awk {'print $4 \",\" $8'} | sed '/^,$/d'" + grep_command) 107 | except: 108 | return False 109 | 110 | # Get overview of peer hypervisors and their available memory 111 | def construct_poolmembers(self): 112 | self.hvlist = self.get_hypervisor_free_memory() 113 | 114 | # Construct poolmembers 115 | poolmember = {} 116 | hvlist_iter = self.hvlist.split('\n') 117 | for hv in hvlist_iter: 118 | info = hv.split(',') 119 | try: 120 | hv = info[0] 121 | mem = info[1] 122 | except: 123 | continue 124 | poolmember[hv] = {} 125 | poolmember[hv]['memory_free'] = int(mem) 126 | poolmember[hv]['name'] = hv 127 | return poolmember 128 | 129 | # Get the hypervisor with the most free memory 130 | def get_hypervisor_with_most_free_memory(self): 131 | if self.poolmember == False: 132 | self.poolmember = self.construct_poolmembers() 133 | return sorted(self.poolmember.items(),key = lambda x :x[1]['memory_free'],reverse = True)[:1][0][1] 134 | 135 | # Generate migration plan 136 | def generate_migration_plan(self, grep_for=None): 137 | if self.skip_checks is False: 138 | # Make sure host is disabled 139 | if self.is_host_enabled() is not False: 140 | print "Error: Host should be disabled first." 141 | return False 142 | 143 | # Make sure pool HA is turned off 144 | if self.pool_ha_check() is not False: 145 | print "Error: Pool HA should be disabled first." 146 | return False 147 | 148 | # Generate migration plan 149 | migration_cmds = "" 150 | if self.vmlist == False: 151 | self.vmlist = self.get_vms_with_memory_from_hypervisor(grep_for) 152 | 153 | vmlist_iter = self.vmlist.split('\n') 154 | 155 | for vm in vmlist_iter: 156 | info = vm.split(',') 157 | try: 158 | vm = info[0] 159 | mem = int(info[1].strip()) 160 | except: 161 | continue 162 | 163 | while True: 164 | to_hv = self.get_hypervisor_with_most_free_memory() 165 | 166 | # If the hv with the most memory cannot host this vm, we're in trouble 167 | if to_hv['memory_free'] > mem: 168 | # update free_mem 169 | self.poolmember[to_hv['name']]['memory_free'] -= int(mem) 170 | # Prepare migration command 171 | migration_cmds += "xe vm-migrate vm=" + vm + " host=" + to_hv['name'] + ";\n" 172 | print "OK, found migration destination for " + vm 173 | break 174 | else: 175 | # Unable to empty this hv 176 | print "Error: not enough memory (need: " + str(mem) + ") on any hypervisor to migrate vm " + vm + ". This means N+1 rule is not met, please investigate!" 177 | return False 178 | return migration_cmds 179 | 180 | # Execute migration plan 181 | def execute_migration_plan(self, grep_for=None): 182 | try: 183 | migration_cmds = self.generate_migration_plan(grep_for) 184 | if migration_cmds == False: 185 | return False 186 | return self.run_local_command("nohup echo \"" + migration_cmds + "\" | \ 187 | xargs -n 1 -P " + str(self.threads) + " -I {} bash -c \{\} 2>&1 >/dev/null &") 188 | except: 189 | return False 190 | 191 | # Is host enabled? 192 | def is_host_enabled(self): 193 | print "Note: Checking if host is enabled or disabled.." 194 | try: 195 | if self.run_local_command("xe host-list params=enabled name-label=$HOSTNAME --minimal").strip() == "true": 196 | return True 197 | else: 198 | return False 199 | except: 200 | return "Error" 201 | 202 | # Check the current state of HA 203 | def pool_ha_check(self): 204 | try: 205 | if self.run_local_command("xe pool-list params=ha-enabled | \ 206 | awk {'print $5'} | tr -d '\n'") == "true": 207 | return True 208 | else: 209 | return False 210 | except: 211 | return "Error" 212 | 213 | # Main program 214 | if __name__ == "__main__": 215 | arg = handleArguments() 216 | arg.handleArguments(sys.argv[1:]) 217 | 218 | # Init our class 219 | x = xenserver_parallel_evacuation(arg) 220 | 221 | if arg.DRYRUN == 1: 222 | print "Note: Running in DRY-run mode, not executing. Use --exec to execute." 223 | print "Note: Calculating migration plan.." 224 | print "Note: This is the migration plan:" 225 | print "Instances (threads = %s)" % str(x.threads) 226 | print x.generate_migration_plan("i-") 227 | x.threads = 1 228 | x.vmlist = False 229 | print "Routers (threads = %s)" % str(x.threads) 230 | print x.generate_migration_plan("r-") 231 | sys.exit(0) 232 | 233 | print "Note: Executing migration plan using " + str(x.threads) + " threads.." 234 | print x.execute_migration_plan("i-") 235 | x.threads = 1 236 | x.vmlist = False 237 | print "Note: Executing migration plan using " + str(x.threads) + " threads.." 238 | print x.execute_migration_plan() 239 | 240 | -------------------------------------------------------------------------------- /xenserver_patches_to_install.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MissionCriticalCloud/cloudstackOps/cc21338b8f40437e52c309b2ea4d49e4bd3262ba/xenserver_patches_to_install.txt -------------------------------------------------------------------------------- /xenserver_post_empty_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "This is the post_empty script you could customise." 4 | 5 | -------------------------------------------------------------------------------- /xenserver_pre_empty_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "This is the pre_empty script you could customise." 4 | -------------------------------------------------------------------------------- /xenserver_upload_patches_to_poolmaster.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$(xe host-list name-label=$HOSTNAME --minimal)" == "$(xe pool-list params=master --minimal)" ] 4 | then 5 | echo "We are the poolmaster, so let's process the patches." 6 | 7 | echo "Unzipping patches" 8 | cd /root/xenserver_patches 9 | for file in $(ls /root/xenserver_patches/*.zip); do unzip ${file}; done 10 | 11 | echo "Deleting what we don't need" 12 | rm -f /root/xenserver_patches/*.zip /root/xenserver_patches/*.bz2 13 | 14 | # Check if already included 15 | XEN_ALL_PATCHES=$(xe patch-list params=name-label --minimal | tr ',' '\n' ) 16 | for patch in $(ls *.xsupdate) 17 | do 18 | echo "Processing patch file ${patch}" 19 | 20 | patchname=${patch%.*} 21 | echo "Checking if ${patchname} from file ${patch} is already uploaded" 22 | echo ${XEN_ALL_PATCHES} | tr ' ' '\n' | grep ^${patchname}$ 2>&1 >/dev/null 23 | if [ $? -eq 0 ]; then 24 | echo Patch ${patch} is already installed, skipping. 25 | else 26 | echo "Uploading patch ${patchname}" 27 | xe patch-upload file-name=${patch} 28 | if [ $? -gt 0 ]; then 29 | echo "Uploading failed, continuing with other patches" 30 | fi 31 | fi 32 | done 33 | else 34 | echo "We are NOT poolmaster, so skipping uploading patches" 35 | fi 36 | --------------------------------------------------------------------------------