view srxproxy.py @ 41:386a6117d08e default tip

Routing instance for nat on srx
author zynzel <zynzel+bitbucket@spof.pl>
date Tue, 19 Feb 2013 09:29:47 +0100
parents 8062c5f64c33
children
line wrap: on
line source

#!/usr/bin/env python

import eventlet
eventlet.monkey_patch()

import datetime
import sys

from nova import rpc
from nova.rpc import amqp
from nova import log
from nova import flags
from nova import utils
from nova import exception
from nova.network.manager import NetworkManager

from ncclient import manager, operations


DELAY = 10
FLAGS = flags.FLAGS
LOG = log
RULESET_XML = """\
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
   <configuration><security><nat><static><rule-set>
        <name>{name}</name>
        {rules}
    </rule-set></static></nat></security></configuration>
</config>"""
RULE_XML = """\
<rule>
    <name>{name}</name>
    <static-nat-rule-match>
        <destination-address>
            <dst-addr>{destination}/32</dst-addr>
        </destination-address>
    </static-nat-rule-match>
    <then>
        <static-nat>
            <prefix>
                <addr-prefix>{prefix}/32</addr-prefix>
                <routing-instance>cloud</routing-instance>
            </prefix>
        </static-nat>
    </then>
</rule>"""
DELETE_XML = """\
<rule operation="delete">
    <name>{name}</name>
</rule>"""


def ensure_floating_forwards(ips):
    rules_xml = [
        RULE_XML.format(
            name=floating_ip.replace('.', '_'),
            destination=floating_ip,
            prefix=fixed_ip,
        )
        for context, floating_ip, fixed_ip, host, instance_id in ips
    ]
    apply_ruleset(rules_xml, default_operation="merge")


def remove_floating_forwards(ips):
    rules_xml = [
        DELETE_XML.format(name=address.replace('.', '_'))
        for context, address, instance_id in ips
    ]
    apply_ruleset(rules_xml, default_operation="none")


def apply_ruleset(rules_xml, default_operation):
    netconf_retries = FLAGS.netconf_retries
    netconf_ip = FLAGS.netconf_server_ip
    netconf_port = FLAGS.netconf_server_port
    netconf_user = FLAGS.netconf_server_user
    netconf_password = FLAGS.netconf_server_password
    netconf_ruleset = FLAGS.netconf_rule_set
    LOG.debug('Connecting to %s:%s', netconf_ip, netconf_port)
    for retry in xrange(netconf_retries):
        try:
            m = manager.connect_ssh(
                netconf_ip,
                port=netconf_port,
                unknown_host_cb=lambda host, fingerprint: True,
                username=netconf_user,
                password=netconf_password,
                allow_agent=False,
                look_for_keys=False,
            )
        except Exception as e:
            LOG.warning("Failed to connect %s:%s (%s)", netconf_ip, netconf_port, e)
        else:
            break
        eventlet.sleep(2**retry + 1)
    else:
        raise e
    with m as m:
        assert ':url' in m.server_capabilities
        LOG.debug('Starting private edit')
        for retry in xrange(netconf_retries):
            try:
                m.private()
            except operations.rpc.RPCError as e:
                if (e._message ==
                    '\nuncommitted changes will be discarded on exit\n'):
                    break
            LOG.debug('Failed to start private edit, try %d', retry)
            m.privateend()
            eventlet.sleep(2**retry + 1)
        else:
            LOG.debug('Failed to start private edit %r' % e)
            raise Exception("Failed to start private edit")
        ruleset_xml = RULESET_XML.format(
            name=netconf_ruleset,
            rules=''.join(rules_xml),
        )
        LOG.debug('Editing config')
        m.edit_config(
            target="candidate",
            config=ruleset_xml,
            default_operation=default_operation,
        )
        LOG.debug('Commiting')
        for retry in xrange(netconf_retries):
            try:
                reply = m.commit(confirmed=False)
            except operations.rpc.RPCError as e:
                pass
            else:
                for child in reply._root.getchildren():
                    error = child.find(
                        '{urn:ietf:params:xml:ns:netconf:base:1.0}rpc-error',
                    )
                    if error:
                        text = error.find(
                            '{urn:ietf:params:xml:ns:netconf:base:1.0}error-message',
                        )
                        LOG.warning('Commit error (%s)', text)
                        break
                else:
                    LOG.debug('Commited')
                    break
            LOG.warning('Failed to commit, try %d', retry)
            eventlet.sleep(2**retry + 1)
        else:
            LOG.error('Failed configuring network device %r.', netconf_ip)
            raise Exception("Failed configuring network device")


