# -- coding: utf-8 -- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # -------------------------------------------------------------------- # # Notes # -------------------------------------------------------------------- # # Vrouter # # eth0 router gateway IP for isolated network # eth1 Control IP for hypervisor # eth2 public ip(s) # # VPC Router # # eth0 control interface # eth1 public ip # eth2+ Guest networks # -------------------------------------------------------------------- # import os import logging import CsHelper from CsFile import CsFile from CsProcess import CsProcess from CsApp import CsPasswdSvc from CsAddress import CsDevice import socket from time import sleep class CsRedundant(object): CS_RAMDISK_DIR = "/ramdisk" CS_PRIO_UP = 1 CS_PRIO_DOWN = -1 CS_ROUTER_DIR = "%s/rrouter" % CS_RAMDISK_DIR CS_TEMPLATES = [ "heartbeat.sh.templ", "check_heartbeat.sh.templ", "arping_gateways.sh.templ" ] CS_TEMPLATES_DIR = "/opt/cloud/templates" CONNTRACKD_BIN = "/usr/sbin/conntrackd" CONNTRACKD_KEEPALIVED_CONFLOCK = "/var/lock/conntrack.lock" CONNTRACKD_CONF = "/etc/conntrackd/conntrackd.conf" RROUTER_LOG = "/var/log/cloud.log" KEEPALIVED_CONF = "/etc/keepalived/keepalived.conf" def __init__(self, config): self.cl = config.cmdline() self.address = config.address() self.config = config def set(self): logging.debug("Router redundancy status is %s", self.cl.is_redundant()) if self.cl.is_redundant(): self._redundant_on() else: self._redundant_off() def _redundant_off(self): CsHelper.service("conntrackd", "stop") CsHelper.service("keepalived", "stop") CsHelper.umount_tmpfs(self.CS_RAMDISK_DIR) CsHelper.rmdir(self.CS_RAMDISK_DIR) CsHelper.rm(self.CONNTRACKD_CONF) CsHelper.rm(self.KEEPALIVED_CONF) def _redundant_on(self): guest = self.address.get_guest_if() # No redundancy if there is no guest network if self.cl.is_master() or guest is None: for obj in [o for o in self.address.get_ips() if o.is_public()]: self.check_is_up(obj.get_device()) if guest is None: self._redundant_off() return CsHelper.mkdir(self.CS_RAMDISK_DIR, 0755, False) CsHelper.mount_tmpfs(self.CS_RAMDISK_DIR) CsHelper.mkdir(self.CS_ROUTER_DIR, 0755, False) for s in self.CS_TEMPLATES: d = s if s.endswith(".templ"): d = s.replace(".templ", "") CsHelper.copy_if_needed("%s/%s" % (self.CS_TEMPLATES_DIR, s), "%s/%s" % (self.CS_ROUTER_DIR, d)) CsHelper.copy_if_needed("%s/%s" % (self.CS_TEMPLATES_DIR, "keepalived.conf.templ"), self.KEEPALIVED_CONF) CsHelper.copy_if_needed("%s/%s" % (self.CS_TEMPLATES_DIR, "conntrackd.conf.templ"), self.CONNTRACKD_CONF) CsHelper.copy_if_needed("%s/%s" % (self.CS_TEMPLATES_DIR, "checkrouter.sh.templ"), "/opt/cloud/bin/checkrouter.sh") CsHelper.execute('sed -i "s/--exec\ \$DAEMON;/--exec\ \$DAEMON\ --\ --vrrp;/g" /etc/init.d/keepalived') # checkrouter.sh configuration file = CsFile("/opt/cloud/bin/checkrouter.sh") file.greplace("[RROUTER_LOG]", self.RROUTER_LOG) file.commit() # keepalived configuration file = CsFile(self.KEEPALIVED_CONF) ads = [o for o in self.address.get_ips() if o.is_public()] file.search(" router_id ", " router_id %s" % self.cl.get_name()) file.search(" interface ", " interface %s" % guest.get_device()) file.search(" virtual_router_id ", " virtual_router_id %s" % self.cl.get_router_id()) file.greplace("[RROUTER_BIN_PATH]", self.CS_ROUTER_DIR) file.section("authentication {", "}", [" auth_type AH \n", " auth_pass %s\n" % self.cl.get_router_password()]) file.section("virtual_ipaddress {", "}", self._collect_ips()) file.commit() # conntrackd configuration connt = CsFile(self.CONNTRACKD_CONF) if guest is not None: connt.section("Multicast {", "}", [ "IPv4_address 225.0.0.50\n", "Group 3780\n", "IPv4_interface %s\n" % guest.get_ip(), "Interface %s\n" % guest.get_device(), "SndSocketBuffer 1249280\n", "RcvSocketBuffer 1249280\n", "Checksum on\n"]) connt.section("Address Ignore {", "}", self._collect_ignore_ips()) connt.commit() if connt.is_changed(): CsHelper.service("conntrackd", "restart") if file.is_changed(): CsHelper.service("keepalived", "reload") # Configure heartbeat cron job cron = CsFile("/etc/cron.d/heartbeat") cron.add("SHELL=/bin/bash", 0) cron.add("PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin", 1) cron.add("*/1 * * * * root $SHELL %s/check_heartbeat.sh 2>&1 > /dev/null" % self.CS_ROUTER_DIR, -1) cron.commit() proc = CsProcess(['/usr/sbin/keepalived', '--vrrp']) if not proc.find(): CsHelper.service("keepalived", "start") def release_lock(self): try: os.remove("/tmp/master_lock") except OSError: pass def set_lock(self): """ Make sure that master state changes happen sequentially """ iterations = 10 time_between = 1 for iter in range(0, iterations): try: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.bind('/tmp/master_lock') return s except socket.error, e: error_code = e.args[0] error_string = e.args[1] print "Process already running (%d:%s). Exiting" % (error_code, error_string) logging.info("Master is already running, waiting") sleep(time_between) def set_fault(self): """ Set fault mode on this router """ if not self.cl.is_redundant(): logging.error("Set fault called on non-redundant router") return self.set_lock() logging.info("Router switched to fault mode") ads = [o for o in self.address.get_ips() if o.is_public()] for o in ads: CsHelper.execute("ifconfig %s down" % o.get_device()) cmd = "%s -C %s" % (self.CONNTRACKD_BIN, self.CONNTRACKD_CONF) CsHelper.execute("%s -s" % cmd) CsHelper.service("ipsec", "stop") CsHelper.service("xl2tpd", "stop") CsHelper.service("dnsmasq", "stop") ads = [o for o in self.address.get_ips() if o.needs_vrrp()] for o in ads: CsPasswdSvc(o.get_gateway()).stop() self.cl.set_fault_state() self.cl.save() self.release_lock() logging.info("Router switched to fault mode") def set_backup(self): """ Set the current router to backup """ if not self.cl.is_redundant(): logging.error("Set backup called on non-redundant router") return self.set_lock() logging.debug("Setting router to backup") ads = [o for o in self.address.get_ips() if o.is_public()] dev = '' for o in ads: if dev == o.get_device(): continue logging.info("Bringing public interface %s down" % o.get_device()) cmd2 = "ip link set %s up" % o.get_device() CsHelper.execute(cmd2) dev = o.get_device() cmd = "%s -C %s" % (self.CONNTRACKD_BIN, self.CONNTRACKD_CONF) CsHelper.execute("%s -d" % cmd) CsHelper.service("ipsec", "stop") CsHelper.service("xl2tpd", "stop") ads = [o for o in self.address.get_ips() if o.needs_vrrp()] for o in ads: CsPasswdSvc(o.get_gateway()).stop() CsHelper.service("dnsmasq", "stop") self.cl.set_master_state(False) self.cl.save() self.release_lock() logging.info("Router switched to backup mode") def set_master(self): """ Set the current router to master """ if not self.cl.is_redundant(): logging.error("Set master called on non-redundant router") return self.set_lock() logging.debug("Setting router to master") ads = [o for o in self.address.get_ips() if o.is_public()] dev = '' for o in ads: if dev == o.get_device(): continue cmd2 = "ip link set %s up" % o.get_device() if CsDevice(o.get_device(), self.config).waitfordevice(): CsHelper.execute(cmd2) dev = o.get_device() logging.info("Bringing public interface %s up" % o.get_device()) else: logging.error("Device %s was not ready could not bring it up" % o.get_device()) # ip route add default via $gw table Table_$dev proto static cmd = "%s -C %s" % (self.CONNTRACKD_BIN, self.CONNTRACKD_CONF) CsHelper.execute("%s -c" % cmd) CsHelper.execute("%s -f" % cmd) CsHelper.execute("%s -R" % cmd) CsHelper.execute("%s -B" % cmd) CsHelper.service("ipsec", "restart") CsHelper.service("xl2tpd", "restart") ads = [o for o in self.address.get_ips() if o.needs_vrrp()] for o in ads: CsPasswdSvc(o.get_gateway()).restart() CsHelper.service("dnsmasq", "restart") self.cl.set_master_state(True) self.cl.save() self.release_lock() logging.info("Router switched to master mode") def _collect_ignore_ips(self): """ This returns a list of ip objects that should be ignored by conntrackd """ lines = [] lines.append("\t\t\tIPv4_address %s\n" % "127.0.0.1") lines.append("\t\t\tIPv4_address %s\n" % self.address.get_control_if().get_ip()) # FIXME - Do we need to also add any internal network gateways? return lines def _collect_ips(self): """ Construct a list containing all the ips that need to be looked afer by vrrp This is based upon the address_needs_vrrp method in CsAddress which looks at the network type and decides if it is an internal address or an external one In a DomR there will only ever be one address in a VPC there can be many The new code also gives the possibility to cloudstack to have a hybrid device thet could function as a router and VPC router at the same time """ lines = [] for o in self.address.get_ips(): if o.needs_vrrp(): str = " %s brd %s dev %s\n" % (o.get_gateway_cidr(), o.get_broadcast(), o.get_device()) lines.append(str) self.check_is_up(o.get_device()) return lines def check_is_up(self, device): """ Ensure device is up """ cmd = "ip link show %s | grep 'state DOWN'" % device for i in CsHelper.execute(cmd): if " DOWN " in i: cmd2 = "ip link set %s up" % device CsHelper.execute(cmd2)