From 2462eab6f02105b15f4686a6183eb043d7b1a5e3 Mon Sep 17 00:00:00 2001 From: Antony Messerli Date: Mon, 20 Dec 2010 10:56:10 -0600 Subject: initial commit of xenserver host protections --- plugins/xenserver/networking/etc/init.d/host-rules | 83 ++++++++++++++++++++++ .../etc/xensource/scripts/vif_5.6-fp1.patch | 22 ++++++ .../networking/etc/xensource/scripts/vif_rules.py | 72 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100755 plugins/xenserver/networking/etc/init.d/host-rules create mode 100644 plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch create mode 100755 plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules new file mode 100755 index 000000000..980396bae --- /dev/null +++ b/plugins/xenserver/networking/etc/init.d/host-rules @@ -0,0 +1,83 @@ +#!/bin/bash +# +# host-rules Start/Stop the networking host rules +# +# chkconfig: 2345 85 15 +# description: Networking Host Rules for Multi Tenancy Protections + +iptables-up() +{ + iptables -P FORWARD DROP + iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT + iptables -A FORWARD -m physdev --physdev-in eth1 -j ACCEPT +} + +ebtables-up() +{ + ebtables -P FORWARD DROP + ebtables -A FORWARD -o eth0 -j ACCEPT + ebtables -A FORWARD -o eth1 -j ACCEPT +} + +arptables-up() +{ + arptables -P FORWARD DROP + arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT + arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + arptables -A FORWARD --opcode Request --in-interface eth1 -j ACCEPT + arptables -A FORWARD --opcode Reply --in-interface eth1 -j ACCEPT +} + +iptables-down() +{ + iptables -P FORWARD ACCEPT + iptables -D FORWARD -m physdev --physdev-in eth0 -j ACCEPT + iptables -D FORWARD -m physdev --physdev-in eth1 -j ACCEPT +} + +ebtables-down() +{ + ebtables -P FORWARD ACCEPT + ebtables -D FORWARD -o eth0 -j ACCEPT + ebtables -D FORWARD -o eth1 -j ACCEPT +} + +arptables-down() +{ + arptables -P FORWARD ACCEPT + arptables -D FORWARD --opcode Request --in-interface eth0 -j ACCEPT + arptables -D FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + arptables -D FORWARD --opcode Request --in-interface eth1 -j ACCEPT + arptables -D FORWARD --opcode Reply --in-interface eth1 -j ACCEPT +} + +start() +{ + iptables-up + ebtables-up + arptables-up +} + +stop() +{ + iptables-down + ebtables-down + arptables-down +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + *) + echo $"Usage: $0 {start|stop|restart}" + exit 1 +esac +exit 0 diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch new file mode 100644 index 000000000..142096ff1 --- /dev/null +++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch @@ -0,0 +1,22 @@ +--- vif 2010-12-20 16:39:46.000000000 +0000 ++++ vif_modified 2010-11-19 23:24:37.000000000 +0000 +@@ -213,6 +213,7 @@ + + # xs-xen.pq.hq:91e986b8e49f netback-wait-for-hotplug + xenstore-write "/local/domain/0/backend/vif/${DOMID}/${DEVID}/hotplug-status" "connected" ++ python /etc/xensource/scripts/vif_rules.py ${DOMID} online 2>&1 >> /dev/null + fi + ;; + +@@ -224,9 +225,11 @@ + + remove) + if [ "${TYPE}" = "vif" ] ;then ++ python /etc/xensource/scripts/vif_rules.py ${DOMID} offline 2>&1 >> /dev/null + xenstore-rm "${HOTPLUG}/hotplug" + fi + logger -t scripts-vif "${dev} has been removed" + remove_from_bridge + ;; + esac ++ diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py new file mode 100755 index 000000000..05141630b --- /dev/null +++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +from os import system, popen4 +import sys +import simplejson as json +from itertools import chain + +# order is important, mmmkay? 1 is domid, 2 command, 3 is vif +# when we add rules, we delete first, to make sure we only keep the one rule we need + +def main(): + fin,fout = popen4("/usr/bin/xenstore-ls /local/domain/%s/vm-data/networking" % sys.argv[1] ) + macs = fout.read().split("\n")[0:-1] + + for mac in macs: + m = mac.split("=")[0].strip() + fin,fout = popen4("/usr/bin/xenstore-read /local/domain/%s/vm-data/networking/%s" % (sys.argv[1],m)) + mjson = json.loads(fout.read()) + for ip in mjson['ips']: + if mjson["label"] == "public": + label = 0 + else: + label = 1 + + VIF = "vif%s.%s" % (sys.argv[1],label) + + if (len(sys.argv) == 4 and sys.argv[3] == VIF) or (len(sys.argv) == 3): + run_rules( + IP = ip['ip'], + VIF = VIF, + MAC = mjson['mac'], + STATUS = (sys.argv[2] == 'online') and '-A' or '-D' + ) + +def run_rules(**kwargs): + map(system, chain(ebtables(**kwargs), arptables(**kwargs), iptables(**kwargs) )) + +def iptables(**kwargs): + return [ + "/sbin/iptables -D FORWARD -m physdev --physdev-in %s -s %s -j ACCEPT 2>&1 > /dev/null" % ( kwargs['VIF'], kwargs['IP']), + "/sbin/iptables %s FORWARD -m physdev --physdev-in %s -s %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']) + ] + +def arptables(**kwargs): + return [ + "/sbin/arptables -D FORWARD --opcode Request --in-interface %s --source-ip %s --source-mac %s -j ACCEPT 2>&1 > /dev/null" % (kwargs['VIF'], kwargs['IP'], kwargs['MAC']), + "/sbin/arptables -D FORWARD --opcode Reply --in-interface %s --source-ip %s --source-mac %s -j ACCEPT 2>&1 > /dev/null" % (kwargs['VIF'], kwargs['IP'], kwargs['MAC']), + "/sbin/arptables %s FORWARD --opcode Request --in-interface %s --source-ip %s --source-mac %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP'], kwargs['MAC']), + "/sbin/arptables %s FORWARD --opcode Reply --in-interface %s --source-ip %s --source-mac %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP'], kwargs['MAC']) + ] + +def ebtables(**kwargs): + cmds = [ + "/sbin/ebtables -D FORWARD -p 0806 -o %s --arp-ip-dst %s -j ACCEPT 2>&1 >> /dev/null" % (kwargs['VIF'], kwargs['IP']), + "/sbin/ebtables -D FORWARD -p 0800 -o %s --ip-dst %s -j ACCEPT 2>&1 >> /dev/null" % (kwargs['VIF'], kwargs['IP']), + "/sbin/ebtables %s FORWARD -p 0806 -o %s --arp-ip-dst %s -j ACCEPT 2>&1 " % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']), + "/sbin/ebtables %s FORWARD -p 0800 -o %s --ip-dst %s -j ACCEPT 2>&1 " % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']) + ] + if kwargs['STATUS'] == "-A": + cmds.append("/sbin/ebtables -D FORWARD -s ! %s -i %s -j DROP 2>&1 > /dev/null" % (kwargs['MAC'], kwargs['VIF'])) + cmds.append("/sbin/ebtables -I FORWARD 1 -s ! %s -i %s -j DROP" % (kwargs['MAC'], kwargs['VIF'])) + else: + cmds.append("/sbin/ebtables %s FORWARD -s ! %s -i %s -j DROP" % (kwargs['STATUS'], kwargs['MAC'], kwargs['VIF'])) + return cmds + +def usage(): + print "Usage: slice_vifs.py optional: " + +if __name__ == "__main__": + if len(sys.argv) < 3: + usage() + else: + main() -- cgit From ab0cba603d96e25ee151222bb5fcf550459cfc7a Mon Sep 17 00:00:00 2001 From: Cory Wright Date: Mon, 20 Dec 2010 17:24:08 -0500 Subject: Rewrite of vif_rules.py to meet coding standards and be more pythonic in general. Use absolute paths for iptables/ebtables/arptables in host-rules. --- plugins/xenserver/networking/etc/init.d/host-rules | 54 ++++--- .../etc/xensource/scripts/vif_5.6-fp1.patch | 4 +- .../networking/etc/xensource/scripts/vif_rules.py | 180 +++++++++++++-------- 3 files changed, 146 insertions(+), 92 deletions(-) diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules index 980396bae..385c59629 100755 --- a/plugins/xenserver/networking/etc/init.d/host-rules +++ b/plugins/xenserver/networking/etc/init.d/host-rules @@ -5,50 +5,54 @@ # chkconfig: 2345 85 15 # description: Networking Host Rules for Multi Tenancy Protections +IPTABLES=/sbin/iptables +EBTABLES=/sbin/ebtables +ARPTABLES=/sbin/arptables + iptables-up() { - iptables -P FORWARD DROP - iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT - iptables -A FORWARD -m physdev --physdev-in eth1 -j ACCEPT + $IPTABLES -P FORWARD DROP + $IPTABLES -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT + $IPTABLES -A FORWARD -m physdev --physdev-in eth1 -j ACCEPT } ebtables-up() { - ebtables -P FORWARD DROP - ebtables -A FORWARD -o eth0 -j ACCEPT - ebtables -A FORWARD -o eth1 -j ACCEPT + $EBTABLES -P FORWARD DROP + $EBTABLES -A FORWARD -o eth0 -j ACCEPT + $EBTABLES -A FORWARD -o eth1 -j ACCEPT } arptables-up() { - arptables -P FORWARD DROP - arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT - arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT - arptables -A FORWARD --opcode Request --in-interface eth1 -j ACCEPT - arptables -A FORWARD --opcode Reply --in-interface eth1 -j ACCEPT + $ARPTABLES -P FORWARD DROP + $ARPTABLES -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT + $ARPTABLES -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + $ARPTABLES -A FORWARD --opcode Request --in-interface eth1 -j ACCEPT + $ARPTABLES -A FORWARD --opcode Reply --in-interface eth1 -j ACCEPT } iptables-down() { - iptables -P FORWARD ACCEPT - iptables -D FORWARD -m physdev --physdev-in eth0 -j ACCEPT - iptables -D FORWARD -m physdev --physdev-in eth1 -j ACCEPT + $IPTABLES -P FORWARD ACCEPT + $IPTABLES -D FORWARD -m physdev --physdev-in eth0 -j ACCEPT + $IPTABLES -D FORWARD -m physdev --physdev-in eth1 -j ACCEPT } ebtables-down() { - ebtables -P FORWARD ACCEPT - ebtables -D FORWARD -o eth0 -j ACCEPT - ebtables -D FORWARD -o eth1 -j ACCEPT + $EBTABLES -P FORWARD ACCEPT + $EBTABLES -D FORWARD -o eth0 -j ACCEPT + $EBTABLES -D FORWARD -o eth1 -j ACCEPT } arptables-down() { - arptables -P FORWARD ACCEPT - arptables -D FORWARD --opcode Request --in-interface eth0 -j ACCEPT - arptables -D FORWARD --opcode Reply --in-interface eth0 -j ACCEPT - arptables -D FORWARD --opcode Request --in-interface eth1 -j ACCEPT - arptables -D FORWARD --opcode Reply --in-interface eth1 -j ACCEPT + $ARPTABLES -P FORWARD ACCEPT + $ARPTABLES -D FORWARD --opcode Request --in-interface eth0 -j ACCEPT + $ARPTABLES -D FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + $ARPTABLES -D FORWARD --opcode Request --in-interface eth1 -j ACCEPT + $ARPTABLES -D FORWARD --opcode Reply --in-interface eth1 -j ACCEPT } start() @@ -68,16 +72,20 @@ stop() case "$1" in start) start + RETVAL=$? ;; stop) stop + RETVAL=$? ;; restart) stop start + RETVAL=$? ;; *) echo $"Usage: $0 {start|stop|restart}" exit 1 + ;; esac -exit 0 +exit $RETVAL diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch index 142096ff1..feaf1312d 100644 --- a/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch +++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_5.6-fp1.patch @@ -4,7 +4,7 @@ # xs-xen.pq.hq:91e986b8e49f netback-wait-for-hotplug xenstore-write "/local/domain/0/backend/vif/${DOMID}/${DEVID}/hotplug-status" "connected" -+ python /etc/xensource/scripts/vif_rules.py ${DOMID} online 2>&1 >> /dev/null ++ python /etc/xensource/scripts/vif_rules.py ${DOMID} online 2>&1 > /dev/null fi ;; @@ -12,7 +12,7 @@ remove) if [ "${TYPE}" = "vif" ] ;then -+ python /etc/xensource/scripts/vif_rules.py ${DOMID} offline 2>&1 >> /dev/null ++ python /etc/xensource/scripts/vif_rules.py ${DOMID} offline 2>&1 > /dev/null xenstore-rm "${HOTPLUG}/hotplug" fi logger -t scripts-vif "${dev} has been removed" diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py index 05141630b..dd27d3c6b 100755 --- a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py +++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py @@ -1,72 +1,118 @@ #!/usr/bin/env python -from os import system, popen4 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed 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. + +""" +This script is used to configure iptables, ebtables, and arptables rules for +XenServer instances. +""" + +import os +import subprocess import sys + +# This is written to Python 2.4, since that is what is available on XenServer import simplejson as json -from itertools import chain - -# order is important, mmmkay? 1 is domid, 2 command, 3 is vif -# when we add rules, we delete first, to make sure we only keep the one rule we need - -def main(): - fin,fout = popen4("/usr/bin/xenstore-ls /local/domain/%s/vm-data/networking" % sys.argv[1] ) - macs = fout.read().split("\n")[0:-1] - - for mac in macs: - m = mac.split("=")[0].strip() - fin,fout = popen4("/usr/bin/xenstore-read /local/domain/%s/vm-data/networking/%s" % (sys.argv[1],m)) - mjson = json.loads(fout.read()) - for ip in mjson['ips']: - if mjson["label"] == "public": - label = 0 - else: - label = 1 - - VIF = "vif%s.%s" % (sys.argv[1],label) - - if (len(sys.argv) == 4 and sys.argv[3] == VIF) or (len(sys.argv) == 3): - run_rules( - IP = ip['ip'], - VIF = VIF, - MAC = mjson['mac'], - STATUS = (sys.argv[2] == 'online') and '-A' or '-D' - ) - -def run_rules(**kwargs): - map(system, chain(ebtables(**kwargs), arptables(**kwargs), iptables(**kwargs) )) - -def iptables(**kwargs): - return [ - "/sbin/iptables -D FORWARD -m physdev --physdev-in %s -s %s -j ACCEPT 2>&1 > /dev/null" % ( kwargs['VIF'], kwargs['IP']), - "/sbin/iptables %s FORWARD -m physdev --physdev-in %s -s %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']) - ] - -def arptables(**kwargs): - return [ - "/sbin/arptables -D FORWARD --opcode Request --in-interface %s --source-ip %s --source-mac %s -j ACCEPT 2>&1 > /dev/null" % (kwargs['VIF'], kwargs['IP'], kwargs['MAC']), - "/sbin/arptables -D FORWARD --opcode Reply --in-interface %s --source-ip %s --source-mac %s -j ACCEPT 2>&1 > /dev/null" % (kwargs['VIF'], kwargs['IP'], kwargs['MAC']), - "/sbin/arptables %s FORWARD --opcode Request --in-interface %s --source-ip %s --source-mac %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP'], kwargs['MAC']), - "/sbin/arptables %s FORWARD --opcode Reply --in-interface %s --source-ip %s --source-mac %s -j ACCEPT" % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP'], kwargs['MAC']) - ] - -def ebtables(**kwargs): - cmds = [ - "/sbin/ebtables -D FORWARD -p 0806 -o %s --arp-ip-dst %s -j ACCEPT 2>&1 >> /dev/null" % (kwargs['VIF'], kwargs['IP']), - "/sbin/ebtables -D FORWARD -p 0800 -o %s --ip-dst %s -j ACCEPT 2>&1 >> /dev/null" % (kwargs['VIF'], kwargs['IP']), - "/sbin/ebtables %s FORWARD -p 0806 -o %s --arp-ip-dst %s -j ACCEPT 2>&1 " % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']), - "/sbin/ebtables %s FORWARD -p 0800 -o %s --ip-dst %s -j ACCEPT 2>&1 " % (kwargs['STATUS'], kwargs['VIF'], kwargs['IP']) - ] - if kwargs['STATUS'] == "-A": - cmds.append("/sbin/ebtables -D FORWARD -s ! %s -i %s -j DROP 2>&1 > /dev/null" % (kwargs['MAC'], kwargs['VIF'])) - cmds.append("/sbin/ebtables -I FORWARD 1 -s ! %s -i %s -j DROP" % (kwargs['MAC'], kwargs['VIF'])) - else: - cmds.append("/sbin/ebtables %s FORWARD -s ! %s -i %s -j DROP" % (kwargs['STATUS'], kwargs['MAC'], kwargs['VIF'])) - return cmds - -def usage(): - print "Usage: slice_vifs.py optional: " + + +def main(dom_id, command, only_this_vif=None): + xsls = execute("/usr/bin/xenstore-ls /local/domain/%s/vm-data/networking" \ + % dom_id, True) + macs = [line.split("=")[0].strip() for line in xsls.splitlines()] + + for mac in macs: + xsr = "/usr/bin/xenstore-read /local/domain/%s/vm-data/networking/%s" + xsread = execute(xsr % (dom_id, mac), True) + data = json.loads(xsread) + for ip in data['ips']: + if data["label"] == "public": + vif = "vif%s.0" % dom_id + else: + vif = "vif%s.1" % dom_id + + if (only_this_vif is None) or (vif == only_this_vif): + params = dict(IP=ip['ip'], VIF=vif, MAC=data['mac']) + apply_ebtables_rules(command, params) + apply_arptables_rules(command, params) + apply_iptables_rules(command, params) + + +def execute(command, return_stdout=False): + devnull = open(os.devnull, 'w') + proc = subprocess.Popen(command, shell=True, close_fds=True, + stdout=subprocess.PIPE, stderr=devnull) + if return_stdout: + return proc.stdout.read() + else: + return None + +# A note about adding rules: +# Whenever we add any rule to iptables, arptables or ebtables we first +# delete the same rule to ensure the rule only exists once. + + +def apply_iptables_rules(command, params): + iptables = lambda rule: execute("/sbin/iptables %s" % rule) + + iptables("-D FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \ + -j ACCEPT" % params) + if command == 'online': + iptables("-A FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \ + -j ACCEPT" % params) + + +def apply_arptables_rules(command, params): + arptables = lambda rule: execute("/sbin/arptables %s" % rule) + + arptables("-D FORWARD --opcode Request --in-interface %(VIF)s \ + --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params) + arptables("-D FORWARD --opcode Reply --in-interface %(VIF)s \ + --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params) + if command == 'online': + arptables("-A FORWARD --opcode Request --in-interface %(VIF)s \ + --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params) + arptables("-A FORWARD --opcode Reply --in-interface %(VIF)s \ + --source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params) + + +def apply_ebtables_rules(command, params): + ebtables = lambda rule: execute("/sbin/ebtables %s" % rule) + + ebtables("-D FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s -j ACCEPT" % + params) + ebtables("-D FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s -j ACCEPT" % + params) + if command == 'online': + ebtables("-A FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s \ + -j ACCEPT" % params) + ebtables("-A FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s \ + -j ACCEPT" % params) + + ebtables("-D FORWARD -s ! %(MAC)s -i %(VIF)s -j DROP" % params) + if command == 'online': + ebtables("-I FORWARD 1 -s ! %(MAC)s -i %(VIF)s -j DROP" % params) + if __name__ == "__main__": - if len(sys.argv) < 3: - usage() - else: - main() + if len(sys.argv) < 3: + print "usage: %s dom_id online|offline [vif]" % \ + os.path.basename(sys.argv[0]) + sys.exit(1) + else: + dom_id, command = sys.argv[1:3] + vif = len(sys.argv) == 4 and sys.argv[3] or None + main(dom_id, command, vif) -- cgit From 1b47ef95fff4d8419e27a7cc247178806cc065ff Mon Sep 17 00:00:00 2001 From: Cory Wright Date: Mon, 20 Dec 2010 18:15:40 -0500 Subject: Close devnull filehandle --- plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py index dd27d3c6b..d60816ce7 100755 --- a/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py +++ b/plugins/xenserver/networking/etc/xensource/scripts/vif_rules.py @@ -17,8 +17,8 @@ # under the License. """ -This script is used to configure iptables, ebtables, and arptables rules for -XenServer instances. +This script is used to configure iptables, ebtables, and arptables rules on +XenServer hosts. """ import os @@ -55,6 +55,7 @@ def execute(command, return_stdout=False): devnull = open(os.devnull, 'w') proc = subprocess.Popen(command, shell=True, close_fds=True, stdout=subprocess.PIPE, stderr=devnull) + devnull.close() if return_stdout: return proc.stdout.read() else: -- cgit From 130d75a8b240068a6251188da68296496c2c1564 Mon Sep 17 00:00:00 2001 From: Antony Messerli Date: Wed, 22 Dec 2010 11:27:23 -0600 Subject: Moved xenapi into xenserver specific directory --- plugins/xenapi/README | 6 - plugins/xenapi/etc/xapi.d/plugins/objectstore | 231 --------------------- .../xenapi/etc/xapi.d/plugins/pluginlib_nova.py | 216 ------------------- plugins/xenserver/xenapi/README | 6 + .../xenapi/etc/xapi.d/plugins/objectstore | 231 +++++++++++++++++++++ .../xenapi/etc/xapi.d/plugins/pluginlib_nova.py | 216 +++++++++++++++++++ 6 files changed, 453 insertions(+), 453 deletions(-) delete mode 100644 plugins/xenapi/README delete mode 100644 plugins/xenapi/etc/xapi.d/plugins/objectstore delete mode 100755 plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py create mode 100644 plugins/xenserver/xenapi/README create mode 100644 plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore create mode 100755 plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py diff --git a/plugins/xenapi/README b/plugins/xenapi/README deleted file mode 100644 index fbd471035..000000000 --- a/plugins/xenapi/README +++ /dev/null @@ -1,6 +0,0 @@ -This directory contains files that are required for the XenAPI support. They -should be installed in the XenServer / Xen Cloud Platform domain 0. - -Also, you need to - -chmod u+x /etc/xapi.d/plugins/objectstore diff --git a/plugins/xenapi/etc/xapi.d/plugins/objectstore b/plugins/xenapi/etc/xapi.d/plugins/objectstore deleted file mode 100644 index 271e7337f..000000000 --- a/plugins/xenapi/etc/xapi.d/plugins/objectstore +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2010 Citrix Systems, Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed 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. - -# -# XenAPI plugin for fetching images from nova-objectstore. -# - -import base64 -import errno -import hmac -import os -import os.path -import sha -import time -import urlparse - -import XenAPIPlugin - -from pluginlib_nova import * -configure_logging('objectstore') - - -KERNEL_DIR = '/boot/guest' - -DOWNLOAD_CHUNK_SIZE = 2 * 1024 * 1024 -SECTOR_SIZE = 512 -MBR_SIZE_SECTORS = 63 -MBR_SIZE_BYTES = MBR_SIZE_SECTORS * SECTOR_SIZE - - -def get_vdi(session, args): - src_url = exists(args, 'src_url') - username = exists(args, 'username') - password = exists(args, 'password') - add_partition = validate_bool(args, 'add_partition', 'false') - - (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) - - sr = find_sr(session) - if sr is None: - raise Exception('Cannot find SR to write VDI to') - - virtual_size = \ - get_content_length(proto, netloc, url_path, username, password) - if virtual_size < 0: - raise Exception('Cannot get VDI size') - - vdi_size = virtual_size - if add_partition: - # Make room for MBR. - vdi_size += MBR_SIZE_BYTES - - vdi = create_vdi(session, sr, src_url, vdi_size, False) - with_vdi_in_dom0(session, vdi, False, - lambda dev: get_vdi_(proto, netloc, url_path, - username, password, add_partition, - virtual_size, '/dev/%s' % dev)) - return session.xenapi.VDI.get_uuid(vdi) - - -def get_vdi_(proto, netloc, url_path, username, password, add_partition, - virtual_size, dest): - - if add_partition: - write_partition(virtual_size, dest) - - offset = add_partition and MBR_SIZE_BYTES or 0 - get(proto, netloc, url_path, username, password, dest, offset) - - -def write_partition(virtual_size, dest): - mbr_last = MBR_SIZE_SECTORS - 1 - primary_first = MBR_SIZE_SECTORS - primary_last = MBR_SIZE_SECTORS + (virtual_size / SECTOR_SIZE) - 1 - - logging.debug('Writing partition table %d %d to %s...', - primary_first, primary_last, dest) - - result = os.system('parted --script %s mklabel msdos' % dest) - if result != 0: - raise Exception('Failed to mklabel') - result = os.system('parted --script %s mkpart primary %ds %ds' % - (dest, primary_first, primary_last)) - if result != 0: - raise Exception('Failed to mkpart') - - logging.debug('Writing partition table %s done.', dest) - - -def find_sr(session): - host = get_this_host(session) - srs = session.xenapi.SR.get_all() - for sr in srs: - sr_rec = session.xenapi.SR.get_record(sr) - if not ('i18n-key' in sr_rec['other_config'] and - sr_rec['other_config']['i18n-key'] == 'local-storage'): - continue - for pbd in sr_rec['PBDs']: - pbd_rec = session.xenapi.PBD.get_record(pbd) - if pbd_rec['host'] == host: - return sr - return None - - -def get_kernel(session, args): - src_url = exists(args, 'src_url') - username = exists(args, 'username') - password = exists(args, 'password') - - (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) - - dest = os.path.join(KERNEL_DIR, url_path[1:]) - - # Paranoid check against people using ../ to do rude things. - if os.path.commonprefix([KERNEL_DIR, dest]) != KERNEL_DIR: - raise Exception('Illegal destination %s %s', (url_path, dest)) - - dirname = os.path.dirname(dest) - try: - os.makedirs(dirname) - except os.error, e: - if e.errno != errno.EEXIST: - raise - if not os.path.isdir(dirname): - raise Exception('Cannot make directory %s', dirname) - - try: - os.remove(dest) - except: - pass - - get(proto, netloc, url_path, username, password, dest, 0) - - return dest - - -def get_content_length(proto, netloc, url_path, username, password): - headers = make_headers('HEAD', url_path, username, password) - return with_http_connection( - proto, netloc, - lambda conn: get_content_length_(url_path, headers, conn)) - - -def get_content_length_(url_path, headers, conn): - conn.request('HEAD', url_path, None, headers) - response = conn.getresponse() - if response.status != 200: - raise Exception('%d %s' % (response.status, response.reason)) - - return long(response.getheader('Content-Length', -1)) - - -def get(proto, netloc, url_path, username, password, dest, offset): - headers = make_headers('GET', url_path, username, password) - download(proto, netloc, url_path, headers, dest, offset) - - -def make_headers(verb, url_path, username, password): - headers = {} - headers['Date'] = \ - time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) - headers['Authorization'] = \ - 'AWS %s:%s' % (username, - s3_authorization(verb, url_path, password, headers)) - return headers - - -def s3_authorization(verb, path, password, headers): - sha1 = hmac.new(password, digestmod=sha) - sha1.update(plaintext(verb, path, headers)) - return base64.encodestring(sha1.digest()).strip() - - -def plaintext(verb, path, headers): - return '%s\n\n\n%s\n%s' % (verb, - "\n".join([headers[h] for h in headers]), - path) - - -def download(proto, netloc, url_path, headers, dest, offset): - with_http_connection( - proto, netloc, - lambda conn: download_(url_path, dest, offset, headers, conn)) - - -def download_(url_path, dest, offset, headers, conn): - conn.request('GET', url_path, None, headers) - response = conn.getresponse() - if response.status != 200: - raise Exception('%d %s' % (response.status, response.reason)) - - length = response.getheader('Content-Length', -1) - - with_file( - dest, 'a', - lambda dest_file: download_all(response, length, dest_file, offset)) - - -def download_all(response, length, dest_file, offset): - dest_file.seek(offset) - i = 0 - while True: - buf = response.read(DOWNLOAD_CHUNK_SIZE) - if buf: - dest_file.write(buf) - else: - return - i += len(buf) - if length != -1 and i >= length: - return - - -if __name__ == '__main__': - XenAPIPlugin.dispatch({'get_vdi': get_vdi, - 'get_kernel': get_kernel}) diff --git a/plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py b/plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py deleted file mode 100755 index 2d323a016..000000000 --- a/plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright (c) 2010 Citrix Systems, Inc. -# -# Licensed 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. - -# -# Helper functions for the Nova xapi plugins. In time, this will merge -# with the pluginlib.py shipped with xapi, but for now, that file is not -# very stable, so it's easiest just to have a copy of all the functions -# that we need. -# - -import httplib -import logging -import logging.handlers -import re -import time - - -##### Logging setup - -def configure_logging(name): - log = logging.getLogger() - log.setLevel(logging.DEBUG) - sysh = logging.handlers.SysLogHandler('/dev/log') - sysh.setLevel(logging.DEBUG) - formatter = logging.Formatter('%s: %%(levelname)-8s %%(message)s' % name) - sysh.setFormatter(formatter) - log.addHandler(sysh) - - -##### Exceptions - -class PluginError(Exception): - """Base Exception class for all plugin errors.""" - def __init__(self, *args): - Exception.__init__(self, *args) - -class ArgumentError(PluginError): - """Raised when required arguments are missing, argument values are invalid, - or incompatible arguments are given. - """ - def __init__(self, *args): - PluginError.__init__(self, *args) - - -##### Helpers - -def ignore_failure(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except XenAPI.Failure, e: - logging.error('Ignoring XenAPI.Failure %s', e) - return None - - -##### Argument validation - -ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$') - -def validate_exists(args, key, default=None): - """Validates that a string argument to a RPC method call is given, and - matches the shell-safe regex, with an optional default value in case it - does not exist. - - Returns the string. - """ - if key in args: - if len(args[key]) == 0: - raise ArgumentError('Argument %r value %r is too short.' % (key, args[key])) - if not ARGUMENT_PATTERN.match(args[key]): - raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key])) - if args[key][0] == '-': - raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key])) - return args[key] - elif default is not None: - return default - else: - raise ArgumentError('Argument %s is required.' % key) - -def validate_bool(args, key, default=None): - """Validates that a string argument to a RPC method call is a boolean string, - with an optional default value in case it does not exist. - - Returns the python boolean value. - """ - value = validate_exists(args, key, default) - if value.lower() == 'true': - return True - elif value.lower() == 'false': - return False - else: - raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value)) - -def exists(args, key): - """Validates that a freeform string argument to a RPC method call is given. - Returns the string. - """ - if key in args: - return args[key] - else: - raise ArgumentError('Argument %s is required.' % key) - -def optional(args, key): - """If the given key is in args, return the corresponding value, otherwise - return None""" - return key in args and args[key] or None - - -def get_this_host(session): - return session.xenapi.session.get_this_host(session.handle) - - -def get_domain_0(session): - this_host_ref = get_this_host(session) - expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref - return session.xenapi.VM.get_all_records_where(expr).keys()[0] - - -def create_vdi(session, sr_ref, name_label, virtual_size, read_only): - vdi_ref = session.xenapi.VDI.create( - { 'name_label': name_label, - 'name_description': '', - 'SR': sr_ref, - 'virtual_size': str(virtual_size), - 'type': 'User', - 'sharable': False, - 'read_only': read_only, - 'xenstore_data': {}, - 'other_config': {}, - 'sm_config': {}, - 'tags': [] }) - logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label, - virtual_size, read_only, sr_ref) - return vdi_ref - - -def with_vdi_in_dom0(session, vdi, read_only, f): - dom0 = get_domain_0(session) - vbd_rec = {} - vbd_rec['VM'] = dom0 - vbd_rec['VDI'] = vdi - vbd_rec['userdevice'] = 'autodetect' - vbd_rec['bootable'] = False - vbd_rec['mode'] = read_only and 'RO' or 'RW' - vbd_rec['type'] = 'disk' - vbd_rec['unpluggable'] = True - vbd_rec['empty'] = False - vbd_rec['other_config'] = {} - vbd_rec['qos_algorithm_type'] = '' - vbd_rec['qos_algorithm_params'] = {} - vbd_rec['qos_supported_algorithms'] = [] - logging.debug('Creating VBD for VDI %s ... ', vdi) - vbd = session.xenapi.VBD.create(vbd_rec) - logging.debug('Creating VBD for VDI %s done.', vdi) - try: - logging.debug('Plugging VBD %s ... ', vbd) - session.xenapi.VBD.plug(vbd) - logging.debug('Plugging VBD %s done.', vbd) - return f(session.xenapi.VBD.get_device(vbd)) - finally: - logging.debug('Destroying VBD for VDI %s ... ', vdi) - vbd_unplug_with_retry(session, vbd) - ignore_failure(session.xenapi.VBD.destroy, vbd) - logging.debug('Destroying VBD for VDI %s done.', vdi) - - -def vbd_unplug_with_retry(session, vbd): - """Call VBD.unplug on the given VBD, with a retry if we get - DEVICE_DETACH_REJECTED. For reasons which I don't understand, we're - seeing the device still in use, even when all processes using the device - should be dead.""" - while True: - try: - session.xenapi.VBD.unplug(vbd) - logging.debug('VBD.unplug successful first time.') - return - except XenAPI.Failure, e: - if (len(e.details) > 0 and - e.details[0] == 'DEVICE_DETACH_REJECTED'): - logging.debug('VBD.unplug rejected: retrying...') - time.sleep(1) - elif (len(e.details) > 0 and - e.details[0] == 'DEVICE_ALREADY_DETACHED'): - logging.debug('VBD.unplug successful eventually.') - return - else: - logging.error('Ignoring XenAPI.Failure in VBD.unplug: %s', e) - return - - -def with_http_connection(proto, netloc, f): - conn = (proto == 'https' and - httplib.HTTPSConnection(netloc) or - httplib.HTTPConnection(netloc)) - try: - return f(conn) - finally: - conn.close() - - -def with_file(dest_path, mode, f): - dest = open(dest_path, mode) - try: - return f(dest) - finally: - dest.close() diff --git a/plugins/xenserver/xenapi/README b/plugins/xenserver/xenapi/README new file mode 100644 index 000000000..fbd471035 --- /dev/null +++ b/plugins/xenserver/xenapi/README @@ -0,0 +1,6 @@ +This directory contains files that are required for the XenAPI support. They +should be installed in the XenServer / Xen Cloud Platform domain 0. + +Also, you need to + +chmod u+x /etc/xapi.d/plugins/objectstore diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore b/plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore new file mode 100644 index 000000000..271e7337f --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore @@ -0,0 +1,231 @@ +#!/usr/bin/env python + +# Copyright (c) 2010 Citrix Systems, Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed 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. + +# +# XenAPI plugin for fetching images from nova-objectstore. +# + +import base64 +import errno +import hmac +import os +import os.path +import sha +import time +import urlparse + +import XenAPIPlugin + +from pluginlib_nova import * +configure_logging('objectstore') + + +KERNEL_DIR = '/boot/guest' + +DOWNLOAD_CHUNK_SIZE = 2 * 1024 * 1024 +SECTOR_SIZE = 512 +MBR_SIZE_SECTORS = 63 +MBR_SIZE_BYTES = MBR_SIZE_SECTORS * SECTOR_SIZE + + +def get_vdi(session, args): + src_url = exists(args, 'src_url') + username = exists(args, 'username') + password = exists(args, 'password') + add_partition = validate_bool(args, 'add_partition', 'false') + + (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) + + sr = find_sr(session) + if sr is None: + raise Exception('Cannot find SR to write VDI to') + + virtual_size = \ + get_content_length(proto, netloc, url_path, username, password) + if virtual_size < 0: + raise Exception('Cannot get VDI size') + + vdi_size = virtual_size + if add_partition: + # Make room for MBR. + vdi_size += MBR_SIZE_BYTES + + vdi = create_vdi(session, sr, src_url, vdi_size, False) + with_vdi_in_dom0(session, vdi, False, + lambda dev: get_vdi_(proto, netloc, url_path, + username, password, add_partition, + virtual_size, '/dev/%s' % dev)) + return session.xenapi.VDI.get_uuid(vdi) + + +def get_vdi_(proto, netloc, url_path, username, password, add_partition, + virtual_size, dest): + + if add_partition: + write_partition(virtual_size, dest) + + offset = add_partition and MBR_SIZE_BYTES or 0 + get(proto, netloc, url_path, username, password, dest, offset) + + +def write_partition(virtual_size, dest): + mbr_last = MBR_SIZE_SECTORS - 1 + primary_first = MBR_SIZE_SECTORS + primary_last = MBR_SIZE_SECTORS + (virtual_size / SECTOR_SIZE) - 1 + + logging.debug('Writing partition table %d %d to %s...', + primary_first, primary_last, dest) + + result = os.system('parted --script %s mklabel msdos' % dest) + if result != 0: + raise Exception('Failed to mklabel') + result = os.system('parted --script %s mkpart primary %ds %ds' % + (dest, primary_first, primary_last)) + if result != 0: + raise Exception('Failed to mkpart') + + logging.debug('Writing partition table %s done.', dest) + + +def find_sr(session): + host = get_this_host(session) + srs = session.xenapi.SR.get_all() + for sr in srs: + sr_rec = session.xenapi.SR.get_record(sr) + if not ('i18n-key' in sr_rec['other_config'] and + sr_rec['other_config']['i18n-key'] == 'local-storage'): + continue + for pbd in sr_rec['PBDs']: + pbd_rec = session.xenapi.PBD.get_record(pbd) + if pbd_rec['host'] == host: + return sr + return None + + +def get_kernel(session, args): + src_url = exists(args, 'src_url') + username = exists(args, 'username') + password = exists(args, 'password') + + (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) + + dest = os.path.join(KERNEL_DIR, url_path[1:]) + + # Paranoid check against people using ../ to do rude things. + if os.path.commonprefix([KERNEL_DIR, dest]) != KERNEL_DIR: + raise Exception('Illegal destination %s %s', (url_path, dest)) + + dirname = os.path.dirname(dest) + try: + os.makedirs(dirname) + except os.error, e: + if e.errno != errno.EEXIST: + raise + if not os.path.isdir(dirname): + raise Exception('Cannot make directory %s', dirname) + + try: + os.remove(dest) + except: + pass + + get(proto, netloc, url_path, username, password, dest, 0) + + return dest + + +def get_content_length(proto, netloc, url_path, username, password): + headers = make_headers('HEAD', url_path, username, password) + return with_http_connection( + proto, netloc, + lambda conn: get_content_length_(url_path, headers, conn)) + + +def get_content_length_(url_path, headers, conn): + conn.request('HEAD', url_path, None, headers) + response = conn.getresponse() + if response.status != 200: + raise Exception('%d %s' % (response.status, response.reason)) + + return long(response.getheader('Content-Length', -1)) + + +def get(proto, netloc, url_path, username, password, dest, offset): + headers = make_headers('GET', url_path, username, password) + download(proto, netloc, url_path, headers, dest, offset) + + +def make_headers(verb, url_path, username, password): + headers = {} + headers['Date'] = \ + time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + headers['Authorization'] = \ + 'AWS %s:%s' % (username, + s3_authorization(verb, url_path, password, headers)) + return headers + + +def s3_authorization(verb, path, password, headers): + sha1 = hmac.new(password, digestmod=sha) + sha1.update(plaintext(verb, path, headers)) + return base64.encodestring(sha1.digest()).strip() + + +def plaintext(verb, path, headers): + return '%s\n\n\n%s\n%s' % (verb, + "\n".join([headers[h] for h in headers]), + path) + + +def download(proto, netloc, url_path, headers, dest, offset): + with_http_connection( + proto, netloc, + lambda conn: download_(url_path, dest, offset, headers, conn)) + + +def download_(url_path, dest, offset, headers, conn): + conn.request('GET', url_path, None, headers) + response = conn.getresponse() + if response.status != 200: + raise Exception('%d %s' % (response.status, response.reason)) + + length = response.getheader('Content-Length', -1) + + with_file( + dest, 'a', + lambda dest_file: download_all(response, length, dest_file, offset)) + + +def download_all(response, length, dest_file, offset): + dest_file.seek(offset) + i = 0 + while True: + buf = response.read(DOWNLOAD_CHUNK_SIZE) + if buf: + dest_file.write(buf) + else: + return + i += len(buf) + if length != -1 and i >= length: + return + + +if __name__ == '__main__': + XenAPIPlugin.dispatch({'get_vdi': get_vdi, + 'get_kernel': get_kernel}) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py new file mode 100755 index 000000000..2d323a016 --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py @@ -0,0 +1,216 @@ +# Copyright (c) 2010 Citrix Systems, Inc. +# +# Licensed 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. + +# +# Helper functions for the Nova xapi plugins. In time, this will merge +# with the pluginlib.py shipped with xapi, but for now, that file is not +# very stable, so it's easiest just to have a copy of all the functions +# that we need. +# + +import httplib +import logging +import logging.handlers +import re +import time + + +##### Logging setup + +def configure_logging(name): + log = logging.getLogger() + log.setLevel(logging.DEBUG) + sysh = logging.handlers.SysLogHandler('/dev/log') + sysh.setLevel(logging.DEBUG) + formatter = logging.Formatter('%s: %%(levelname)-8s %%(message)s' % name) + sysh.setFormatter(formatter) + log.addHandler(sysh) + + +##### Exceptions + +class PluginError(Exception): + """Base Exception class for all plugin errors.""" + def __init__(self, *args): + Exception.__init__(self, *args) + +class ArgumentError(PluginError): + """Raised when required arguments are missing, argument values are invalid, + or incompatible arguments are given. + """ + def __init__(self, *args): + PluginError.__init__(self, *args) + + +##### Helpers + +def ignore_failure(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except XenAPI.Failure, e: + logging.error('Ignoring XenAPI.Failure %s', e) + return None + + +##### Argument validation + +ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$') + +def validate_exists(args, key, default=None): + """Validates that a string argument to a RPC method call is given, and + matches the shell-safe regex, with an optional default value in case it + does not exist. + + Returns the string. + """ + if key in args: + if len(args[key]) == 0: + raise ArgumentError('Argument %r value %r is too short.' % (key, args[key])) + if not ARGUMENT_PATTERN.match(args[key]): + raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key])) + if args[key][0] == '-': + raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key])) + return args[key] + elif default is not None: + return default + else: + raise ArgumentError('Argument %s is required.' % key) + +def validate_bool(args, key, default=None): + """Validates that a string argument to a RPC method call is a boolean string, + with an optional default value in case it does not exist. + + Returns the python boolean value. + """ + value = validate_exists(args, key, default) + if value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + else: + raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value)) + +def exists(args, key): + """Validates that a freeform string argument to a RPC method call is given. + Returns the string. + """ + if key in args: + return args[key] + else: + raise ArgumentError('Argument %s is required.' % key) + +def optional(args, key): + """If the given key is in args, return the corresponding value, otherwise + return None""" + return key in args and args[key] or None + + +def get_this_host(session): + return session.xenapi.session.get_this_host(session.handle) + + +def get_domain_0(session): + this_host_ref = get_this_host(session) + expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref + return session.xenapi.VM.get_all_records_where(expr).keys()[0] + + +def create_vdi(session, sr_ref, name_label, virtual_size, read_only): + vdi_ref = session.xenapi.VDI.create( + { 'name_label': name_label, + 'name_description': '', + 'SR': sr_ref, + 'virtual_size': str(virtual_size), + 'type': 'User', + 'sharable': False, + 'read_only': read_only, + 'xenstore_data': {}, + 'other_config': {}, + 'sm_config': {}, + 'tags': [] }) + logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label, + virtual_size, read_only, sr_ref) + return vdi_ref + + +def with_vdi_in_dom0(session, vdi, read_only, f): + dom0 = get_domain_0(session) + vbd_rec = {} + vbd_rec['VM'] = dom0 + vbd_rec['VDI'] = vdi + vbd_rec['userdevice'] = 'autodetect' + vbd_rec['bootable'] = False + vbd_rec['mode'] = read_only and 'RO' or 'RW' + vbd_rec['type'] = 'disk' + vbd_rec['unpluggable'] = True + vbd_rec['empty'] = False + vbd_rec['other_config'] = {} + vbd_rec['qos_algorithm_type'] = '' + vbd_rec['qos_algorithm_params'] = {} + vbd_rec['qos_supported_algorithms'] = [] + logging.debug('Creating VBD for VDI %s ... ', vdi) + vbd = session.xenapi.VBD.create(vbd_rec) + logging.debug('Creating VBD for VDI %s done.', vdi) + try: + logging.debug('Plugging VBD %s ... ', vbd) + session.xenapi.VBD.plug(vbd) + logging.debug('Plugging VBD %s done.', vbd) + return f(session.xenapi.VBD.get_device(vbd)) + finally: + logging.debug('Destroying VBD for VDI %s ... ', vdi) + vbd_unplug_with_retry(session, vbd) + ignore_failure(session.xenapi.VBD.destroy, vbd) + logging.debug('Destroying VBD for VDI %s done.', vdi) + + +def vbd_unplug_with_retry(session, vbd): + """Call VBD.unplug on the given VBD, with a retry if we get + DEVICE_DETACH_REJECTED. For reasons which I don't understand, we're + seeing the device still in use, even when all processes using the device + should be dead.""" + while True: + try: + session.xenapi.VBD.unplug(vbd) + logging.debug('VBD.unplug successful first time.') + return + except XenAPI.Failure, e: + if (len(e.details) > 0 and + e.details[0] == 'DEVICE_DETACH_REJECTED'): + logging.debug('VBD.unplug rejected: retrying...') + time.sleep(1) + elif (len(e.details) > 0 and + e.details[0] == 'DEVICE_ALREADY_DETACHED'): + logging.debug('VBD.unplug successful eventually.') + return + else: + logging.error('Ignoring XenAPI.Failure in VBD.unplug: %s', e) + return + + +def with_http_connection(proto, netloc, f): + conn = (proto == 'https' and + httplib.HTTPSConnection(netloc) or + httplib.HTTPConnection(netloc)) + try: + return f(conn) + finally: + conn.close() + + +def with_file(dest_path, mode, f): + dest = open(dest_path, mode) + try: + return f(dest) + finally: + dest.close() -- cgit From a653173c75fdd3810ce75c3d5de5ea491d5d6922 Mon Sep 17 00:00:00 2001 From: Antony Messerli Date: Wed, 22 Dec 2010 11:28:08 -0600 Subject: Added networking protections readme --- plugins/xenserver/networking/README | 126 ++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 plugins/xenserver/networking/README diff --git a/plugins/xenserver/networking/README b/plugins/xenserver/networking/README new file mode 100644 index 000000000..b59de16b8 --- /dev/null +++ b/plugins/xenserver/networking/README @@ -0,0 +1,126 @@ +Multi Tenancy Networking Protections in XenServer +================================================= + +The purpose of the vif_rules script is to allow multi-tenancy on a XenServer +host. In a multi-tenant cloud environment a host machine needs to be able to +enforce network isolation amongst guest instances, at both layer two and layer +three. The rules prevent guests from taking and using unauthorized IP addresses, +sniffing other guests traffic, and prevents ARP poisoning attacks. This current +revision only supports IPv4, but will support IPv6 in the future. + +Kernel Requirements +=================== + +- physdev module +- arptables support +- ebtables support +- iptables support + +If the kernel doesn't support these, you will need to obtain the Source RPMS for +the proper version of XenServer to recompile the dom0 kernel. + +XenServer Requirements (32-bit dom0) +==================================== + +- arptables 32-bit rpm +- ebtables 32-bit rpm +- python-simplejson + +XenServer Environment Specific Notes +==================================== + +- XenServer 5.5 U1 based on the 2.6.18 kernel didn't include physdev module + support. Support for this had to be recompiled into the kernel. +- XenServer 5.6 based on the 2.6.27 kernel didn't include physdev, ebtables, or + arptables. +- XenServer 5.6 FP1 didn't include physdev, ebtables, or arptables but they do + have a Cloud Supplemental pack available to partners which swaps out the + kernels for kernels that support the networking rules. + +How it works - tl;dr +==================== + +iptables, ebtables, and arptables drop rules are applied to all forward chains +on the host. These are applied at boot time with an init script. They ensure +all forwarded packets are dropped by default. Allow rules are then applied to +the instances to ensure they have permission to talk on the internet. + +How it works - Long +=================== + +Any time an underprivileged domain or domU is started or stopped, it gets a +unique domain id (dom_id). This dom_id is utilized in a number of places, one +of which is it's assigned to the virtual interface (vif). The vifs are attached +to the bridge that is attached to the physical network. For instance, if you +had a public bridge attached to eth0 and your domain id was 5, your vif would be +vif5.0. + +The networking rules are applied to the VIF directly so they apply at the lowest +level of the networking stack. Because the VIF changes along with the domain id +on any start, stop, or reboot of the instance, the rules need to be removed and +re-added any time that occurs. + +Because the dom_id can change often, the vif_rules script is hooked into the +/etc/xensource/scripts/vif script that gets called anytime an instance is +started, or stopped, which includes pauses and resumes. + +Examples of the rules ran for the host on boot: + +iptables -P FORWARD DROP iptables -A FORWARD -m physdev --physdev-in eth0 -j +ACCEPT ebtables -P FORWARD DROP ebtables -A FORWARD -o eth0 -j ACCEPT arptables +-P FORWARD DROP arptables -A FORWARD --opcode Request --in-interface eth0 -j +ACCEPT arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + +Examples of the rules that are ran per instance state change: + +iptables -A FORWARD -m physdev --physdev-in vif1.0 -s 10.1.135.22/32 -j ACCEPT +arptables -A FORWARD --opcode Request --in-interface "vif1.0" --source-ip +10.1.135.22 -j ACCEPT arptables -A FORWARD --opcode Reply --in-interface +"vif1.0" --source-ip 10.1.135.22 --source-mac 9e:6e:cc:19:7f:fe -j ACCEPT +ebtables -A FORWARD -p 0806 -o vif1.0 --arp-ip-dst 10.1.135.22 -j ACCEPT +ebtables -A FORWARD -p 0800 -o vif1.0 --ip-dst 10.1.135.22 -j ACCEPT ebtables -I +FORWARD 1 -s ! 9e:6e:cc:19:7f:fe -i vif1.0 -j DROP + +Typically when you see a vif, it'll look like vif.. +vif2.1 for example would be domain 2 on the second interface. + +The vif_rules.py script needs to pull information about the IPs and MAC +addresses assigned to the instance. The current implementation assumes that +information is put into the VM Record into the xenstore-data key in a JSON +string. The vif_rules.py script reads out of the JSON string to determine the +IPs, and MAC addresses to protect. + +An example format is given below: + +xe vm-param-get uuid= param-name=xenstore-data xenstore-data (MRW): +vm-data/networking/4040fa7292e4: +{"label":"public","ips":[{"netmask":"255.255.255.0", "enabled":"1", +"ip":"173.200.100.10"}], "mac":"40:40:fa:72:92:e4", "gateway":"173.200.100.1", +"vm_id":"123456", "dns":["72.3.128.240","72.3.128.241"]}; +vm-data/networking/40402321c9b8: +{"label":"private","ips":[{"netmask":"255.255.224.0","enabled":"1", +"ip":"10.177.10.10"}], "routes":[{"route":"10.176.0.0","netmask":"255.248.0.0", +"gateway":"10.177.10.1"}, {"route":"10.191.192.0", "netmask":"255.255.192.0", +"gateway":"10.177.10.1"}], "mac":"40:40:23:21:c9:b8"} + +The key is used for two purposes. One, the vif_rules.py script will read from +it to apply the rules needed after parsing the JSON. The second is that because +it's put into the xenstore-data field, the xenstore will be populated with this +data on boot. This allows a guest agent the ability to read out data about the +instance and apply configurations as needed. + +Installation +============ + +- Copy host-rules into /etc/init.d/ and make sure to chmod +x host-rules. +- Run 'chkconfig host-rules on' to add the init script to start up. +- Copy vif_rules.py into /etc/xensource/scripts +- Patch /etc/xensource/scripts/vif using the supplied patch file. It may vary + for different versions of XenServer but it should be pretty self explanitory. + It calls the vif_rules.py script on domain creation and tear down. +- Run '/etc/init.d/host-rules start' to start up the host based rules. +- The instance rules will then fire on creation of the VM as long as the correct + JSON is in place. +- You can check to see if the rules are in place with: iptables --list, + arptables --list, or ebtables --list + -- cgit From 1f9f997ae342ea16d0640c3e406402950b1d2a9b Mon Sep 17 00:00:00 2001 From: Cory Wright Date: Wed, 22 Dec 2010 13:27:51 -0500 Subject: Adding myself and Antony Messerli to the Authors file --- Authors | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Authors b/Authors index fa38ef0b1..6e873acf3 100644 --- a/Authors +++ b/Authors @@ -1,9 +1,11 @@ Andy Smith Anne Gentle Anthony Young +Antony Messerli Armando Migliaccio Chris Behrens Chmouel Boudjnah +Cory Wright Dean Troyer Devin Carlen Ed Leafe -- cgit From 59c3e5bf0dda0c0c1b77307a339f3102c7179885 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 23 Dec 2010 18:09:52 -0600 Subject: Faked out handling for shared ip groups so they return something --- nova/api/openstack/ratelimiting/__init__.py | 4 ++-- nova/api/openstack/sharedipgroups.py | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 91a8b2e55..cbb4b897e 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -64,9 +64,9 @@ class RateLimitingMiddleware(wsgi.Middleware): If the request should be rate limited, return a 413 status with a Retry-After header giving the time when the request would succeed. """ - return self.limited_request(req, self.application) + return self.rate_limited_request(req, self.application) - def limited_request(self, req, application): + def rate_limited_request(self, req, application): """Rate limit the request. If the request should be rate limited, return a 413 status with a diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 75d02905c..4f4bb1016 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from webob import exc + from nova import wsgi @@ -22,19 +24,25 @@ class Controller(wsgi.Controller): """ The Shared IP Groups Controller for the Openstack API """ def index(self, req): - raise NotImplementedError + """ Returns a list of Shared IP Groups for the user """ + return dict(sharedipgroups=[]) def show(self, req, id): - raise NotImplementedError + """ Shows in-depth information on a specific Shared IP Group """ + return dict(sharedipgroup={}) def update(self, req, id): - raise NotImplementedError + """ You can't update a Shared IP Group """ + raise faults.Fault(exc.HTTPNotImplemented()) def delete(self, req, id): - raise NotImplementedError + """ Deletes a Shared IP Group """ + raise faults.Fault(exc.HTTPNotFound()) - def detail(self, req): - raise NotImplementedError + def detail(self, req, id): + """ Returns a complete list of Shared IP Groups """ + return dict(sharedipgroups=[]) def create(self, req): - raise NotImplementedError + """ Creates a new Shared IP group """ + raise faults.Fault(exc.HTTPNotFound()) -- cgit From 002bbfa7a648a1117e14713eab3ee3ee4b2b6d8e Mon Sep 17 00:00:00 2001 From: Cory Wright Date: Mon, 27 Dec 2010 12:06:36 -0500 Subject: Moving README to doc/networking.rst per recommendation from Jay Pipes --- plugins/xenserver/doc/networking.rst | 144 +++++++++++++++++++++++++++++++++++ plugins/xenserver/networking/README | 144 ----------------------------------- 2 files changed, 144 insertions(+), 144 deletions(-) create mode 100644 plugins/xenserver/doc/networking.rst delete mode 100644 plugins/xenserver/networking/README diff --git a/plugins/xenserver/doc/networking.rst b/plugins/xenserver/doc/networking.rst new file mode 100644 index 000000000..67f2d9af3 --- /dev/null +++ b/plugins/xenserver/doc/networking.rst @@ -0,0 +1,144 @@ +Multi Tenancy Networking Protections in XenServer +================================================= + +The purpose of the vif_rules script is to allow multi-tenancy on a XenServer +host. In a multi-tenant cloud environment a host machine needs to be able to +enforce network isolation amongst guest instances, at both layer two and layer +three. The rules prevent guests from taking and using unauthorized IP addresses, +sniffing other guests traffic, and prevents ARP poisoning attacks. This current +revision only supports IPv4, but will support IPv6 in the future. + +Kernel Requirements +=================== + +- physdev module +- arptables support +- ebtables support +- iptables support + +If the kernel doesn't support these, you will need to obtain the Source RPMS for +the proper version of XenServer to recompile the dom0 kernel. + +XenServer Requirements (32-bit dom0) +==================================== + +- arptables 32-bit rpm +- ebtables 32-bit rpm +- python-simplejson + +XenServer Environment Specific Notes +==================================== + +- XenServer 5.5 U1 based on the 2.6.18 kernel didn't include physdev module + support. Support for this had to be recompiled into the kernel. +- XenServer 5.6 based on the 2.6.27 kernel didn't include physdev, ebtables, or + arptables. +- XenServer 5.6 FP1 didn't include physdev, ebtables, or arptables but they do + have a Cloud Supplemental pack available to partners which swaps out the + kernels for kernels that support the networking rules. + +How it works - tl;dr +==================== + +iptables, ebtables, and arptables drop rules are applied to all forward chains +on the host. These are applied at boot time with an init script. They ensure +all forwarded packets are dropped by default. Allow rules are then applied to +the instances to ensure they have permission to talk on the internet. + +How it works - Long +=================== + +Any time an underprivileged domain or domU is started or stopped, it gets a +unique domain id (dom_id). This dom_id is utilized in a number of places, one +of which is it's assigned to the virtual interface (vif). The vifs are attached +to the bridge that is attached to the physical network. For instance, if you +had a public bridge attached to eth0 and your domain id was 5, your vif would be +vif5.0. + +The networking rules are applied to the VIF directly so they apply at the lowest +level of the networking stack. Because the VIF changes along with the domain id +on any start, stop, or reboot of the instance, the rules need to be removed and +re-added any time that occurs. + +Because the dom_id can change often, the vif_rules script is hooked into the +/etc/xensource/scripts/vif script that gets called anytime an instance is +started, or stopped, which includes pauses and resumes. + +Examples of the rules ran for the host on boot: + +iptables -P FORWARD DROP +iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT +ebtables -P FORWARD DROP +ebtables -A FORWARD -o eth0 -j ACCEPT +arptables -P FORWARD DROP +arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT +arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT + +Examples of the rules that are ran per instance state change: + +iptables -A FORWARD -m physdev --physdev-in vif1.0 -s 10.1.135.22/32 -j ACCEPT +arptables -A FORWARD --opcode Request --in-interface "vif1.0" \ + --source-ip 10.1.135.22 -j ACCEPT +arptables -A FORWARD --opcode Reply --in-interface "vif1.0" \ + --source-ip 10.1.135.22 --source-mac 9e:6e:cc:19:7f:fe -j ACCEPT +ebtables -A FORWARD -p 0806 -o vif1.0 --arp-ip-dst 10.1.135.22 -j ACCEPT +ebtables -A FORWARD -p 0800 -o vif1.0 --ip-dst 10.1.135.22 -j ACCEPT +ebtables -I FORWARD 1 -s ! 9e:6e:cc:19:7f:fe -i vif1.0 -j DROP + +Typically when you see a vif, it'll look like vif.. +vif2.1 for example would be domain 2 on the second interface. + +The vif_rules.py script needs to pull information about the IPs and MAC +addresses assigned to the instance. The current implementation assumes that +information is put into the VM Record into the xenstore-data key in a JSON +string. The vif_rules.py script reads out of the JSON string to determine the +IPs, and MAC addresses to protect. + +An example format is given below: + +# xe vm-param-get uuid= param-name=xenstore-data +xenstore-data (MRW): +vm-data/networking/4040fa7292e4: +{"label": "public", + "ips": [{"netmask":"255.255.255.0", + "enabled":"1", + "ip":"173.200.100.10"}], + "mac":"40:40:fa:72:92:e4", + "gateway":"173.200.100.1", + "vm_id":"123456", + "dns":["72.3.128.240","72.3.128.241"]}; + +vm-data/networking/40402321c9b8: +{"label":"private", + "ips":[{"netmask":"255.255.224.0", + "enabled":"1", + "ip":"10.177.10.10"}], + "routes":[{"route":"10.176.0.0", + "netmask":"255.248.0.0", + "gateway":"10.177.10.1"}, + {"route":"10.191.192.0", + "netmask":"255.255.192.0", + "gateway":"10.177.10.1"}], + "mac":"40:40:23:21:c9:b8"} + +The key is used for two purposes. One, the vif_rules.py script will read from +it to apply the rules needed after parsing the JSON. The second is that because +it's put into the xenstore-data field, the xenstore will be populated with this +data on boot. This allows a guest agent the ability to read out data about the +instance and apply configurations as needed. + +Installation +============ + +- Copy host-rules into /etc/init.d/ and make sure to chmod +x host-rules. +- Run 'chkconfig host-rules on' to add the init script to start up. +- Copy vif_rules.py into /etc/xensource/scripts +- Patch /etc/xensource/scripts/vif using the supplied patch file. It may vary + for different versions of XenServer but it should be pretty self explanatory. + It calls the vif_rules.py script on domain creation and tear down. +- Run '/etc/init.d/host-rules start' to start up the host based rules. +- The instance rules will then fire on creation of the VM as long as the correct + JSON is in place. +- You can check to see if the rules are in place with: iptables --list, + arptables --list, or ebtables --list + diff --git a/plugins/xenserver/networking/README b/plugins/xenserver/networking/README deleted file mode 100644 index 67f2d9af3..000000000 --- a/plugins/xenserver/networking/README +++ /dev/null @@ -1,144 +0,0 @@ -Multi Tenancy Networking Protections in XenServer -================================================= - -The purpose of the vif_rules script is to allow multi-tenancy on a XenServer -host. In a multi-tenant cloud environment a host machine needs to be able to -enforce network isolation amongst guest instances, at both layer two and layer -three. The rules prevent guests from taking and using unauthorized IP addresses, -sniffing other guests traffic, and prevents ARP poisoning attacks. This current -revision only supports IPv4, but will support IPv6 in the future. - -Kernel Requirements -=================== - -- physdev module -- arptables support -- ebtables support -- iptables support - -If the kernel doesn't support these, you will need to obtain the Source RPMS for -the proper version of XenServer to recompile the dom0 kernel. - -XenServer Requirements (32-bit dom0) -==================================== - -- arptables 32-bit rpm -- ebtables 32-bit rpm -- python-simplejson - -XenServer Environment Specific Notes -==================================== - -- XenServer 5.5 U1 based on the 2.6.18 kernel didn't include physdev module - support. Support for this had to be recompiled into the kernel. -- XenServer 5.6 based on the 2.6.27 kernel didn't include physdev, ebtables, or - arptables. -- XenServer 5.6 FP1 didn't include physdev, ebtables, or arptables but they do - have a Cloud Supplemental pack available to partners which swaps out the - kernels for kernels that support the networking rules. - -How it works - tl;dr -==================== - -iptables, ebtables, and arptables drop rules are applied to all forward chains -on the host. These are applied at boot time with an init script. They ensure -all forwarded packets are dropped by default. Allow rules are then applied to -the instances to ensure they have permission to talk on the internet. - -How it works - Long -=================== - -Any time an underprivileged domain or domU is started or stopped, it gets a -unique domain id (dom_id). This dom_id is utilized in a number of places, one -of which is it's assigned to the virtual interface (vif). The vifs are attached -to the bridge that is attached to the physical network. For instance, if you -had a public bridge attached to eth0 and your domain id was 5, your vif would be -vif5.0. - -The networking rules are applied to the VIF directly so they apply at the lowest -level of the networking stack. Because the VIF changes along with the domain id -on any start, stop, or reboot of the instance, the rules need to be removed and -re-added any time that occurs. - -Because the dom_id can change often, the vif_rules script is hooked into the -/etc/xensource/scripts/vif script that gets called anytime an instance is -started, or stopped, which includes pauses and resumes. - -Examples of the rules ran for the host on boot: - -iptables -P FORWARD DROP -iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT -ebtables -P FORWARD DROP -ebtables -A FORWARD -o eth0 -j ACCEPT -arptables -P FORWARD DROP -arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT -arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT - -Examples of the rules that are ran per instance state change: - -iptables -A FORWARD -m physdev --physdev-in vif1.0 -s 10.1.135.22/32 -j ACCEPT -arptables -A FORWARD --opcode Request --in-interface "vif1.0" \ - --source-ip 10.1.135.22 -j ACCEPT -arptables -A FORWARD --opcode Reply --in-interface "vif1.0" \ - --source-ip 10.1.135.22 --source-mac 9e:6e:cc:19:7f:fe -j ACCEPT -ebtables -A FORWARD -p 0806 -o vif1.0 --arp-ip-dst 10.1.135.22 -j ACCEPT -ebtables -A FORWARD -p 0800 -o vif1.0 --ip-dst 10.1.135.22 -j ACCEPT -ebtables -I FORWARD 1 -s ! 9e:6e:cc:19:7f:fe -i vif1.0 -j DROP - -Typically when you see a vif, it'll look like vif.. -vif2.1 for example would be domain 2 on the second interface. - -The vif_rules.py script needs to pull information about the IPs and MAC -addresses assigned to the instance. The current implementation assumes that -information is put into the VM Record into the xenstore-data key in a JSON -string. The vif_rules.py script reads out of the JSON string to determine the -IPs, and MAC addresses to protect. - -An example format is given below: - -# xe vm-param-get uuid= param-name=xenstore-data -xenstore-data (MRW): -vm-data/networking/4040fa7292e4: -{"label": "public", - "ips": [{"netmask":"255.255.255.0", - "enabled":"1", - "ip":"173.200.100.10"}], - "mac":"40:40:fa:72:92:e4", - "gateway":"173.200.100.1", - "vm_id":"123456", - "dns":["72.3.128.240","72.3.128.241"]}; - -vm-data/networking/40402321c9b8: -{"label":"private", - "ips":[{"netmask":"255.255.224.0", - "enabled":"1", - "ip":"10.177.10.10"}], - "routes":[{"route":"10.176.0.0", - "netmask":"255.248.0.0", - "gateway":"10.177.10.1"}, - {"route":"10.191.192.0", - "netmask":"255.255.192.0", - "gateway":"10.177.10.1"}], - "mac":"40:40:23:21:c9:b8"} - -The key is used for two purposes. One, the vif_rules.py script will read from -it to apply the rules needed after parsing the JSON. The second is that because -it's put into the xenstore-data field, the xenstore will be populated with this -data on boot. This allows a guest agent the ability to read out data about the -instance and apply configurations as needed. - -Installation -============ - -- Copy host-rules into /etc/init.d/ and make sure to chmod +x host-rules. -- Run 'chkconfig host-rules on' to add the init script to start up. -- Copy vif_rules.py into /etc/xensource/scripts -- Patch /etc/xensource/scripts/vif using the supplied patch file. It may vary - for different versions of XenServer but it should be pretty self explanatory. - It calls the vif_rules.py script on domain creation and tear down. -- Run '/etc/init.d/host-rules start' to start up the host based rules. -- The instance rules will then fire on creation of the VM as long as the correct - JSON is in place. -- You can check to see if the rules are in place with: iptables --list, - arptables --list, or ebtables --list - -- cgit From 3490fde00fd8bfb00834b1085de62d86c9c9d061 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Dec 2010 12:08:22 -0600 Subject: A few fixes --- nova/api/openstack/backup_schedules.py | 5 ++++- nova/api/openstack/servers.py | 3 ++- nova/api/openstack/sharedipgroups.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index fc70b5c6c..c484023e0 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -24,12 +24,14 @@ import nova.image.service class Controller(wsgi.Controller): + """ The backup schedule API controller for the Openstack API """ def __init__(self): pass def index(self, req, server_id): - return faults.Fault(exc.HTTPNotFound()) + """ Returns the list of backup schedules for a given instance """ + return dict(backup_schedules=[]) def create(self, req, server_id): """ No actual update method required, since the existing API allows @@ -37,4 +39,5 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) def delete(self, req, server_id, id): + """ Deletes an existing backup schedule """ return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8d60e2cab..c8851387d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -43,6 +43,7 @@ def _entity_list(entities): def _entity_detail(inst): """ Maps everything to Rackspace-like attributes for return""" power_mapping = { + None: 'build', power_state.NOSTATE: 'build', power_state.RUNNING: 'active', power_state.BLOCKED: 'active', @@ -153,7 +154,7 @@ class Controller(wsgi.Controller): try: self.compute_api.update_instance(req.environ['nova.context'], - instance['id'], + inst_dict['id'], **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 4f4bb1016..7e98b0712 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -18,6 +18,7 @@ from webob import exc from nova import wsgi +from nova.api.openstack import faults class Controller(wsgi.Controller): -- cgit From b879c746049241837af3785adc3068fbe35f199d Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Dec 2010 13:20:37 -0600 Subject: backup schedule changes --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/backup_schedules.py | 10 +++++++++- nova/api/openstack/servers.py | 6 ++++-- nova/api/openstack/sharedipgroups.py | 27 ++++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index c49399f28..1f78820c8 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -98,7 +98,7 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, member=server_members) - mapper.resource("backup_schedule", "backup_schedules", + mapper.resource("backup_schedule", "backup_schedule", controller=backup_schedules.Controller(), parent_resource=dict(member_name='server', collection_name='servers')) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index c484023e0..a8b0fbbb9 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -22,16 +22,24 @@ from nova import wsgi from nova.api.openstack import faults import nova.image.service +def _entity_inst(inst): + """ Coerces the backup schedule into proper dictionary format """ + return dict(backupSchedule=inst) class Controller(wsgi.Controller): """ The backup schedule API controller for the Openstack API """ + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'backupSchedule': []}}} + def __init__(self): pass def index(self, req, server_id): """ Returns the list of backup schedules for a given instance """ - return dict(backup_schedules=[]) + return _entity_inst({}) def create(self, req, server_id): """ No actual update method required, since the existing API allows diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c8851387d..7abf5ec53 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -153,8 +153,10 @@ class Controller(wsgi.Controller): update_dict['display_name'] = inst_dict['server']['name'] try: - self.compute_api.update_instance(req.environ['nova.context'], - inst_dict['id'], + ctxt = req.environ['nova.context'] + inst_ref = self.compute_api.get_instance(ctxt, id) + self.compute_api.update_instance(ctxt, + id, **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 7e98b0712..32ebfa45a 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -21,16 +21,37 @@ from nova import wsgi from nova.api.openstack import faults +def _entity_list(entities): + """ Coerces a list of shared IP groups into proper dictionary format """ + return dict(sharedIpGroups=entities) + + +def _entity_inst(inst): + """ Coerces a shared IP group instance into proper dictionary format """ + return dict(sharedIpGroup=inst) + + +def _entity_detail(inst): + """ Coerces a shared IP group instance into proper dictionary format with + correctly mapped attributes """ + return dict(sharedIpGroup=inst) + + class Controller(wsgi.Controller): """ The Shared IP Groups Controller for the Openstack API """ + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'sharedIpGroup': []}}} + def index(self, req): """ Returns a list of Shared IP Groups for the user """ - return dict(sharedipgroups=[]) + return _entity_list([]) def show(self, req, id): """ Shows in-depth information on a specific Shared IP Group """ - return dict(sharedipgroup={}) + return _entity_inst({}) def update(self, req, id): """ You can't update a Shared IP Group """ @@ -42,7 +63,7 @@ class Controller(wsgi.Controller): def detail(self, req, id): """ Returns a complete list of Shared IP Groups """ - return dict(sharedipgroups=[]) + return _entity_detail({}) def create(self, req): """ Creates a new Shared IP group """ -- cgit From 431c54ba76a2a85ff55658c571f68378b47ce39d Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Dec 2010 17:29:45 -0600 Subject: Renamed based on feedback from another branch --- nova/api/openstack/backup_schedules.py | 2 ++ nova/api/openstack/servers.py | 27 ++++++++++++--------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index a8b0fbbb9..9b8e605f6 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -22,10 +22,12 @@ from nova import wsgi from nova.api.openstack import faults import nova.image.service + def _entity_inst(inst): """ Coerces the backup schedule into proper dictionary format """ return dict(backupSchedule=inst) + class Controller(wsgi.Controller): """ The backup schedule API controller for the Openstack API """ diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7abf5ec53..9369f6516 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -35,13 +35,9 @@ LOG = logging.getLogger('server') LOG.setLevel(logging.DEBUG) -def _entity_list(entities): - """ Coerces a list of servers into proper dictionary format """ - return dict(servers=entities) - - -def _entity_detail(inst): - """ Maps everything to Rackspace-like attributes for return""" +def _translate_detail_keys(inst): + """ Coerces into dictionary format, mapping everything to Rackspace-like + attributes for return""" power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -67,8 +63,9 @@ def _entity_detail(inst): return dict(server=inst_dict) -def _entity_inst(inst): - """ Filters all model attributes save for id and name """ +def _translate_keys(inst): + """ Coerces into dictionary format, excluding all model attributes + save for id and name """ return dict(server=dict(id=inst['internal_id'], name=inst['display_name'])) @@ -87,29 +84,29 @@ class Controller(wsgi.Controller): def index(self, req): """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_entity_inst) + return self._items(req, entity_maker=_translte_keys) def detail(self, req): """ Returns a list of server details for a given user """ - return self._items(req, entity_maker=_entity_detail) + return self._items(req, entity_maker=_translate_detail_keys) def _items(self, req, entity_maker): """Returns a list of servers for a given user. - entity_maker - either _entity_detail or _entity_inst + entity_maker - either _translate_detail_keys or _translate_keys """ instance_list = self.compute_api.get_instances( req.environ['nova.context']) limited_list = common.limited(instance_list, req) res = [entity_maker(inst)['server'] for inst in limited_list] - return _entity_list(res) + return dict(servers=res) def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get_instance( req.environ['nova.context'], int(id)) - return _entity_detail(instance) + return _translate_detail_keys(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -138,7 +135,7 @@ class Controller(wsgi.Controller): description=env['server']['name'], key_name=key_pair['name'], key_data=key_pair['public_key']) - return _entity_inst(instances[0]) + return _translate_keys(instances[0]) def update(self, req, id): """ Updates the server name or password """ -- cgit From 31d3aed581302e73b3f155b1dd72586324433e91 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Dec 2010 17:35:59 -0600 Subject: Typo fix --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 9369f6516..d18889563 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -84,7 +84,7 @@ class Controller(wsgi.Controller): def index(self, req): """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_translte_keys) + return self._items(req, entity_maker=_translate_keys) def detail(self, req): """ Returns a list of server details for a given user """ -- cgit From eab0ce934e6296910b26c087e4268a65dc233a55 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Tue, 28 Dec 2010 01:37:04 +0000 Subject: Bug #694880: nova-compute now depends upon Cheetah even when not using libvirt Only import Cheetah when needed, as we do already with libvirt and libxml2. This ensures that users of other virt backends don't need Cheetah to run nova-compute. --- nova/tests/test_virt.py | 1 + nova/virt/libvirt_conn.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 1c155abe4..4aa489d08 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -33,6 +33,7 @@ flags.DECLARE('instances_path', 'nova.compute.manager') class LibvirtConnTestCase(test.TestCase): def setUp(self): super(LibvirtConnTestCase, self).setUp() + libvirt_conn._late_load_cheetah() self.flags(fake_call=True) self.manager = manager.AuthManager() self.user = self.manager.create_user('fake', 'fake', 'fake', diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 67b9fc47e..8b67b937f 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -58,10 +58,9 @@ from nova.compute import instance_types from nova.compute import power_state from nova.virt import images -from Cheetah.Template import Template - libvirt = None libxml2 = None +Template = None FLAGS = flags.FLAGS @@ -88,14 +87,23 @@ flags.DEFINE_bool('allow_project_net_traffic', def get_connection(read_only): # These are loaded late so that there's no need to install these # libraries when not using libvirt. + # Cheetah is separate because the unit tests want to load Cheetah, + # but not libvirt. global libvirt global libxml2 if libvirt is None: libvirt = __import__('libvirt') if libxml2 is None: libxml2 = __import__('libxml2') + _late_load_cheetah() return LibvirtConnection(read_only) +def _late_load_cheetah(): + global Template + if Template is None: + t = __import__('Cheetah.Template', globals(), locals(), ['Template'], + -1) + Template = t.Template def _get_net_and_mask(cidr): net = IPy.IP(cidr) -- cgit From 8d03539e15bf4ac168af550e27a81353a896df54 Mon Sep 17 00:00:00 2001 From: Cory Wright Date: Tue, 28 Dec 2010 16:35:56 -0500 Subject: Update .mailmap with both email addresses for Ant and myself --- .mailmap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mailmap b/.mailmap index 9ab7db743..9e7fb1ec0 100644 --- a/.mailmap +++ b/.mailmap @@ -27,3 +27,5 @@ + + -- cgit From 902df6eb4968743dd451e54cde27ce88fc83ddaa Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Dec 2010 15:48:48 -0600 Subject: Whoops --- nova/api/openstack/backup_schedules.py | 4 ++-- nova/api/openstack/sharedipgroups.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index 9b8e605f6..fcc07bdd3 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -23,7 +23,7 @@ from nova.api.openstack import faults import nova.image.service -def _entity_inst(inst): +def _translate_keys(inst): """ Coerces the backup schedule into proper dictionary format """ return dict(backupSchedule=inst) @@ -41,7 +41,7 @@ class Controller(wsgi.Controller): def index(self, req, server_id): """ Returns the list of backup schedules for a given instance """ - return _entity_inst({}) + return _translate_keys({}) def create(self, req, server_id): """ No actual update method required, since the existing API allows diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 32ebfa45a..845f5bead 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -21,17 +21,12 @@ from nova import wsgi from nova.api.openstack import faults -def _entity_list(entities): - """ Coerces a list of shared IP groups into proper dictionary format """ - return dict(sharedIpGroups=entities) - - -def _entity_inst(inst): +def _translate_keys(inst): """ Coerces a shared IP group instance into proper dictionary format """ return dict(sharedIpGroup=inst) -def _entity_detail(inst): +def _translate_detail_keys(inst): """ Coerces a shared IP group instance into proper dictionary format with correctly mapped attributes """ return dict(sharedIpGroup=inst) @@ -47,11 +42,11 @@ class Controller(wsgi.Controller): def index(self, req): """ Returns a list of Shared IP Groups for the user """ - return _entity_list([]) + return dict(sharedIpGroups=[]) def show(self, req, id): """ Shows in-depth information on a specific Shared IP Group """ - return _entity_inst({}) + return _translate_keys({}) def update(self, req, id): """ You can't update a Shared IP Group """ @@ -63,7 +58,7 @@ class Controller(wsgi.Controller): def detail(self, req, id): """ Returns a complete list of Shared IP Groups """ - return _entity_detail({}) + return _translate_detail_keys({}) def create(self, req): """ Creates a new Shared IP group """ -- cgit From 8ee15b6e93b5666b4645f2cefef357b0af3d26d0 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 28 Dec 2010 23:25:50 +0100 Subject: Address bug #695157 by using a blank request class and setting an empty request path. --- nova/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/wsgi.py b/nova/wsgi.py index c7ee9ed14..b5d6b96c1 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -270,7 +270,7 @@ class Serializer(object): needed to serialize a dictionary to that type. """ self.metadata = metadata or {} - req = webob.Request(environ) + req = webob.Request.blank('', environ) suffix = req.path_info.split('.')[-1].lower() if suffix == 'json': self.handler = self._to_json -- cgit From 99a228a8ef3ee2760774fbafd136f137bd578dba Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Dec 2010 17:34:51 -0600 Subject: removed superfluous line --- nova/api/openstack/servers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 2232d24b2..845183258 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -152,7 +152,6 @@ class Controller(wsgi.Controller): try: ctxt = req.environ['nova.context'] - inst_ref = self.compute_api.get_instance(ctxt, id) self.compute_api.update_instance(ctxt, id, **update_dict) -- cgit From 380b28f89481c52dbcda0b54fd7409b6bc72bb56 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Wed, 29 Dec 2010 01:58:04 +0000 Subject: Fix pep8 violations. --- nova/virt/libvirt_conn.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 8b67b937f..e9c6c2713 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -98,6 +98,7 @@ def get_connection(read_only): _late_load_cheetah() return LibvirtConnection(read_only) + def _late_load_cheetah(): global Template if Template is None: @@ -105,6 +106,7 @@ def _late_load_cheetah(): -1) Template = t.Template + def _get_net_and_mask(cidr): net = IPy.IP(cidr) return str(net.net()), str(net.netmask()) -- cgit From 34f5bed4d9c99af58c83b82b499f898c270124a8 Mon Sep 17 00:00:00 2001 From: Ryan Lucio Date: Wed, 29 Dec 2010 11:24:42 -0800 Subject: Re-added flag definition for injected_network_template. Tested & verified fix in the same env as the original bug. --- nova/virt/libvirt_conn.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index f6a218fa4..39215c4e1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -68,6 +68,9 @@ FLAGS = flags.FLAGS flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image') flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image') flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image') +flags.DEFINE_string('injected_network_template', + utils.abspath('virt/interfaces.template'), + 'Template file for injected network') flags.DEFINE_string('libvirt_xml_template', utils.abspath('virt/libvirt.xml.template'), 'Libvirt XML Template') -- cgit