Merging trunk

This commit is contained in:
Rick Harris 2010-12-30 11:26:21 -06:00
commit 2a08bed775
19 changed files with 479 additions and 32 deletions

View File

@ -27,4 +27,6 @@
<vishvananda@gmail.com> <root@ubuntu>
<sleepsonthefloor@gmail.com> <root@tonbuntu>
<rlane@wikimedia.org> <laner@controller>
<rick.harris@rackspace.com> <rconradharris@gmail.com>
<rconradharris@gmail.com> <rick.harris@rackspace.com>
<corywright@gmail.com> <cory.wright@rackspace.com>
<ant@openstack.org> <amesserl@rackspace.com>

View File

@ -1,9 +1,11 @@
Andy Smith <code@term.ie>
Anne Gentle <anne@openstack.org>
Anthony Young <sleepsonthefloor@gmail.com>
Antony Messerli <ant@openstack.org>
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
Chris Behrens <cbehrens@codestud.com>
Chmouel Boudjnah <chmouel@chmouel.com>
Cory Wright <corywright@gmail.com>
David Pravec <David.Pravec@danix.org>
Dean Troyer <dtroyer@gmail.com>
Devin Carlen <devin.carlen@gmail.com>

View File

@ -100,7 +100,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'))

View File

@ -23,13 +23,25 @@ from nova.api.openstack import faults
import nova.image.service
def _translate_keys(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):
return faults.Fault(exc.HTTPNotFound())
""" Returns the list of backup schedules for a given instance """
return _translate_keys({})
def create(self, req, server_id):
""" No actual update method required, since the existing API allows
@ -37,4 +49,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())

View File

@ -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

View File

@ -35,14 +35,11 @@ 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',
power_state.RUNNING: 'active',
power_state.BLOCKED: 'active',
@ -67,8 +64,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 +85,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=_translate_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 +136,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 """
@ -153,8 +151,9 @@ class Controller(wsgi.Controller):
update_dict['display_name'] = inst_dict['server']['name']
try:
self.compute_api.update_instance(req.environ['nova.context'],
instance['id'],
ctxt = req.environ['nova.context']
self.compute_api.update_instance(ctxt,
id,
**update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())

View File

@ -15,26 +15,51 @@
# License for the specific language governing permissions and limitations
# under the License.
from webob import exc
from nova import wsgi
from nova.api.openstack import faults
def _translate_keys(inst):
""" Coerces a shared IP group instance into proper dictionary format """
return dict(sharedIpGroup=inst)
def _translate_detail_keys(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):
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 _translate_keys({})
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 _translate_detail_keys({})
def create(self, req):
raise NotImplementedError
""" Creates a new Shared IP group """
raise faults.Fault(exc.HTTPNotFound())

View File

@ -33,6 +33,7 @@ from nova.scheduler import driver
FLAGS = flags.FLAGS
flags.DECLARE('max_cores', 'nova.scheduler.simple')
flags.DECLARE('stub_network', 'nova.compute.manager')
class TestDriver(driver.Scheduler):

View File

@ -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',

View File

@ -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
@ -69,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')
@ -88,15 +90,26 @@ 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)
return str(net.net()), str(net.netmask())

View File

@ -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

View File

@ -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<domain id>.<network bridge>.
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=<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

View File

@ -0,0 +1,106 @@
#!/bin/bash
#
# host-rules Start/Stop the networking host rules
#
# chkconfig: 2345 85 15
# description: Networking Host Rules for Multi Tenancy Protections
# 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.
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
}
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
RETVAL=$?
;;
stop)
stop
RETVAL=$?
;;
restart)
stop
start
RETVAL=$?
;;
*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
;;
esac
exit $RETVAL

View File

@ -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
+

View File

@ -0,0 +1,119 @@
#!/usr/bin/env python
# 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 on
XenServer hosts.
"""
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
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)
devnull.close()
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:
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)