summaryrefslogtreecommitdiffstats
path: root/plugins/xenserver
diff options
context:
space:
mode:
authorCory Wright <cory.wright@rackspace.com>2010-12-22 13:18:03 -0500
committerCory Wright <cory.wright@rackspace.com>2010-12-22 13:18:03 -0500
commit3d810cf7c6eb102dd38bcddf707d4b6b828bba1d (patch)
treedb4a8441732a10672ef3758295e7333bb1f65cae /plugins/xenserver
parentba7afd00a4ea826fc12113d98de391aace1fd2d2 (diff)
parenta653173c75fdd3810ce75c3d5de5ea491d5d6922 (diff)
merge antonymesserli's changes, fixed some formatting, and added copyright notice
Diffstat (limited to 'plugins/xenserver')
-rw-r--r--plugins/xenserver/networking/README144
-rwxr-xr-xplugins/xenserver/networking/etc/init.d/host-rules15
-rw-r--r--plugins/xenserver/xenapi/README6
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/objectstore231
-rwxr-xr-xplugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py216
5 files changed, 612 insertions, 0 deletions
diff --git a/plugins/xenserver/networking/README b/plugins/xenserver/networking/README
new file mode 100644
index 000000000..67f2d9af3
--- /dev/null
+++ b/plugins/xenserver/networking/README
@@ -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
+
diff --git a/plugins/xenserver/networking/etc/init.d/host-rules b/plugins/xenserver/networking/etc/init.d/host-rules
index 385c59629..798da9552 100755
--- a/plugins/xenserver/networking/etc/init.d/host-rules
+++ b/plugins/xenserver/networking/etc/init.d/host-rules
@@ -5,6 +5,21 @@
# 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
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()