#!/usr/bin/python2.7
"""
Rewrite existing ethenet interface into bridge setup.
"""
# Copyright 2014 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

import os
import re
import sys
from optparse import OptionParser, OptionGroup, SUPPRESS_HELP
from univention.config_registry import ConfigRegistry
from univention.config_registry.frontend import ucr_update
from univention.config_registry.interfaces import Interfaces, RE_IFACE
import subprocess
import logging


class NameError(Exception):
	pass


class Bridging(object):
	BRIDGE = 'br0'
	ETHERNET = 'eth0'

	RE_ROUTE = re.compile(r'default via [0-9./]{9,18} dev ([^ ]{1,15})\b.*')

	def __init__(self):
		self.ucr = self.load_ucr()
		self.changes = {}
		self.opt = self.bridge = self.ethernet = None

	def load_ucr(self):
		ucr = ConfigRegistry()
		ucr.load()
		return ucr

	def main(self):
		self.parse_args()
		try:
			self.transfer(self.ethernet, self.bridge)
			self.setup_ethernet()
			self.setup_bridge()
			self.setup_primary()
		except NameError as ex:
			print >> sys.stderr, ex
			sys.exit(1)

		if self.opt.dry_run:
			for key, value in sorted(self.changes.iteritems()):
				print '%s: %r' % (key, value)
		else:
			ucr_update(self.ucr, self.changes)

	def parse_args(self):
		parser = self.build_parser()
		self.opt, args = parser.parse_args()

		if self.opt.verbose:
			logging.getLogger().setLevel(logging.DEBUG)

		self.bridge = self.get_bridge(args)
		try:
			self.valid_bridge(self.bridge)
		except NameError as ex:
			parser.error(ex)
		logging.info('Using bridge "%s"', self.bridge)

		self.ethernet = self.get_ethernet(args)
		try:
			self.valid_eth(self.ethernet)
		except NameError as ex:
			parser.error(ex)
		logging.info('Using ethernet "%s"', self.ethernet)

		if args:
			parser.error('Additional arguments: %r' % (args,))

	def build_parser(self):
		usage = '%prog [bridge-interface [ethernet-interface]]'
		description = sys.modules[__name__].__doc__
		parser = OptionParser(usage=usage, description=description)
		parser.add_option(
			'--options', '-o',
			dest='options', action='store_true',
			help='Transfer additional options')
		parser.add_option(
			'--verbose', '-v',
			dest='verbose', action='store_true',
			help='Increase verbosity')

		debug = OptionGroup(parser, "Debugging options")
		debug.add_option(
			'--dry-run', '-n',
			dest='dry_run', action='store_true',
			help='Do not apply changes')
		debug.add_option(
			'--sys-root',
			dest='sys_root', action='store', default='/sys/class/net',
			help='Overwrite %default hierarchy')
		parser.add_option_group(debug)
		return parser

	def get_bridge(self, args):
		try:
			return args.pop(0)
		except IndexError:
			return self.BRIDGE

	def get_ethernet(self, args):
		try:
			return args.pop(0)
		except IndexError:
			pass

		ethernet = self.ucr.get('uvmm/kvm/bridge/interface')
		logging.debug('uvmm/kvm/bridge/interface="%s"', ethernet)
		if ethernet:
			return ethernet

		proc = subprocess.Popen(('ip', 'route', 'list', '0.0.0.0/0'), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = proc.communicate()
		logging.debug('ip route list: "%s"', stdout)
		if proc.returncode:
			raise NameError('Failed to determine default route: %s' % (stderr,))
		if stdout:
			match = self.RE_ROUTE.match(stdout)
			if match:
				return match.group(1)

		ethernet = self.ucr.get('interfaces/primary')
		logging.debug('interfaces/primary="%s"', ethernet)
		if ethernet:
			return ethernet

		return self.INTERFACE

	def valid_name(self, name):
		if not all((
			name != '.',
			name != '..',
			' ' not in name,
			'\t' not in name,
			'\n' not in name,
			'\r' not in name,
			'\f' not in name,
			1 <= len(name) <= 15,
		)):
			raise NameError('Invalid name "%s"' % (name,))

	def valid_bridge(self, name):
		self.valid_name(name)
		if self.net_path_exists(name):
			raise NameError('Bridge interface "%s" already exist' % (name,))

	def valid_eth(self, name):
		self.valid_name(name)
		if not self.net_path_exists(name):
			raise NameError('Interface "%s" does not exist' % (name,))
		if not all((
			not self.net_path_exists(name, 'bridge'),
			not self.net_path_exists(name, 'brport'),
		)):
			raise NameError('Interface "%s" is already part of a bridge' % (name,))

	def net_path_exists(self, *args):
		return os.path.exists(os.path.join(self.opt.sys_root, *args))

	def transfer(self, src, dst):
		RE = re.compile(r'%s([.:]\d+)?$' % (re.escape(src),))
		VRD = 'vlan-raw-device '
		vlans = {}
		for key, value in self.ucr.items():
			if not value:
				continue
			match = RE_IFACE.match(key)
			if not match:
				continue
			iface, subkey, ipv6_name = match.groups()
			if iface == dst:
				raise NameError('Interface "%s" already configured' % (iface,))

			option = subkey.startswith('options/')
			vlan = option and value.startswith(VRD)
			if vlan:
				match = RE.match(value[len(VRD):])
				if match:
					value = VRD + dst + (match.group(1) or '')
					self.changes[key] = value

			match = RE.match(iface)
			if not match:
				continue
			if match.group(1):  # VLAN
				vdst = dst + match.group(1)
				vlans.setdefault(vdst, False)
			else:
				vdst = dst

			if option and value.startswith('bridge_'):
				raise NameError('Interface "%s" contains bridge option: %s' % (iface, value))

			if subkey == 'order':
				try:
					value = str(int(value) + 1)
				except ValueError:
					value = '99'
			self.changes['interfaces/%s/%s' % (vdst, subkey)] = value
			if subkey == 'order':
				continue
			if vlan:
				vlans[vdst] = True

			if self.opt.options or not option:
				self.changes[key] = None

		for vdst, val in vlans.iteritems():
			if not val:
				self.add_option(vdst, VRD + dst)

	def setup_ethernet(self):
		prefix = 'interfaces/' + self.ethernet
		self.changes.update({
			prefix + '/start': self.ucr.get(prefix + '/start', 'true'),
			prefix + '/type': 'manual',
		})

	def setup_bridge(self):
		for value in [
			'bridge_fd 0',
			'bridge_ports ' + self.ethernet,
		]:
			self.add_option(self.bridge, value)

	def setup_primary(self):
		if self.ucr.get('interfaces/primary') == self.ethernet:
			self.changes['interfaces/primary'] = self.bridge

	def add_option(self, iface, value):
		for option in xrange(1000):
			key = 'interfaces/%s/options/%d' % (iface, option)
			val = self.changes.get(key)
			if not val:
				self.changes[key] = value
			elif val != value:
				continue
			return


if __name__ == '__main__':
	logging.basicConfig(level=logging.WARN, stream=sys.stderr)
	Bridging().main()