def associate_floating_ips(ips):
    if not ips:
        return
    ips = [
        (context, floating_ip, fixed_ip, host, instance_id)
        for fixed_ip, (context, floating_ip, host, instance_id) in {
            fixed_ip: (context, floating_ip, host, instance_id)
            for context, floating_ip, fixed_ip, host, instance_id in ips
        }.iteritems()
    ]
    try:
        # Change the device configuration
        LOG.info("Changing device configuration for %r", ips)
        ensure_floating_forwards(ips)
    except Exception as e:
        LOG.exception(e)
        return
    nm = NetworkManager()
    for context, floating_ip, fixed_ip, host, instance_id in ips:
        # Write the changes to the database
        LOG.info("Updating database for %s", fixed_ip)
        try:
            instance_ids = {instance_id}
            floating_ip_ref = nm.db.floating_ip_get_by_address(
                context,
                floating_ip,
            )
            fixed_ip_id = floating_ip_ref['fixed_ip_id']
            if fixed_ip_id:
                old_fixed_ip_ref = nm.db.fixed_ip_get(context, fixed_ip_id)
                if old_fixed_ip_ref and old_fixed_ip_ref['instance_id']:
                    instance_ids.add(old_fixed_ip_ref['instance_id'])
            nm.db.floating_ip_fixed_ip_associate(
                context,
                floating_ip,
                fixed_ip,
                host,
            )
            for id_ in instance_ids:
                instance = nm.db.instance_get(context, id_)
                nm.get_instance_nw_info(
                    context.elevated(),
                    instance['id'],
                    instance['uuid'],
                    instance['instance_type_id'],
                    instance['host'],
                )
        except Exception as e:
            LOG.exception(e)


def disassociate_floating_ips(ips):
    if not ips:
        return
    ips = [
        (context, address, instance_id)
        for address, (context, instance_id) in {
            address: (context, instance_id)
            for context, address, instance_id in ips
        }.iteritems()
    ]
    nm = NetworkManager()
    good_ips = []
    for context, address, instance_id in ips:
        floating_ip_ref = nm.db.floating_ip_get_by_address(context, address)
        if floating_ip_ref['fixed_ip_id']:
            good_ips.append((context, address, instance_id))
        else:
            LOG.warning("Address %s already removed!" % address)
    if not good_ips:
        return
    try:
        # Change the device configuration
        LOG.info("Changing device configuration for %r", ips)
        remove_floating_forwards(good_ips)
    except Exception as e:
        LOG.exception(e)
        return
    for context, address, instance_id in good_ips:
        # Write the changes to the database
        LOG.info("Updating database for %s", address)
        try:
            nm.db.floating_ip_disassociate(context, address)
            try:
                instance = nm.db.instance_get(context, instance_id)
                nm.get_instance_nw_info(
                    context.elevated(),
                    instance['id'],
                    instance['uuid'],
                    instance['instance_type_id'],
                    instance['host'],
                )
            except exception.InstanceNotFound:
                pass
        except Exception as e:
            LOG.exception(e)


def on_message(data):
    method = data.get('method')
    kwargs = data.get('args', {})
    context = amqp.unpack_context(data)
    if method == 'srx_associate_floating_ip':
        assoc_acc.add((
            context,
            kwargs.get('floating_ip'),
            kwargs.get('fixed_ip'),
            kwargs.get('host'),
            kwargs.get('instance_id'),
        ))
    elif method == 'srx_disassociate_floating_ip':
        dis_acc.add((
            context,
            kwargs.get('address'),
            kwargs.get('instance_id'),
        ))


class Accumulator(object):
    """Accumulates entries until the oldest one is older than timeout."""

    def __init__(self, timeout):
        self.cache = []
        self.oldest = None
        self.timeout = datetime.timedelta(seconds=timeout)

    def add(self, item):
        if not self.cache:
            self.oldest = datetime.datetime.now()
        self.cache.append(item)

    def purge(self):
        content = self.cache
        self.cache = []
        self.oldest = None
        return content

    def check(self):
        if self.oldest and self.oldest + self.timeout < datetime.datetime.now():
            return self.purge()
        return []

    def delay(self):
        if not self.oldest:
            return 0
        return (datetime.datetime.now() - self.oldest).seconds



if __name__ == '__main__':
    utils.default_flagfile()
    FLAGS(sys.argv)
    log.setup()
    assoc_acc = Accumulator(DELAY)
    dis_acc = Accumulator(DELAY)
    for retry in xrange(FLAGS.netconf_retries):
        try:
            connection = rpc.create_connection()
        except Exception as e:
            log.warning("Can't connect, retry %d", retry + 1)
            eventlet.sleep(2**retry + 1)
        else:
            break
    else:
        log.error("Failed to connect.")
        raise e
    connection.declare_topic_consumer('srxproxy', on_message)
    connection.declare_topic_consumer('srxproxy.%s' % flags.FLAGS.host, on_message)
    connection.consume_in_thread()
    while True:
        associate_floating_ips(assoc_acc.check())
        disassociate_floating_ips(dis_acc.check())
        delay = min(DELAY - assoc_acc.delay(), DELAY - dis_acc.delay())
        eventlet.sleep(delay)