|
| 1 | +# -- coding: utf-8 -- |
| 2 | +# Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +# or more contributor license agreements. See the NOTICE file |
| 4 | +# distributed with this work for additional information |
| 5 | +# regarding copyright ownership. The ASF licenses this file |
| 6 | +# to you under the Apache License, Version 2.0 (the |
| 7 | +# "License"); you may not use this file except in compliance |
| 8 | +# with the License. You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, |
| 13 | +# software distributed under the License is distributed on an |
| 14 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +# KIND, either express or implied. See the License for the |
| 16 | +# specific language governing permissions and limitations |
| 17 | +# under the License. |
| 18 | +from CsDatabag import CsDataBag, CsCmdLine |
| 19 | +from CsApp import CsApache, CsDnsmasq, CsPasswdSvc |
| 20 | +import CsHelper |
| 21 | +import logging |
| 22 | +import CsHelper |
| 23 | +import subprocess |
| 24 | +from CsRoute import CsRoute |
| 25 | +from CsRule import CsRule |
| 26 | + |
| 27 | +class CsAddress(CsDataBag): |
| 28 | + |
| 29 | + def compare(self): |
| 30 | + for dev in CsDevice('', self.fw).list(): |
| 31 | + ip = CsIP(dev, self.fw) |
| 32 | + ip.compare(self.dbag) |
| 33 | + |
| 34 | + def get_ips(self): |
| 35 | + ret = [] |
| 36 | + for dev in self.dbag: |
| 37 | + if dev == "id": |
| 38 | + continue |
| 39 | + for ip in self.dbag[dev]: |
| 40 | + ret.append(CsInterface(ip)) |
| 41 | + return ret |
| 42 | + |
| 43 | + def needs_vrrp(self,o): |
| 44 | + """ |
| 45 | + Returns if the ip needs to be managed by keepalived or not |
| 46 | + """ |
| 47 | + if "nw_type" in o and o['nw_type'] in [ 'guest' ]: |
| 48 | + return True |
| 49 | + return False |
| 50 | + |
| 51 | + def get_control_if(self): |
| 52 | + """ |
| 53 | + Return the address object that has the control interface |
| 54 | + """ |
| 55 | + for ip in self.get_ips(): |
| 56 | + if ip.is_control(): |
| 57 | + return ip |
| 58 | + return None |
| 59 | + |
| 60 | + def process(self): |
| 61 | + for dev in self.dbag: |
| 62 | + if dev == "id": |
| 63 | + continue |
| 64 | + ip = CsIP(dev, self.fw) |
| 65 | + addcnt = 0 |
| 66 | + for address in self.dbag[dev]: |
| 67 | + if not address["nw_type"] == "control": |
| 68 | + CsRoute(dev).add(address) |
| 69 | + ip.setAddress(address) |
| 70 | + if ip.configured(): |
| 71 | + logging.info("Address %s on device %s already configured", ip.ip(), dev) |
| 72 | + ip.post_configure() |
| 73 | + else: |
| 74 | + logging.info("Address %s on device %s not configured", ip.ip(), dev) |
| 75 | + if CsDevice(dev, self.fw).waitfordevice(): |
| 76 | + ip.configure() |
| 77 | + # This could go one level up but the ip type is stored in the |
| 78 | + # ip address object and not in the device object |
| 79 | + # Call only once |
| 80 | + if addcnt == 0: |
| 81 | + self.add_netstats(address) |
| 82 | + addcnt += 1 |
| 83 | + |
| 84 | + def add_netstats(self, address): |
| 85 | + # add in the network stats iptables rules |
| 86 | + dev = "eth%s" % address['nic_dev_id'] |
| 87 | + if address["nw_type"] == "public_ip": |
| 88 | + self.fw.append(["", "front", "-A FORWARD -j NETWORK_STATS"]) |
| 89 | + self.fw.append(["", "front", "-A INPUT -j NETWORK_STATS"]) |
| 90 | + self.fw.append(["", "front", "-A OUTPUT -j NETWORK_STATS"]) |
| 91 | + # it is not possible to calculate these devices |
| 92 | + # When the vrouter and the vpc router are combined this silliness can go |
| 93 | + self.fw.append(["", "", "-A NETWORK_STATS -i %s -o eth0 -p tcp" % dev]) |
| 94 | + self.fw.append(["", "", "-A NETWORK_STATS -o %s -i eth0 -p tcp" % dev]) |
| 95 | + self.fw.append(["", "", "-A NETWORK_STATS -o %s ! -i eth0 -p tcp" % dev]) |
| 96 | + self.fw.append(["", "", "-A NETWORK_STATS -i %s ! -o eth0 -p tcp" % dev]) |
| 97 | + |
| 98 | + if address["nw_type"] == "guest": |
| 99 | + self.fw.append(["", "front", "-A FORWARD -j NETWORK_STATS_%s" % dev]) |
| 100 | + self.fw.append(["", "front", "-A NETWORK_STATS_%s -o %s -s %s" % (dev, dev, address['network'])]) |
| 101 | + self.fw.append(["", "front", "-A NETWORK_STATS_%s -o %s -d %s" % (dev, dev, address['network'])]) |
| 102 | + # Only relevant if there is a VPN configured so will have to move |
| 103 | + # at some stage |
| 104 | + self.fw.append(["mangle", "", "-A FORWARD -j VPN_STATS_%s" % dev]) |
| 105 | + self.fw.append(["mangle", "", "-A VPN_STATS_%s -o %s -m mark --set-xmark 0x525/0xffffffff" % (dev, dev)]) |
| 106 | + self.fw.append(["mangle", "", "-A VPN_STATS_%s -i %s -m mark --set-xmark 0x524/0xffffffff" % (dev, dev)]) |
| 107 | + |
| 108 | +class CsInterface: |
| 109 | + """ Hold one single ip """ |
| 110 | + def __init__(self, o): |
| 111 | + self.address = o |
| 112 | + |
| 113 | + def get_ip(self): |
| 114 | + return self.get_attr("public_ip") |
| 115 | + |
| 116 | + def get_device(self): |
| 117 | + return self.get_attr("device") |
| 118 | + |
| 119 | + def get_cidr(self): |
| 120 | + return self.get_attr("cidr") |
| 121 | + |
| 122 | + def get_broadcast(self): |
| 123 | + return self.get_attr("broadcast") |
| 124 | + |
| 125 | + def get_attr(self, attr): |
| 126 | + if attr in self.address: |
| 127 | + return self.address[attr] |
| 128 | + else: |
| 129 | + return "ERROR" |
| 130 | + |
| 131 | + def needs_vrrp(self): |
| 132 | + """ |
| 133 | + Returns if the ip needs to be managed by keepalived or not |
| 134 | + """ |
| 135 | + if "nw_type" in self.address and self.address['nw_type'] in [ 'guest' ]: |
| 136 | + return True |
| 137 | + return False |
| 138 | + |
| 139 | + def is_control(self): |
| 140 | + if "nw_type" in self.address and self.address['nw_type'] in [ 'control' ]: |
| 141 | + return True |
| 142 | + return False |
| 143 | + |
| 144 | + def to_str(self): |
| 145 | + pprint(self.address) |
| 146 | + |
| 147 | +class CsDevice: |
| 148 | + """ Configure Network Devices """ |
| 149 | + def __init__(self, dev, fw): |
| 150 | + self.devlist = [] |
| 151 | + self.dev = dev |
| 152 | + self.buildlist() |
| 153 | + self.table = '' |
| 154 | + self.tableNo = '' |
| 155 | + if dev != '': |
| 156 | + self.tableNo = dev[3] |
| 157 | + self.table = "Table_%s" % dev |
| 158 | + self.fw = fw |
| 159 | + |
| 160 | + def configure_rp(self): |
| 161 | + """ |
| 162 | + Configure Reverse Path Filtering |
| 163 | + """ |
| 164 | + filename = "/proc/sys/net/ipv4/conf/%s/rp_filter" % self.dev |
| 165 | + CsHelper.updatefile(filename, "1\n", "w") |
| 166 | + |
| 167 | + def buildlist(self): |
| 168 | + """ |
| 169 | + List all available network devices on the system |
| 170 | + """ |
| 171 | + self.devlist = [] |
| 172 | + for line in open('/proc/net/dev'): |
| 173 | + vals = line.lstrip().split(':') |
| 174 | + if (not vals[0].startswith("eth")): |
| 175 | + continue |
| 176 | + self.devlist.append(vals[0]) |
| 177 | + |
| 178 | + |
| 179 | + def waitfordevice(self): |
| 180 | + """ Wait up to 15 seconds for a device to become available """ |
| 181 | + count = 0 |
| 182 | + while count < 15: |
| 183 | + if self.dev in self.devlist: |
| 184 | + return True |
| 185 | + time.sleep(1) |
| 186 | + count += 1 |
| 187 | + self.buildlist(); |
| 188 | + logging.error("Device %s cannot be configured - device was not found", self.dev) |
| 189 | + return False |
| 190 | + |
| 191 | + def list(self): |
| 192 | + return self.devlist |
| 193 | + |
| 194 | + def setUp(self): |
| 195 | + """ Ensure device is up """ |
| 196 | + cmd = "ip link show %s | grep 'state DOWN'" % self.dev |
| 197 | + for i in CsHelper.execute(cmd): |
| 198 | + if " DOWN " in i: |
| 199 | + cmd2 = "ip link set %s up" % self.dev |
| 200 | + CsHelper.execute(cmd2) |
| 201 | + cmd = "-A PREROUTING -i %s -m state --state NEW -j CONNMARK --set-xmark 0x%s/0xffffffff" % \ |
| 202 | + (self.dev, self.dev[3]) |
| 203 | + self.fw.append(["mangle", "", cmd]) |
| 204 | + |
| 205 | + |
| 206 | +class CsIP: |
| 207 | + |
| 208 | + def __init__(self, dev, fw): |
| 209 | + self.dev = dev |
| 210 | + self.iplist = {} |
| 211 | + self.address = {} |
| 212 | + self.list() |
| 213 | + self.fw = fw |
| 214 | + |
| 215 | + def setAddress(self, address): |
| 216 | + self.address = address |
| 217 | + |
| 218 | + def getAddress(self): |
| 219 | + return self.address |
| 220 | + |
| 221 | + def configure(self): |
| 222 | + logging.info("Configuring address %s on device %s", self.ip(), self.dev) |
| 223 | + cmd = "ip addr add dev %s %s brd +" % (self.dev, self.ip()) |
| 224 | + subprocess.call(cmd, shell=True) |
| 225 | + self.post_configure() |
| 226 | + |
| 227 | + def post_configure(self): |
| 228 | + """ The steps that must be done after a device is configured """ |
| 229 | + if not self.get_type() in [ "control" ]: |
| 230 | + route = CsRoute(self.dev) |
| 231 | + route.routeTable() |
| 232 | + CsRule(self.dev).addMark() |
| 233 | + CsDevice(self.dev, self.fw).setUp() |
| 234 | + self.arpPing() |
| 235 | + CsRpsrfs(self.dev).enable() |
| 236 | + self.post_config_change("add") |
| 237 | + |
| 238 | + def get_type(self): |
| 239 | + """ Return the type of the IP |
| 240 | + guest |
| 241 | + control |
| 242 | + public |
| 243 | + """ |
| 244 | + if "nw_type" in self.address: |
| 245 | + return self.address['nw_type'] |
| 246 | + return "unknown" |
| 247 | + |
| 248 | + def get_ip_address(self): |
| 249 | + """ |
| 250 | + Return ip address if known |
| 251 | + """ |
| 252 | + if "public_ip" in self.address: |
| 253 | + return self.address['public_ip'] |
| 254 | + return "unknown" |
| 255 | + |
| 256 | + def post_config_change(self, method): |
| 257 | + route = CsRoute(self.dev) |
| 258 | + route.routeTable() |
| 259 | + route.add(self.address, method) |
| 260 | + # On deletion nw_type will no longer be known |
| 261 | + if self.get_type() in [ "guest" ]: |
| 262 | + devChain = "ACL_INBOUND_%s" % (self.dev) |
| 263 | + CsDevice(self.dev, self.fw).configure_rp() |
| 264 | + |
| 265 | + self.fw.append(["nat", "front", |
| 266 | + "-A POSTROUTING -s %s -o %s -j SNAT --to-source %s" % \ |
| 267 | + (self.address['network'], self.dev, self.address['public_ip']) |
| 268 | + ]) |
| 269 | + self.fw.append(["mangle", "front", "-A %s -j ACCEPT" % devChain]) |
| 270 | + |
| 271 | + self.fw.append(["", "front", |
| 272 | + "-A FORWARD -o %s -d %s -j %s" % (self.dev, self.address['network'], devChain) |
| 273 | + ]) |
| 274 | + self.fw.append(["", "", "-A %s -j DROP" % devChain]) |
| 275 | + self.fw.append(["mangle", "", |
| 276 | + "-A PREROUTING -m state --state NEW -i %s -s %s ! -d %s/32 -j %s" % \ |
| 277 | + (self.dev, self.address['network'], self.address['public_ip'], devChain) |
| 278 | + ]) |
| 279 | + dns = CsDnsmasq(self) |
| 280 | + dns.add_firewall_rules() |
| 281 | + app = CsApache(self) |
| 282 | + app.setup() |
| 283 | + pwdsvc = CsPasswdSvc(self).setup() |
| 284 | + elif self.get_type() == "public": |
| 285 | + if self.address["source_nat"] == True: |
| 286 | + cmdline = CsDataBag("cmdline", self.fw) |
| 287 | + dbag = cmdline.get_bag() |
| 288 | + type = dbag["config"]["type"] |
| 289 | + if type == "vpcrouter": |
| 290 | + vpccidr = dbag["config"]["vpccidr"] |
| 291 | + self.fw.append(["filter", "", "-A FORWARD -s %s ! -d %s -j ACCEPT" % (vpccidr, vpccidr)]) |
| 292 | + self.fw.append(["nat","","-A POSTROUTING -j SNAT -o %s --to-source %s" % (self.dev, self.address['public_ip'])]) |
| 293 | + elif type == "router": |
| 294 | + logging.error("Not able to setup sourcenat for a regular router yet") |
| 295 | + else: |
| 296 | + logging.error("Unable to process source nat configuration for router of type %s" % type) |
| 297 | + route.flush() |
| 298 | + |
| 299 | + def list(self): |
| 300 | + self.iplist = {} |
| 301 | + cmd = ("ip addr show dev " + self.dev) |
| 302 | + for i in CsHelper.execute(cmd): |
| 303 | + vals = i.lstrip().split() |
| 304 | + if (vals[0] == 'inet'): |
| 305 | + self.iplist[vals[1]] = self.dev |
| 306 | + |
| 307 | + def configured(self): |
| 308 | + if self.address['cidr'] in self.iplist.keys(): |
| 309 | + return True |
| 310 | + return False |
| 311 | + |
| 312 | + def ip(self): |
| 313 | + return str(self.address['cidr']) |
| 314 | + |
| 315 | + def getDevice(self): |
| 316 | + return self.dev |
| 317 | + |
| 318 | + def hasIP(self, ip): |
| 319 | + return ip in self.address.values() |
| 320 | + |
| 321 | + def arpPing(self): |
| 322 | + cmd = "arping -c 1 -I %s -A -U -s %s %s" % (self.dev, self.address['public_ip'], self.address['public_ip']) |
| 323 | + CsHelper.execute(cmd) |
| 324 | + |
| 325 | + # Delete any ips that are configured but not in the bag |
| 326 | + def compare(self, bag): |
| 327 | + if len(self.iplist) > 0 and (not self.dev in bag.keys() or len(bag[self.dev]) == 0): |
| 328 | + # Remove all IPs on this device |
| 329 | + logging.info("Will remove all configured addresses on device %s", self.dev) |
| 330 | + self.delete("all") |
| 331 | + app = CsApache(self) |
| 332 | + app.remove() |
| 333 | + |
| 334 | + # This condition should not really happen but did :) |
| 335 | + # It means an apache file got orphaned after a guest network address was deleted |
| 336 | + if len(self.iplist) == 0 and (not self.dev in bag.keys() or len(bag[self.dev]) == 0): |
| 337 | + app = CsApache(self) |
| 338 | + app.remove() |
| 339 | + |
| 340 | + for ip in self.iplist: |
| 341 | + found = False |
| 342 | + if self.dev in bag.keys(): |
| 343 | + for address in bag[self.dev]: |
| 344 | + self.setAddress(address) |
| 345 | + if self.hasIP(ip): |
| 346 | + found = True |
| 347 | + if not found: |
| 348 | + self.delete(ip) |
| 349 | + |
| 350 | + def delete(self, ip): |
| 351 | + remove = [] |
| 352 | + if ip == "all": |
| 353 | + logging.info("Removing addresses from device %s", self.dev) |
| 354 | + remove = self.iplist.keys() |
| 355 | + else: |
| 356 | + remove.append(ip) |
| 357 | + for ip in remove: |
| 358 | + cmd = "ip addr del dev %s %s" % (self.dev, ip) |
| 359 | + subprocess.call(cmd, shell=True) |
| 360 | + logging.info("Removed address %s from device %s", ip, self.dev) |
| 361 | + self.post_config_change("delete") |
| 362 | + |
| 363 | +class CsRpsrfs: |
| 364 | + """ Configure rpsrfs if there is more than one cpu """ |
| 365 | + |
| 366 | + def __init__(self, dev): |
| 367 | + self.dev = dev |
| 368 | + |
| 369 | + def enable(self): |
| 370 | + if not self.inKernel(): return |
| 371 | + cpus = self.cpus() |
| 372 | + if cpus < 2: return |
| 373 | + val = format((1 << cpus) - 1, "x") |
| 374 | + filename = "/sys/class/net/%s/queues/rx-0/rps_cpus" % (self.dev) |
| 375 | + CsHelper.updatefile(filename, val, "w+") |
| 376 | + CsHelper.updatefile("/proc/sys/net/core/rps_sock_flow_entries", "256", "w+") |
| 377 | + filename = "/sys/class/net/%s/queues/rx-0/rps_flow_cnt" % (self.dev) |
| 378 | + CsHelper.updatefile(filename, "256", "w+") |
| 379 | + logging.debug("rpsfr is configured for %s cpus" % (cpus)) |
| 380 | + |
| 381 | + def inKernel(self): |
| 382 | + try: |
| 383 | + open('/etc/rpsrfsenable') |
| 384 | + except IOError: |
| 385 | + logging.debug("rpsfr is not present in the kernel") |
| 386 | + return False |
| 387 | + else: |
| 388 | + logging.debug("rpsfr is present in the kernel") |
| 389 | + return True |
| 390 | + |
| 391 | + def cpus(self): |
| 392 | + count = 0 |
| 393 | + for line in open('/proc/cpuinfo'): |
| 394 | + if "processor" not in line: continue |
| 395 | + count += 1 |
| 396 | + if count < 2: logging.debug("Single CPU machine") |
| 397 | + return count |
0 commit comments