├── kiwi ├── __init__.py ├── tests │ ├── __init__.py │ └── test_iptables.py ├── defaults.py ├── utils.py ├── exc.py ├── servicewatcher.py ├── interface.py ├── main.py ├── firewall.py ├── addresswatcher.py ├── iptables.py └── manager.py ├── requirements.txt ├── .gitignore ├── deprecated.png ├── .gitreview ├── .travis.yml ├── Dockerfile ├── setup.py ├── README.md └── LICENSE.txt /kiwi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kiwi/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | uuid 3 | netaddr 4 | six 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .*.sw? 3 | build/ 4 | dist/ 5 | kiwi.egg-info/ 6 | -------------------------------------------------------------------------------- /deprecated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larsks/kiwi/HEAD/deprecated.png -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.gerrithub.io 3 | port=29418 4 | project=larsks/kiwi.git 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | - "2.7" 5 | install: 6 | - "pip install ." 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /kiwi/defaults.py: -------------------------------------------------------------------------------- 1 | etcd_endpoint = 'http://localhost:4001' 2 | kube_endpoint = 'http://localhost:8080' 3 | interface = 'eth0' 4 | fwchain = 'KUBE-PUBLIC' 5 | fwmark = 1 6 | etcd_prefix = '/kiwi' 7 | refresh_interval = 10 8 | reconnect_interval = 5 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora 2 | MAINTAINER Lars Kellogg-Stedman 3 | 4 | RUN yum -y install \ 5 | python-netaddr \ 6 | python-requests \ 7 | python-setuptools \ 8 | python-uuid \ 9 | iproute \ 10 | ; yum clean all 11 | 12 | COPY .git/refs/heads/master /commit 13 | COPY . /src 14 | RUN cd /src; python setup.py install 15 | 16 | ENTRYPOINT ["/usr/bin/kiwi"] 17 | CMD ["--help"] 18 | 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('requirements.txt') as fd: 4 | setup(name='kiwi', 5 | version='1', 6 | packages=find_packages(), 7 | install_requires=fd.readlines(), 8 | entry_points={ 9 | 'console_scripts': [ 10 | 'kiwi = kiwi.main:main', 11 | ], 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /kiwi/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def iter_lines(fd, chunk_size=1024): 5 | '''Iterates over the content of a file-like object line-by-line.''' 6 | 7 | pending = None 8 | 9 | while True: 10 | chunk = os.read(fd.fileno(), chunk_size) 11 | if not chunk: 12 | break 13 | 14 | if pending is not None: 15 | chunk = pending + chunk 16 | pending = None 17 | 18 | lines = chunk.splitlines() 19 | 20 | if lines and lines[-1]: 21 | pending = lines.pop() 22 | 23 | for line in lines: 24 | yield line 25 | 26 | if pending: 27 | yield(pending) 28 | -------------------------------------------------------------------------------- /kiwi/exc.py: -------------------------------------------------------------------------------- 1 | class KiwiError (Exception): 2 | def __init__(self, message=None, reason=None, returncode=None, 3 | stdout=None, stderr=None, *args, **kwargs): 4 | self.reason = reason 5 | self.stdout = stdout 6 | self.stderr = stderr 7 | self.returncode = returncode 8 | super(KiwiError, self).__init__(message) 9 | 10 | 11 | class InterfaceDriverError (KiwiError): 12 | pass 13 | 14 | 15 | class FirewallDriverError (KiwiError): 16 | pass 17 | 18 | 19 | class UnknownAddressError (KiwiError): 20 | pass 21 | 22 | 23 | class UnclaimedAddressError(KiwiError): 24 | pass 25 | 26 | 27 | class ClaimFailedError (KiwiError): 28 | pass 29 | 30 | 31 | class RefreshFailedError (KiwiError): 32 | pass 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Deprecated!](deprecated.png) 2 | 3 | THIS CODE WAS WRITTEN FOR AN EARLIER VERSION OF KUBERNETES AND WILL 4 | NOT FUNCTION WITH CURRENT RELEASES. 5 | 6 | # Kiwi, the Kubernetes IP Manager 7 | 8 | This is a simple service for managing the assignment of IP addresses 9 | to network interfaces and the associated firewall rules for Kubernetes 10 | services. 11 | 12 | The `kiwi` service will listen for notifications from the 13 | Kubernetes API regarding new or deleted services, and for those that 14 | contain a `publicIPs` element the service will: 15 | 16 | - Associate the public ip with a network interface, if it has not 17 | already been assigned, 18 | - Create `mangle` table rules to mark inbound traffic 19 | 20 | Kiwi uses etcd to coordinate assignments between multiple systems. If 21 | Kiwi stops running on one system, any active ip addresses will be 22 | assigned on the remaining systems. 23 | 24 | ## Using Kiwi 25 | 26 | The easiest way to use Kiwi is to use the docker image: 27 | 28 | docker run --privileged --net=host larsks/kiwi --interface br0 --verbose 29 | 30 | Kiwi needs `--net=host` and `--privileged` because it will be 31 | modifying your host iptables and network interface configuration. 32 | 33 | ## Example 34 | 35 | Assume that you have a Kubernetes service definition like this: 36 | 37 | kind: Service 38 | id: web 39 | apiVersion: v1beta1 40 | port: 8080 41 | selector: 42 | name: web 43 | containerPort: 80 44 | publicIps: 45 | - 192.168.1.41 46 | - 172.16.1.41 47 | 48 | If you run `kiwi` like this: 49 | 50 | kiwi --interface em1 -r 192.168.1.0/24 51 | 52 | And then create the Kubernetes services: 53 | 54 | kubectl create -f web-service.yaml 55 | 56 | Then `kiwi` will: 57 | 58 | - Add address 192.168.1.42/32 to device `em1`: 59 | 60 | # ip addr show em1 | grep 192.168.1.41 61 | inet 192.168.1.41/32 scope global em1:kube 62 | 63 | - Add the following rule to the `mangle` `KUBE-PUBLIC` 64 | table: 65 | 66 | -A KUBE-PUBLIC -d 192.168.1.41/32 -p tcp -m tcp --dport 8080 -m comment --comment web -j MARK --set-mark 1 67 | 68 | - Kiwi will ignore `172.16.1.41` because it does not match any valid 69 | CIDR range. 70 | 71 | These changes will be removed if you delete the service. 72 | 73 | When `kiwi` exits, it will remove any addresses and firewall rules it 74 | created while it was running. 75 | 76 | ## Technical details 77 | 78 | Kiwi works by listening to the Kubernetes API at 79 | `/api/v1beta1/watch/services`. As new services appear, Kiwi iterates 80 | over the list of ip addresses and attempts to create corresponding 81 | keys under the etcd prefix `/kiwi/publicips`. 82 | 83 | If it is able to successfully create an entry, the local Kiwi agent 84 | has "claimed" that address and will provision it locally. 85 | 86 | Addresses are set with a TTL (10 seconds by default). The local kiwi 87 | agent will heartbeat on that address entry while it is running. If 88 | the local agent stops running, the `/kiwi/publicips/x.x.x.x` entry 89 | will eventually expire, at which point another agent will attempt to 90 | claim. 91 | 92 | -------------------------------------------------------------------------------- /kiwi/servicewatcher.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import requests 4 | import time 5 | from itertools import izip 6 | 7 | import defaults 8 | from utils import iter_lines 9 | 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | def iter_request_events(fd): 15 | '''Iterate over the events from a Kubernetes event stream.''' 16 | 17 | lines = iter_lines(fd) 18 | for expected_len, data, marker in izip(lines, lines, lines): 19 | expected_len = int(expected_len, base=16) 20 | actual_len = len(data) 21 | if expected_len != actual_len + 1: 22 | raise ValueError('data length mismatch (expected %d, have %d)', 23 | expected_len, 24 | actual_len) 25 | 26 | yield json.loads(data) 27 | 28 | 29 | def iter_events(url, interval=1): 30 | '''Generates an infinite string of Kubernetes events''' 31 | 32 | while True: 33 | try: 34 | r = requests.get(url, stream=True) 35 | r.raise_for_status() 36 | for event in iter_request_events(r.raw): 37 | yield event 38 | except Exception as exc: 39 | LOG.error('connection failed: %s' % exc) 40 | time.sleep(interval) 41 | 42 | 43 | class ServiceWatcher (object): 44 | '''A ServiceWatcher is an iterator that watches the Kubernetes API for 45 | changes to services, and yields these events as Python dictionaries.''' 46 | 47 | def __init__(self, 48 | reconnect_interval=defaults.reconnect_interval, 49 | kube_endpoint=defaults.kube_endpoint): 50 | super(ServiceWatcher, self).__init__() 51 | 52 | self.kube_api = '%s/api/v1beta1' % kube_endpoint 53 | self.reconnect_interval = reconnect_interval 54 | 55 | def __iter__(self): 56 | url = '%s/watch/services' % self.kube_api 57 | 58 | for event in iter_events(url, interval=self.reconnect_interval): 59 | service = event['object'] 60 | LOG.debug('received %s for %s', 61 | event['type'], 62 | service['id']) 63 | 64 | handler = getattr(self, 65 | 'handle_%s' % event['type'].lower()) 66 | 67 | # we log missing handlers at debug level because we probably 68 | # intentionally have not written a handler for the event. 69 | if not handler: 70 | LOG.debug('unknown event: %(type)s' % event) 71 | continue 72 | 73 | yield(handler(service)) 74 | 75 | def handle_added(self, service): 76 | return({'message': 'add-service', 77 | 'target': service['id'], 78 | 'service': service}) 79 | 80 | def handle_deleted(self, service): 81 | return({'message': 'delete-service', 82 | 'target': service['id'], 83 | 'service': service}) 84 | 85 | def handle_modified(self, service): 86 | return({'message': 'update-service', 87 | 'target': service['id'], 88 | 'service': service}) 89 | 90 | if __name__ == '__main__': 91 | import pprint 92 | 93 | logging.basicConfig(level=logging.DEBUG) 94 | s = ServiceWatcher() 95 | 96 | for msg in s: 97 | pprint.pprint(msg) 98 | -------------------------------------------------------------------------------- /kiwi/interface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import subprocess 4 | 5 | from exc import * 6 | 7 | re_label = re.compile(r'''\d+: \s+ (?P\S+) \s+ inet \s+ 8 | (?P\S+) \s+ scope \s+ (?P\S+) \s+ 9 | (?P.*)''', re.VERBOSE) 10 | 11 | LOG = logging.getLogger(__name__) 12 | 13 | 14 | class Interface (object): 15 | '''This is a network interface driver for Kiwi. It is responsible for 16 | adding and removing address to and from network interfaces.''' 17 | 18 | def __init__(self, 19 | interface='eth0', 20 | label='kube'): 21 | self.interface = interface 22 | self.label = label 23 | 24 | self.remove_labelled_addresses() 25 | 26 | def remove_labelled_addresses(self): 27 | '''Remove all addresses labelled with self.label from 28 | self.interface.''' 29 | 30 | try: 31 | out = subprocess.check_output([ 32 | 'ip', '-o', 'addr', 'show', 33 | 'label', '%s:%s' % (self.interface, self.label) 34 | ]) 35 | except subprocess.CalledProcessError as exc: 36 | raise InterfaceDriverError(reason=exc) 37 | 38 | # we're parsing the output of the 'ip' command here, which always 39 | # makes me nervous. 40 | for line in out.splitlines(): 41 | m = re_label.match(line) 42 | if not m: 43 | LOG.warn('unexpected interface configuration: %s', 44 | line) 45 | continue 46 | 47 | address = m.group('ipv4addr').split('/')[0] 48 | self.remove_address(address) 49 | 50 | def add_address(self, address, lft=None): 51 | '''Add the given address to the managed interface.''' 52 | LOG.info('add address %s to device %s', 53 | address, 54 | self.interface) 55 | 56 | # Note that we're using the 'label' option here to apply a 57 | # label to the address. This allows us to identify addresses 58 | # that we have added, which in turns allows us to clean them up 59 | # at startup without needing to otherwise preserve state. 60 | cmd = [ 'ip', 'addr', 'replace', 61 | '%s/32' % address, 62 | 'label', '%s:%s' % (self.interface, self.label), 63 | 'dev', self.interface ] 64 | 65 | if lft: 66 | cmd += ['preferred_lft', str(lft), 67 | 'valid_lft', str(lft)] 68 | 69 | try: 70 | subprocess.check_call(cmd) 71 | except subprocess.CalledProcessError as exc: 72 | raise InterfaceDriverError(reason=exc) 73 | 74 | def refresh_address(self, address, lft=None): 75 | self.add_address(address, lft=lft) 76 | 77 | def remove_address(self, address): 78 | '''Remove the given address from the managed interface.''' 79 | LOG.info('remove address %s from device %s', 80 | address, 81 | self.interface) 82 | try: 83 | subprocess.check_call([ 84 | 'ip', 'addr', 'del', 85 | '%s/32' % address, 86 | 'dev', self.interface 87 | ]) 88 | except subprocess.CalledProcessError as exc: 89 | raise InterfaceDriverError(reason=exc) 90 | 91 | def cleanup(self): 92 | self.remove_labelled_addresses() 93 | -------------------------------------------------------------------------------- /kiwi/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import argparse 6 | import logging 7 | 8 | import manager 9 | import defaults 10 | import interface 11 | import firewall 12 | 13 | LOG = logging.getLogger(__name__) 14 | 15 | 16 | def parse_args(): 17 | p = argparse.ArgumentParser() 18 | 19 | p.add_argument('--agent-id', '--id') 20 | p.add_argument('--refresh-interval', 21 | default=defaults.refresh_interval, 22 | type=int) 23 | p.add_argument('--reconnect-interval', 24 | default=defaults.reconnect_interval, 25 | type=int) 26 | 27 | g = p.add_argument_group('API endpoints') 28 | g.add_argument('--kube-endpoint', '-k', 29 | default=defaults.kube_endpoint) 30 | g.add_argument('--etcd-endpoint', '-s', 31 | default=defaults.etcd_endpoint) 32 | g.add_argument('--etcd-prefix', '-p', 33 | default=defaults.etcd_prefix) 34 | 35 | g = p.add_argument_group('Network options') 36 | g.add_argument('--interface', '-i', 37 | default=defaults.interface) 38 | g.add_argument('--fwchain', 39 | default=defaults.fwchain) 40 | g.add_argument('--fwmark', 41 | type=int, 42 | default=defaults.fwmark) 43 | g.add_argument('--cidr-range', '-r', 44 | action='append') 45 | g.add_argument('--no-driver', '-n', 46 | action='store_true') 47 | 48 | g = p.add_argument_group('Logging options') 49 | g.add_argument('--verbose', '-v', 50 | action='store_const', 51 | const=logging.INFO, 52 | dest='loglevel') 53 | g.add_argument('--debug', '-d', 54 | action='store_const', 55 | const=logging.DEBUG, 56 | dest='loglevel') 57 | g.add_argument('--debug-requests', 58 | action='store_true') 59 | 60 | p.set_defaults(loglevel=logging.WARN) 61 | 62 | return p.parse_args() 63 | 64 | 65 | def main(): 66 | args = parse_args() 67 | logging.basicConfig( 68 | level=args.loglevel, 69 | format='%(name)s [%(process)d] %(levelname)s %(message)s') 70 | 71 | if args.loglevel and not args.debug_requests: 72 | logging.getLogger('requests').setLevel(logging.WARN) 73 | 74 | LOG.info('Starting up') 75 | LOG.info('Kubernetes is %s', args.kube_endpoint) 76 | LOG.info('Etcd is %s', args.etcd_endpoint) 77 | LOG.info('Managing interface %s', args.interface) 78 | 79 | if args.no_driver: 80 | iface_driver = None 81 | fw_driver = None 82 | else: 83 | iface_driver = interface.Interface(args.interface) 84 | fw_driver = firewall.Firewall(fwchain=args.fwchain, 85 | fwmark=args.fwmark) 86 | 87 | mgr = manager.Manager(etcd_endpoint=args.etcd_endpoint, 88 | kube_endpoint=args.kube_endpoint, 89 | etcd_prefix=args.etcd_prefix, 90 | iface_driver=iface_driver, 91 | fw_driver=fw_driver, 92 | cidr_ranges=args.cidr_range, 93 | refresh_interval=args.refresh_interval, 94 | id=args.agent_id) 95 | 96 | LOG.info('My id is: %s', mgr.id) 97 | mgr.run() 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /kiwi/firewall.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | 4 | import defaults 5 | import iptables 6 | from exc import * 7 | 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | 12 | class Firewall (object): 13 | '''This is a firewall driver for kiwi, the Kubernetes address manager. 14 | This driver operates by creating rules in the `mangle` table that will 15 | apply a specific firewall mark to inbound packets. The driver uses the 16 | mangle table in order to match packets before they are modified by the 17 | REDIRECT rules generated in the nat table by kube-proxy.''' 18 | 19 | def __init__(self, 20 | fwchain=defaults.fwchain, 21 | fwmark=defaults.fwmark): 22 | 23 | self.fwchain = fwchain 24 | self.fwmark = fwmark 25 | self.rules = set() 26 | 27 | self.create_chain() 28 | self.flush_rules() 29 | 30 | def cleanup(self): 31 | self.flush_rules() 32 | 33 | def create_chain(self): 34 | '''Create self.fwchain if it does not already exist.''' 35 | 36 | if iptables.mangle.chain_exists(self.fwchain): 37 | return 38 | 39 | LOG.info('creating chain %s', self.fwchain) 40 | try: 41 | iptables.mangle.create_chain(self.fwchain) 42 | except iptables.CommandError as exc: 43 | raise FirewallDriverError(reason=exc) 44 | 45 | def flush_rules(self): 46 | '''Flush all rules in self.fwchain.''' 47 | 48 | LOG.info('flushing all rules from %s', 49 | self.fwchain) 50 | self.rules = set() 51 | try: 52 | iptables.mangle.flush_chain(self.fwchain) 53 | except iptables.CommandError as exc: 54 | raise FirewallDriverError(reason=exc) 55 | 56 | def rule_for(self, address, service): 57 | '''Generate an iptables rule (returned as a tuple) for the given 58 | address and service.''' 59 | 60 | return iptables.Rule(str(arg) for arg in [ 61 | '-d', address, 62 | '-p', service['protocol'].lower(), 63 | '--dport', service['port'], 64 | '-m', 'comment', 65 | '--comment', service['id'], 66 | '-j', 'MARK', '--set-mark', self.fwmark 67 | ]) 68 | 69 | def add_service(self, address, service): 70 | '''Add a new service to the firewall.''' 71 | 72 | rule = self.rule_for(address, service) 73 | if rule in self.rules: 74 | LOG.info('not adding rule for service %s ' 75 | 'on %s port %d (already exists)', 76 | service['id'], address, service['port']) 77 | return 78 | 79 | LOG.info('adding firewall rules for service %s ' 80 | 'on %s port %d', 81 | service['id'], address, service['port']) 82 | 83 | try: 84 | iptables.mangle.chains[self.fwchain].append(rule) 85 | except iptables.CommandError as exc: 86 | raise FirewallDriverError(reason=exc) 87 | else: 88 | self.rules.add(rule) 89 | 90 | def remove_service(self, address, service): 91 | '''Remove a service from the firewall.''' 92 | 93 | rule = self.rule_for(address, service) 94 | 95 | LOG.info('removing firewall rules for service %s ' 96 | 'on %s port %d', 97 | service['id'], address, service['port']) 98 | self.rules.remove(rule) 99 | try: 100 | iptables.mangle.chains[self.fwchain].remove(rule=rule) 101 | except iptables.CommandError as exc: 102 | raise FirewallDriverError(reason=exc) 103 | -------------------------------------------------------------------------------- /kiwi/addresswatcher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import time 4 | import re 5 | 6 | import defaults 7 | 8 | LOG = logging.getLogger(__name__) 9 | re_address = re.compile('\d+\.\d+\.\d+\.\d+') 10 | 11 | 12 | def iter_events(url, interval=1, recursive=True): 13 | '''Produces an inifite stream of events from etcd regarding the given 14 | URL.''' 15 | 16 | waitindex = None 17 | 18 | while True: 19 | try: 20 | params = {'recursive': recursive, 21 | 'wait': True, 22 | 'waitIndex': waitindex} 23 | 24 | r = requests.get(url, params=params) 25 | r.raise_for_status() 26 | 27 | event = r.json() 28 | waitindex = event['node']['modifiedIndex'] + 1 29 | yield event 30 | except Exception as exc: 31 | LOG.error('connection failed: %s' % exc) 32 | time.sleep(interval) 33 | 34 | 35 | class AddressWatcher (object): 36 | '''An AddressWatcher is an iterator that watches an etcd directory of 37 | keys that represent public ip addresses being managed by kiwi, and 38 | yields these events as Python dictionaries.''' 39 | 40 | def __init__(self, 41 | etcd_endpoint=defaults.etcd_endpoint, 42 | etcd_prefix=defaults.etcd_prefix, 43 | reconnect_interval=defaults.reconnect_interval): 44 | super(AddressWatcher, self).__init__() 45 | 46 | self.etcd_endpoint = etcd_endpoint 47 | self.etcd_prefix = etcd_prefix 48 | self.reconnect_interval = reconnect_interval 49 | 50 | def __iter__(self): 51 | url = '%s/v2/keys%s/publicips' % (self.etcd_endpoint, 52 | self.etcd_prefix) 53 | 54 | for event in iter_events(url, interval=self.reconnect_interval): 55 | LOG.debug('event: %s', event) 56 | 57 | node = event['node'] 58 | address = node['key'].split('/')[-1] 59 | 60 | if not re_address.match(address): 61 | LOG.error('invalid address %s', address) 62 | continue 63 | 64 | handler = getattr(self, 'handle_%s' % 65 | event['action'].lower(), None) 66 | 67 | # we log missing handlers at debug level because we probably 68 | # intentionally have not written a handler for the event. 69 | if not handler: 70 | LOG.debug('unknown event: %(action)s' % event) 71 | continue 72 | 73 | yield(handler(address, node)) 74 | 75 | def handle_create(self, address, node): 76 | return({'message': 'create-address', 77 | 'target': address, 78 | 'address': address, 79 | 'node': node}) 80 | 81 | def handle_set(self, address, node): 82 | return({'message': 'set-address', 83 | 'target': address, 84 | 'address': address, 85 | 'node': node}) 86 | 87 | def handle_delete(self, address, node): 88 | return({'message': 'delete-address', 89 | 'target': address, 90 | 'address': address, 91 | 'node': node}) 92 | 93 | handle_compareanddelete = handle_delete 94 | 95 | def handle_expire(self, address, node): 96 | return({'message': 'expire-address', 97 | 'target': address, 98 | 'address': address, 99 | 'node': node}) 100 | 101 | if __name__ == '__main__': 102 | import pprint 103 | 104 | logging.basicConfig(level=logging.DEBUG) 105 | s = AddressWatcher() 106 | 107 | for msg in s: 108 | pprint.pprint(msg) 109 | -------------------------------------------------------------------------------- /kiwi/tests/test_iptables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import argparse 6 | import unittest 7 | import mock 8 | import subprocess 9 | 10 | from kiwi import iptables 11 | 12 | iptables_filter_input_output = '\n'.join([ 13 | '-P INPUT ACCEPT', 14 | '-A INPUT -s 192.168.1.1 -j ACCEPT', 15 | '-A INPUT -s 192.168.1.0/24 -p tcp --dport 80 -j ACCEPT', 16 | ]) 17 | 18 | iptables_filter_output = '\n'.join([ 19 | '-P INPUT ACCEPT', 20 | '-P FORWARD ACCEPT', 21 | '-P OUTPUT ACCEPT', 22 | '-N testchain -', 23 | ]) 24 | 25 | 26 | class TestTables(unittest.TestCase): 27 | @mock.patch('subprocess.Popen') 28 | def test_chain_exists(self, mock_popen): 29 | mock_popen_return = mock.Mock() 30 | attrs = { 31 | 'communicate.return_value': (iptables_filter_input_output, 32 | ''), 33 | 'returncode': 0, 34 | } 35 | mock_popen_return.configure_mock(**attrs) 36 | mock_popen.configure_mock(return_value=mock_popen_return) 37 | assert iptables.filter.chain_exists('INPUT') 38 | mock_popen.assert_called_with(('iptables', '-w', '-t', 39 | 'filter', '-S', 'INPUT'), 40 | stdout=subprocess.PIPE, 41 | stderr=subprocess.PIPE) 42 | 43 | @mock.patch('subprocess.Popen') 44 | def test_chain_does_not_exist(self, mock_popen): 45 | mock_popen_return = mock.Mock() 46 | attrs = { 47 | 'communicate.return_value': ('\n', '\n'), 48 | 'returncode': 1, 49 | } 50 | mock_popen_return.configure_mock(**attrs) 51 | mock_popen.configure_mock(return_value=mock_popen_return) 52 | assert not iptables.filter.chain_exists('does_not_exist') 53 | mock_popen.assert_called_with(('iptables', '-w', '-t', 54 | 'filter', '-S', 'does_not_exist'), 55 | stdout=subprocess.PIPE, 56 | stderr=subprocess.PIPE) 57 | 58 | @mock.patch('subprocess.Popen') 59 | def test_list_chains(self, mock_popen): 60 | mock_popen_return = mock.Mock() 61 | attrs = { 62 | 'communicate.return_value': (iptables_filter_output, '\n'), 63 | 'returncode': 0, 64 | } 65 | mock_popen_return.configure_mock(**attrs) 66 | mock_popen.configure_mock(return_value=mock_popen_return) 67 | chains = tuple(iptables.filter.list_chains()) 68 | assert chains == ('INPUT', 'FORWARD', 'OUTPUT', 'testchain') 69 | mock_popen.assert_called_with(('iptables', '-w', '-t', 70 | 'filter', '-S'), 71 | stdout=subprocess.PIPE, 72 | stderr=subprocess.PIPE) 73 | 74 | @mock.patch('subprocess.Popen') 75 | def test_get_chain(self, mock_popen): 76 | mock_popen_return = mock.Mock() 77 | attrs = { 78 | 'communicate.return_value': (iptables_filter_output, '\n'), 79 | 'returncode': 0, 80 | } 81 | mock_popen_return.configure_mock(**attrs) 82 | mock_popen.configure_mock(return_value=mock_popen_return) 83 | chain = iptables.filter.chains['INPUT'] 84 | assert chain.name == 'INPUT' 85 | mock_popen.assert_called_with(('iptables', '-w', '-t', 86 | 'filter', '-S', 'INPUT'), 87 | stdout=subprocess.PIPE, 88 | stderr=subprocess.PIPE) 89 | 90 | class TestChains(unittest.TestCase): 91 | @mock.patch('subprocess.Popen') 92 | def test_rule_exists(self, mock_popen): 93 | mock_popen_return = mock.Mock() 94 | attrs = { 95 | 'communicate.return_value': (iptables_filter_output, '\n'), 96 | 'returncode': 0, 97 | } 98 | mock_popen_return.configure_mock(**attrs) 99 | mock_popen.configure_mock(return_value=mock_popen_return) 100 | chain = iptables.filter.chains['INPUT'] 101 | assert chain.name == 'INPUT' 102 | mock_popen.assert_called_with(('iptables', '-w', '-t', 103 | 'filter', '-S', 'INPUT'), 104 | stdout=subprocess.PIPE, 105 | stderr=subprocess.PIPE) 106 | 107 | rule = iptables.Rule( 108 | '-A INPUT -s 192.168.1.0/24 -p tcp --dport 80 -j ACCEPT') 109 | assert chain.rule_exists(rule) 110 | mock_popen.assert_called_with(('iptables', '-w', '-t', 111 | 'filter', '-C', 'INPUT') + rule, 112 | stdout=subprocess.PIPE, 113 | stderr=subprocess.PIPE) 114 | 115 | @mock.patch('subprocess.Popen') 116 | def test_rule_does_not_exist(self, mock_popen): 117 | mock_popen_return = mock.Mock() 118 | attrs = { 119 | 'communicate.return_value': (iptables_filter_output, '\n'), 120 | 'returncode': 0, 121 | } 122 | mock_popen_return.configure_mock(**attrs) 123 | mock_popen.configure_mock(return_value=mock_popen_return) 124 | chain = iptables.filter.chains['INPUT'] 125 | assert chain.name == 'INPUT' 126 | mock_popen.assert_called_with(('iptables', '-w', '-t', 127 | 'filter', '-S', 'INPUT'), 128 | stdout=subprocess.PIPE, 129 | stderr=subprocess.PIPE) 130 | 131 | rule = iptables.Rule( 132 | '-A INPUT -j does_not_exist') 133 | mock_popen_return.configure_mock(returncode=1) 134 | assert not chain.rule_exists(rule) 135 | mock_popen.assert_called_with(('iptables', '-w', '-t', 136 | 'filter', '-C', 'INPUT') + rule, 137 | stdout=subprocess.PIPE, 138 | stderr=subprocess.PIPE) 139 | -------------------------------------------------------------------------------- /kiwi/iptables.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import six 3 | import functools 4 | import shlex 5 | import logging 6 | 7 | LOG = logging.getLogger(__name__) 8 | 9 | 10 | class CommandError(Exception): 11 | def __init__(self, command, returncode, stdout, stderr): 12 | self.command = command 13 | self.returncode = returncode 14 | self.stdout = stdout 15 | self.stderr = stderr 16 | 17 | def __repr__(self): 18 | return '' % ( 19 | self.returncode, 20 | self.stderr.splitlines()[0]) 21 | 22 | def __str__(self): 23 | return repr(self) 24 | 25 | 26 | def cmd(*args): 27 | '''This acts very much like subprocess.check_output, except that 28 | it raises CommandError if a command exits with a non-zero exit code, 29 | and the CommandError objects include the full command spec, a 30 | returncode, stdout, and stderr.''' 31 | 32 | LOG.debug('running command: %s', ' '.join(args)) 33 | p = subprocess.Popen(args, 34 | stdout=subprocess.PIPE, 35 | stderr=subprocess.PIPE) 36 | out, err = p.communicate() 37 | 38 | if p.returncode != 0: 39 | LOG.debug('command failed [%d]: %s...', 40 | p.returncode, 41 | err.splitlines()[0]) 42 | raise CommandError(args, p.returncode, out, err) 43 | 44 | return out 45 | 46 | 47 | class Rule(tuple): 48 | def __new__(cls, *args): 49 | if isinstance(args[0], six.string_types): 50 | args = (shlex.split(args[0]),) 51 | 52 | return super(Rule, cls).__new__(cls, *args) 53 | 54 | def __str__(self): 55 | return ' '.join(self) 56 | 57 | 58 | class Chain(object): 59 | def __init__(self, name, table): 60 | self.name = name 61 | self.table = table 62 | self.iptables = table.iptables 63 | 64 | def __str__(self): 65 | return '' % ( 66 | self.table.table, 67 | self.name) 68 | 69 | def __repr__(self): 70 | return str(self) 71 | 72 | def rules(self): 73 | for rule in self.iptables('-S', self.name).splitlines(): 74 | rule = Rule(rule) 75 | if rule[0] != '-A': 76 | continue 77 | 78 | yield Rule(rule[2:]) 79 | 80 | def rule_exists(self, rule): 81 | try: 82 | self.iptables('-C', self.name, *rule) 83 | except CommandError as err: 84 | if err.returncode != 1: 85 | raise 86 | 87 | return False 88 | else: 89 | return True 90 | 91 | @property 92 | def policy(self): 93 | '''This is property that when read returns the current default 94 | policy for this chain and when assigned to changes the default 95 | policy.''' 96 | for rule in self.iptables('-S', self.name).splitlines(): 97 | rule = Rule(rule) 98 | if rule[0] == '-P': 99 | return rule[2] 100 | 101 | raise ValueError('chain does not have default policy') 102 | 103 | @policy.setter 104 | def policy(self, value): 105 | '''Set the default policy for this chain.''' 106 | self.iptables('-P', self.name, value) 107 | 108 | def append(self, rule): 109 | self.iptables('-A', self.name, *rule) 110 | 111 | def insert(self, rule, pos=1): 112 | self.iptables('-I', self.name, str(pos), *rule) 113 | 114 | def replace(self, pos, rule): 115 | self.iptables('-R', self.name, str(pos), *rule) 116 | 117 | def zero(self): 118 | self.iptables('-Z', self.name) 119 | 120 | def delete(self, rule=None, pos=None): 121 | if rule is not None: 122 | self.iptables('-D', self.name, *rule) 123 | elif pos is not None: 124 | self.iptables('-D', self.name, str(pos)) 125 | else: 126 | raise ValueError('requires either rule or position') 127 | 128 | def flush(self): 129 | self.iptables('-F', self.name) 130 | 131 | 132 | class ChainFinder(object): 133 | def __init__(self, table): 134 | self.table = table 135 | 136 | def __getitem__(self, k): 137 | return self.table.get_chain(k) 138 | 139 | def __iter__(self): 140 | for k in self.keys(): 141 | yield self.table.get_chain(k) 142 | 143 | def keys(self): 144 | for chain in self.table.list_chains(): 145 | yield chain 146 | 147 | 148 | class Table(object): 149 | def __init__(self, name='filter', netns=None): 150 | self.name = name 151 | 152 | prefix = () 153 | if netns is not None: 154 | prefix = ('ip', 'netns', 'exec', netns) 155 | 156 | self.iptables = functools.partial( 157 | cmd, *(prefix + ('iptables', '-w', '-t', name))) 158 | 159 | self.chains = ChainFinder(self) 160 | 161 | def __str__(self): 162 | return '' % (self.name,) 163 | 164 | def __repr__(self): 165 | return str(self) 166 | 167 | def chain_exists(self, chain): 168 | try: 169 | self.iptables('-S', chain) 170 | except CommandError: 171 | return False 172 | else: 173 | return True 174 | 175 | def list_chains(self): 176 | for rule in self.iptables('-S').splitlines(): 177 | rule = Rule(rule) 178 | if rule[0] in ['-P', '-N']: 179 | yield rule[1] 180 | 181 | def get_chain(self, chain): 182 | if not self.chain_exists(chain): 183 | raise KeyError(chain) 184 | 185 | return Chain(chain, self) 186 | 187 | def create_chain(self, chain): 188 | self.iptables('-N', chain) 189 | return self.chains[chain] 190 | 191 | def delete_chain(self, chain): 192 | self.iptables('-X', chain) 193 | 194 | def flush_chain(self, chain): 195 | self.iptables('-F', chain) 196 | 197 | def flush_all(self): 198 | self.iptables('-F') 199 | 200 | def zero_all(self): 201 | self.iptables('-Z') 202 | 203 | def rule_exists(self, chain, rule): 204 | chain = self.chain[chain] 205 | return chain.rule_exists(rule) 206 | 207 | 208 | filter = Table('filter') 209 | nat = Table('nat') 210 | mangle = Table('mangle') 211 | raw = Table('raw') 212 | -------------------------------------------------------------------------------- /kiwi/manager.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import uuid 3 | import time 4 | import logging 5 | import netaddr 6 | import threading 7 | import Queue 8 | 9 | from exc import * 10 | import defaults 11 | import addresswatcher 12 | import servicewatcher 13 | 14 | 15 | LOG = logging.getLogger(__name__) 16 | 17 | 18 | class Manager (object): 19 | def __init__(self, 20 | id=None, 21 | kube_endpoint=defaults.kube_endpoint, 22 | etcd_endpoint=defaults.etcd_endpoint, 23 | etcd_prefix=defaults.etcd_prefix, 24 | iface_driver=None, 25 | fw_driver=None, 26 | cidr_ranges=None, 27 | refresh_interval=defaults.refresh_interval): 28 | 29 | super(Manager, self).__init__() 30 | 31 | if id is None: 32 | id = str(uuid.uuid1()) 33 | 34 | self.id = id 35 | self.refresh_interval = refresh_interval 36 | 37 | self.etcd_endpoint = etcd_endpoint 38 | self.etcd_prefix = etcd_prefix 39 | self.kube_endpoint = kube_endpoint 40 | self.iface_driver = iface_driver 41 | self.fw_driver = fw_driver 42 | self.cidr_ranges = cidr_ranges 43 | 44 | if self.cidr_ranges: 45 | self.cidr_ranges = [netaddr.IPNetwork(n) 46 | for n in cidr_ranges] 47 | 48 | self.addresses = {} 49 | 50 | self.q = Queue.Queue() 51 | 52 | def run(self): 53 | try: 54 | self.mainloop() 55 | finally: 56 | self.cleanup() 57 | 58 | def watch_addresses(self): 59 | '''Read address events and stuff them into the queue.''' 60 | watcher = addresswatcher.AddressWatcher( 61 | etcd_endpoint=self.etcd_endpoint, 62 | etcd_prefix=self.etcd_prefix) 63 | 64 | for event in watcher: 65 | self.q.put(event) 66 | 67 | def watch_services(self): 68 | '''Read service events and stuff them into the queue.''' 69 | watcher = servicewatcher.ServiceWatcher( 70 | kube_endpoint=self.kube_endpoint) 71 | 72 | for event in watcher: 73 | LOG.debug('event:', event) 74 | self.q.put(event) 75 | 76 | def mainloop(self): 77 | last_refresh = 0 78 | 79 | # start worker threads to feed the event queue 80 | [thread.start() for thread in [ 81 | threading.Thread(target=self.watch_services), 82 | threading.Thread(target=self.watch_addresses), 83 | ]] 84 | 85 | while True: 86 | try: 87 | msg = self.q.get(True, self.refresh_interval) 88 | LOG.debug('dequeued message %s for %s', 89 | msg['message'], 90 | msg['target']) 91 | 92 | self.handle_message(msg) 93 | except AttributeError: 94 | LOG.debug('unhandled message %s for %s', 95 | msg['message'], 96 | msg['target']) 97 | except Queue.Empty: 98 | LOG.debug('Punt!') 99 | pass 100 | 101 | now = time.time() 102 | if now > last_refresh + self.refresh_interval: 103 | self.refresh() 104 | last_refresh = now 105 | 106 | def handle_message(self, msg): 107 | attr = 'handle_%s' % msg['message'].replace('-', '_') 108 | LOG.debug('looking for %s', attr) 109 | handler = getattr(self, attr) 110 | 111 | handler(msg) 112 | 113 | def refresh(self): 114 | LOG.info('start refresh pass (%d addresses)', 115 | len(self.addresses)) 116 | 117 | claimed = 0 118 | for address in self.addresses.keys(): 119 | if self.address_is_claimed(address): 120 | claimed += 1 121 | self.refresh_address(address) 122 | 123 | LOG.info('finished refresh pass (%d addresses, %d claimed)', 124 | len(self.addresses), 125 | claimed) 126 | 127 | def url_for(self, address): 128 | return '%s/v2/keys%s/publicips/%s' % ( 129 | self.etcd_endpoint, 130 | self.etcd_prefix, 131 | address) 132 | 133 | def refresh_address(self, address): 134 | assert address in self.addresses 135 | assert self.addresses[address]['claimed'] 136 | 137 | LOG.info('refresh %s', address) 138 | try: 139 | r = requests.put(self.url_for(address), 140 | params={'prevValue': self.id, 141 | 'ttl': self.refresh_interval * 2}, 142 | data={'value': self.id}) 143 | r.raise_for_status() 144 | 145 | if self.iface_driver: 146 | self.iface_driver.refresh_address( 147 | address, 148 | lft=self.refresh_interval*2) 149 | except Exception as exc: 150 | LOG.error('failed to refresh address %s: %s', 151 | address, exc) 152 | self.release_address(address) 153 | 154 | def claim_address(self, address): 155 | assert address in self.addresses 156 | 157 | try: 158 | r = requests.put(self.url_for(address), 159 | params={'prevExist': 'false', 160 | 'ttl': self.refresh_interval*2}, 161 | data={'value': self.id}) 162 | except requests.ConnectionError as exc: 163 | LOG.error('connection to %s failed: %s', 164 | self.url_for(address), 165 | exc) 166 | return 167 | else: 168 | if not r.ok: 169 | # We log failures at debug level because we expect to see 170 | # failures here if another node asserts a claim first. 171 | LOG.debug('failed to claim %s: %s', 172 | address, 173 | r.reason) 174 | return 175 | 176 | LOG.warn('claimed %s', address) 177 | self.addresses[address]['claimed'] = True 178 | 179 | if self.iface_driver: 180 | try: 181 | self.iface_driver.add_address(address, 182 | lft=self.refresh_interval*2) 183 | except InterfaceDriverError as exc: 184 | LOG.error('failed to configure address on system: %d', 185 | exc.returncode) 186 | 187 | def release_address(self, address): 188 | if not self.address_is_claimed(address): 189 | LOG.debug('not releasing unclaimed address %s', 190 | address) 191 | return 192 | 193 | self.addresses[address]['claimed'] = False 194 | 195 | try: 196 | r = requests.delete(self.url_for(address), 197 | params={'prevValue': self.id}) 198 | except requests.ConnectionError as exc: 199 | LOG.error('connection to %s failed: %s', 200 | self.url_for(address), 201 | exc) 202 | else: 203 | if not r.ok: 204 | LOG.error('failed to release %s: %s', 205 | address, 206 | r.reason) 207 | else: 208 | LOG.warn('released %s', address) 209 | 210 | if self.iface_driver: 211 | try: 212 | self.iface_driver.remove_address(address) 213 | except InterfaceDriverError as exc: 214 | LOG.error('failed to remove address on system: %d', 215 | exc.returncode) 216 | 217 | def remove_address(self, address): 218 | assert address in self.addresses 219 | 220 | LOG.info('removing address %s', address) 221 | self.release_address(address) 222 | del self.addresses[address] 223 | 224 | def release_all_addresses(self): 225 | for address in self.addresses.keys(): 226 | self.release_address(address) 227 | 228 | def handle_add_service(self, msg): 229 | service = msg['service'] 230 | 231 | for address in service.get('publicIPs', []): 232 | if not self.address_is_valid(address): 233 | LOG.warn('ignoring invalid address %s', 234 | address) 235 | continue 236 | 237 | LOG.info('adding service %s on %s', 238 | service['id'], 239 | address) 240 | 241 | if self.fw_driver: 242 | try: 243 | self.fw_driver.add_service(address, service) 244 | except FirewallDriverError as exc: 245 | LOG.error('failed to configure host firewall: %d', 246 | exc.returncode) 247 | 248 | try: 249 | self.addresses[address]['count'] += 1 250 | except KeyError: 251 | self.addresses[address] = { 252 | 'count': 1, 253 | 'claimed': False 254 | } 255 | 256 | if not self.address_is_claimed(address): 257 | self.claim_address(address) 258 | 259 | def handle_delete_service(self, msg): 260 | service = msg['service'] 261 | 262 | for address in service.get('publicIPs', []): 263 | if not self.address_is_valid(address): 264 | LOG.warn('ignoring invalid address %s', 265 | address) 266 | continue 267 | 268 | LOG.info('removing service %s on %s', 269 | service['id'], 270 | address) 271 | 272 | if self.fw_driver: 273 | try: 274 | self.fw_driver.remove_service(address, service) 275 | except FirewallDriverError as exc: 276 | LOG.error('failed to configure host firewall: %d', 277 | exc.returncode) 278 | 279 | if address in self.addresses: 280 | self.addresses[address]['count'] -= 1 281 | if not self.address_is_active(address): 282 | self.remove_address(address) 283 | 284 | def handle_delete_address(self, msg): 285 | address = msg['address'] 286 | if self.address_is_active(address): 287 | self.claim_address(address) 288 | 289 | handle_expire_address = handle_delete_address 290 | 291 | def address_is_active(self, address): 292 | return (address in self.addresses and 293 | self.addresses[address]['count'] > 0) 294 | 295 | def address_is_claimed(self, address): 296 | return (address in self.addresses and 297 | self.addresses[address]['claimed']) 298 | 299 | def address_is_valid(self, address): 300 | if self.cidr_ranges is None: 301 | return True 302 | 303 | for net in self.cidr_ranges: 304 | if address in net: 305 | return True 306 | 307 | return False 308 | 309 | def cleanup(self): 310 | self.release_all_addresses() 311 | 312 | if self.fw_driver: 313 | self.fw_driver.cleanup() 314 | 315 | if self.iface_driver: 316 | self.iface_driver.cleanup() 317 | 318 | 319 | if __name__ == '__main__': 320 | logging.basicConfig(level=logging.DEBUG) 321 | l = logging.getLogger('requests') 322 | l.setLevel(logging.WARN) 323 | m = Manager() 324 | m.run() 325 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------