├── consul ├── __init__.py ├── consul_single_svc.ctmpl ├── consul_all_svc.ctmpl ├── README.md ├── cfg.example.json └── cfg_file.py ├── swarm ├── __init__.py ├── README.md └── docker_swarm.py ├── kubernetes ├── __init__.py ├── nitrox-rc.yaml ├── k8s.md ├── client.py ├── README.md └── kubernetes.py ├── marathon ├── __init__.py ├── nitrox.json ├── README.md └── mesos_marathon.py ├── nitrox.png ├── Dockerfile ├── README.md ├── main.py ├── netscaler.py └── LICENSE /consul/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swarm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kubernetes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /marathon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nitrox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/nitrox/master/nitrox.png -------------------------------------------------------------------------------- /consul/consul_single_svc.ctmpl: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "servicename": "python-micro-service", 4 | "backends": [ 5 | {{- range $index, $backend := service "python-micro-service"}} 6 | {{- if $index}} 7 | {{- ","}} 8 | {{- end}} 9 | {"host": "{{.Address}}", "port":"{{.Port}}"} 10 | {{- end}} 11 | ] 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /consul/consul_all_svc.ctmpl: -------------------------------------------------------------------------------- 1 | [ 2 | {{- range $index, $svc := services}} 3 | {{- if $index}} 4 | {{- ","}} 5 | {{- end}} 6 | { 7 | "servicename": "{{$svc.Name}}", 8 | "backends": [ 9 | {{- range $index1, $backend := service $svc.Name}} 10 | {{- if $index1}} 11 | {{- ","}} 12 | {{- end}} 13 | {"host": "{{.Address}}", "port":"{{.Port}}"} 14 | {{- end}} 15 | ] 16 | } 17 | {{- end}} 18 | ] 19 | -------------------------------------------------------------------------------- /marathon/nitrox.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "nitrox", 3 | "cpus": 0.5, 4 | "mem": 256.0, 5 | "container": { 6 | "type": "DOCKER", 7 | "docker": { 8 | "image": "chiradeeptest/nitrox", 9 | "network": "BRIDGE" 10 | } 11 | }, 12 | "args": [ 13 | "--marathon-url", "http://marathon-master:8080/" 14 | ], 15 | "env": { 16 | "NS_IP":"ns1.lab.myco.org" 17 | "NS_USER":"nsroot", 18 | "NS_PASSWORD":"4hja8A9922@G", 19 | "APP_INFO":"{\"apps\": [{\"name\": \"basic-5\"}, {\"name\": \"basic-3\"}]}" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /consul/README.md: -------------------------------------------------------------------------------- 1 | # Nitrox for Consul-Template 2 | 3 | # Usage 4 | ## Pre-requisites 5 | 1. [Consul-template] (https://github.com/hashicorp/consul-template) 6 | 2. Netscaler pre-requisites are [here](../README.md) 7 | 8 | ## Theory of operation 9 | `consul-template` creates a JSON config file for a Consul service. This config file is fed to the python script which drives Netscaler configuration 10 | 11 | ## Example 12 | 13 | ```` 14 | # in top-level directory of project 15 | consul-template -consul $CONSUL_IP:8500 -template consul_single_svc.ctmpl:cfg.json:"python main.py --cfg-file cfg.json" 16 | ```` 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2 2 | MAINTAINER Chiradeep Vittal 3 | RUN mkdir -p /tmp 4 | RUN (cd /tmp && wget http://downloadns.citrix.com.edgesuite.net/10902/ns-10.5-58.11-sdk.tar.gz ) 5 | RUN (cd /tmp && tar xvzf ns-10.5-58.11-sdk.tar.gz && \ 6 | tar xvzf ns-10.5-58.11-nitro-python.tgz && \ 7 | tar xvf ns_nitro-python_tagma_58_11.tar && \ 8 | cd nitro-python-1.0/ && \ 9 | python setup.py install && \ 10 | cd / && \ 11 | rm -rf /tmp && \ 12 | mkdir -p /usr/src/app) 13 | 14 | RUN pip install docker-py 15 | RUN pip install pyyaml 16 | 17 | COPY *.py /usr/src/app/ 18 | COPY swarm/ /usr/src/app/swarm/ 19 | COPY marathon/ /usr/src/app/marathon/ 20 | COPY kubernetes/ /usr/src/app/kubernetes/ 21 | 22 | ENTRYPOINT ["python", "/usr/src/app/main.py" ] 23 | -------------------------------------------------------------------------------- /consul/cfg.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "servicename": "consul", 4 | "backends": [ 5 | {"host": "192.168.99.100", "port":"8300"} 6 | ] 7 | }, 8 | { 9 | "servicename": "consul-53", 10 | "backends": [ 11 | {"host": "192.168.99.100", "port":"8600"} 12 | ] 13 | }, 14 | { 15 | "servicename": "consul-8400", 16 | "backends": [ 17 | {"host": "192.168.99.100", "port":"8400"} 18 | ] 19 | }, 20 | { 21 | "servicename": "consul-8500", 22 | "backends": [ 23 | {"host": "192.168.99.100", "port":"8500"} 24 | ] 25 | }, 26 | { 27 | "servicename": "python-micro-service", 28 | "backends": [ 29 | {"host": "192.168.99.100", "port":"32768"}, 30 | {"host": "192.168.99.100", "port":"32769"}, 31 | {"host": "192.168.99.100", "port":"32770"} 32 | ] 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /kubernetes/nitrox-rc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: nitrox 5 | labels: 6 | svc: nitrox 7 | spec: 8 | replicas: 1 9 | template: 10 | metadata: 11 | labels: 12 | svc: nitrox 13 | spec: 14 | containers: 15 | - args: 16 | - --insecure-skip-tls-verify=true 17 | - --kube-apiserver=https://$(KUBERNETES_SERVICE_HOST)/api 18 | - --kube-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token 19 | name: nitrox 20 | image: chiradeeptest/nitrox 21 | resources: 22 | requests: 23 | cpu: 100m 24 | memory: 100Mi 25 | env: 26 | - name: GET_HOSTS_FROM 27 | value: env 28 | - name: NS_IP 29 | value: 10.220.225.225 30 | - name: NS_USER 31 | value: nsroot 32 | - name: NS_PASSWORD 33 | value: T4ju8kkk9 34 | - name: APP_INFO 35 | value: '{"apps": [{"name": "frontend"}]}' 36 | -------------------------------------------------------------------------------- /consul/cfg_file.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import json 4 | import logging 5 | 6 | 7 | logger = logging.getLogger('docker_netscaler') 8 | 9 | 10 | class ConfigFileDriver(object): 11 | """Uses a config file to drive Nitro APIs""" 12 | 13 | def __init__(self, netskaler, filename): 14 | """Constructor 15 | 16 | :param str filename: config filename 17 | """ 18 | self.netskaler = netskaler 19 | self.filename = filename 20 | self.cfg_json = json.load(open(self.filename)) 21 | 22 | def get_backends_for_app(self, appid): 23 | """Get host endpoints for apps 24 | 25 | :returns: endpoints list of tuples 26 | :rtype: list 27 | """ 28 | for svc in self.cfg_json: 29 | if svc["servicename"] == appid: 30 | return [(b['host'], b['port']) 31 | for b in svc['backends']] 32 | return [] 33 | 34 | def configure_ns_for_app(self, appname): 35 | backends = self.get_backends_for_app(appname) 36 | logger.debug("Backends for %s are %s" % (appname, str(backends))) 37 | self.netskaler.configure_app(appname, backends) 38 | 39 | 40 | if __name__ == "__main__": 41 | parser = argparse.ArgumentParser(description='Process Cfg File args') 42 | parser.add_argument("--cfg-file", required=True, dest='cfg_file') 43 | 44 | result = parser.parse_args() 45 | 46 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 47 | # {"name": "bar"}]}' 48 | app_info = json.loads(os.environ['APP_INFO']) 49 | appnames = map(lambda x: x['name'], app_info['apps']) 50 | 51 | cfg_file_driver = ConfigFileDriver(None, result.cfg_file) 52 | for app in appnames: 53 | endpoints = cfg_file_driver.get_backends_for_app(app) 54 | logger.info("Endpoints for app " + app + ": " + str(endpoints)) 55 | print("Endpoints for app " + app + ": " + str(endpoints)) 56 | -------------------------------------------------------------------------------- /marathon/README.md: -------------------------------------------------------------------------------- 1 | # Nitrox for Marathon 2 | You can run Nitrox as a container or as a regular python script. 3 | 4 | # Usage 5 | ## Pre-requisites 6 | 1. Marathon framework (at least v0.9) running on Mesos. 7 | 2. NetScaler pre-requisites are [here](../README.md) 8 | 9 | ## As a container 10 | ### Launch the 'nitrox' container 11 | The code has been containerized into `chiradeeptest/nitrox` . You can either run it as a Marathon app, or use this container on the same server as the Marathon master. 12 | 13 | #### On the master 14 | ```` 15 | [root@marathon-master ~]# 16 | [root@marathon-master ~]# docker run \ 17 | -e NS_IP=$NS_IP \ 18 | -e NS_USER=$NS_USER \ 19 | -e NS_PASSWORD=$NS_PASSWORD \ 20 | -e APP_INFO='{"apps": [{"name": "AccountService"}, {"name": "ProductCatalog"}, {"name":"ShoppingCart"}, {"name":"OrderServer"}]}' \ 21 | -d \ 22 | --name nitrox \ 23 | chiradeeptest/nitrox \ 24 | --marathon-url=http://marathon-master:8080/ 25 | ```` 26 | Monitor the logs of the containers with `docker logs nitrox` 27 | 28 | #### As a Marathon app 29 | The nitrox container can be scheduled using the Marathon APIs: 30 | 31 | ```` 32 | curl -X POST http://marathon-master:8080/v2/apps -d @nitrox.json -H "Content-type: application/json" 33 | ```` 34 | 35 | You can find `nitrox.json` [here](./nitrox.json) 36 | 37 | ### Test 38 | Run some containers using Marathon and see your netscaler get reconfigured 39 | 40 | ```` 41 | curl -X POST http://marathon-master:8080/v2/apps -d @basic-3.json -H "Content-type: application/json 42 | ```` 43 | Use the UI to scale the process up or down and watch the NetScaler being reconfigured 44 | 45 | 46 | 47 | ## For developers / hackers 48 | 49 | Download and install the Citrix NetScaler SDK for Python: 50 | 51 | ``` 52 | wget http://downloadns.citrix.com.edgesuite.net/10902/ns-11.0-65.31-sdk.tar.gz 53 | tar xzf ns-11.0-65.31-sdk.tar.gz 54 | tar xzvf ns-11.0-65.31-nitro-python.tgz 55 | cd nitro-python-1.0/ 56 | sudo python setup.py install 57 | ``` 58 | Install the docker python client 59 | 60 | ```` 61 | sudo pip install docker-py 62 | ```` 63 | 64 | Get the code: 65 | 66 | ``` 67 | git clone https://github.com/chiradeep/nitrox.git 68 | cd nitrox 69 | ``` 70 | 71 | Run the code while pointing it to the Marathon environment. 72 | 73 | ``` 74 | python main.py --marathon-url=http://marathon-master:8080/ 75 | ``` 76 | 77 | Test by launching containerized apps using the Marathon API and scaling them up/down using the GUI. 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nitrox 2 | Configure Citrix NetScaler loadbalancing for container platforms such as Docker Swarm, Mesos Marathon, Kubernetes and Consul. 3 | 4 | # Theory of Operation 5 | 1. Containers that form a load-balanced backend for an app/microservice are labeled with the same label (e.g., com.citrix.lb.appname=AccountService, or AccountService) or name 6 | 2. Information (host IP and port) from the container platform (such as [Docker Swarm](https://docs.docker.com/swarm/)) API for the labeled containers are used to configure a NetScaler loadbalancer. 7 | 3. The NetScaler admin creates the "frontend" `lb vserver` with the label/name used in #1 8 | 9 | 10 | 11 | # NetScaler Pre-requisites 12 | 13 | 1. Credentials for a running Citrix NetScaler (VPX/MPX/SDX/CPX). On the host where you run the container/code, replace with your own: 14 | 15 | ```` 16 | export NS_IP=10.220.73.33 17 | export NS_USER=nsroot 18 | export NS_PASSWORD=3df8jha@k0 19 | ```` 20 | 21 | 2. List of microservices / apps that have to be load balanced. For example, 'AccountService', 'ProductCatalog', 'ShoppingCart', etc. 22 | 3. NetScaler that has been configured with VIP(s) for above apps. For example, lets say there is a microservice/app called 'AccountService' with a load balanced IP of 10.220.73.222. On the NetScaler: 23 | 24 | ``` 25 | add lb vserver AccountService HTTP 10.220.73.222 80 -persistenceType COOKIE -lbMethod LEASTCONNECTION 26 | ``` 27 | 28 | Alternatively, if the `lb_ip` and `lb_port` are included in the `APP_INFO` env variable, the `lb vserver` is configured automatically with some default options (`ROUNDROBIN`) 29 | 4. (for developers) The NetScaler Python SDK (can be downloaded here https://www.citrix.com/downloads/netscaler-adc/sdks.html or copied from the NetScaler) 30 | 31 | #Container Platforms 32 | 33 | ### Docker Swarm 34 | [Docker Swarm] (https://docs.docker.com/swarm/) is a clustered container manager. Instructions are [here](swarm/README.md) 35 | 36 | ### Marathon 37 | [Marathon] (https://mesosphere.github.io/marathon/) is a PAAS framework that can run containerized workloads. Instructions are [here](marathon/README.md) 38 | 39 | ### Kubernetes 40 | [Kubernetes] (https://kubernetes.io/) is an open source orchestration system for Docker containers. It . Instructions are [here](kubernetes/README.md) 41 | 42 | ### Consul-template 43 | [consul-template] (https://github.com/hashicorp/consul-template) provides a convenient way to populate values from Consul. Instructions are [here](consul/README.md) 44 | -------------------------------------------------------------------------------- /kubernetes/k8s.md: -------------------------------------------------------------------------------- 1 | #Kubernetes Configuration 2 | 3 | ## Authentication 4 | The simplest way to get services in pods to use the API server is to run the API server with token authentication enabled. If not already enabled, here are some steps: 5 | 6 | 1. If not already using certificates, generate certificates using this [script](https://github.com/kubernetes/kubernetes/blob/master/cluster/saltbase/salt/generate-cert/make-ca-cert.sh). The certificate material will be in /srv/kubernetes 7 | 8 | ``` 9 | export CLUSTER_IP=$(kubectl get service kubernetes -o template --template {{.spec.clusterIP}}) 10 | export CERT_GROUP=kube 11 | ./make-ca-cert.sh IP:,IP:$CLUSTER_IP,DNS:kubernetes,DNS:kubernetes.default,DNS:kubernetes.default.svc,DNS:kubernetes.default.svc.cluster.local 12 | touch /srv/kubernetes/known_tokens.csv 13 | ``` 14 | 15 | 2. Change the parameters of the kube-apiserver and kube-controller-manager using the files `/etc/kubernetes/apiserver` and `/etc/kubernetes/controller-manager`. 16 | 17 | ```` 18 | /etc/kubernetes/apiserver: 19 | KUBE_API_ARGS="--client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key --token-auth-file=/srv/kubernetes/known_tokens.csv" 20 | /etc/kubernetes/controller-manager 21 | KUBE_CONTROLLER_MANAGER_ARGS="--root-ca-file=/srv/kubernetes/ca.crt --service-account-private-key-file=/srv/kubernetes/server.key" 22 | ```` 23 | 3. Restart 24 | 25 | ```` 26 | systemctl restart kube-apiserver 27 | systemctl restart kube-controller-manager 28 | ```` 29 | 4. You may have to clean up all existing services and create them from scratch (YMMV) 30 | 5. You can add users by editing `/srv/kubernetes/known_tokens.csv` 31 | 32 | ```` 33 | echo 'ABCDEFGHIKLMNOPQRSTUVWXYZ,scout,admin' >> /srv/kubernetes/known_tokens.csv 34 | systemctl restart kube-apiserver 35 | ```` 36 | Your cluster users can use this token 37 | 38 | ```` 39 | kubectl config set-credentials --token=ABCDEFGHIJKLMNOPQRSTUVWXYZ 40 | ```` 41 | 6. See the [Kubernetes User Guide to Accessing the Cluster](https://github.com/kubernetes/kubernetes/blob/master/docs/user-guide/accessing-the-cluster.md) for more help 42 | 43 | ## Service Accounts 44 | We need to enable service accounts. Edit `/etc/kubernetes/apiserver`. Ensure that this line has `ServiceAccount`: 45 | 46 | ```` 47 | KUBE_ADMISSION_CONTROL="--admission_control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ResourceQuota,ServiceAccount" 48 | ```` 49 | 50 | and restart: `systemctl restart kube-apiserver` 51 | 52 | -------------------------------------------------------------------------------- /kubernetes/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | K8s client. Deal with auth messiness 3 | """ 4 | import requests 5 | import yaml 6 | import posixpath 7 | 8 | 9 | class K8sConfig(object): 10 | """ 11 | Init from kubectl config 12 | """ 13 | 14 | def __init__(self, filename): 15 | """ 16 | Constructor 17 | 18 | :Parameters: 19 | - `filename`: The full path to the configuration file 20 | """ 21 | self.filename = filename 22 | self.doc = None 23 | 24 | def parse(self): 25 | """ 26 | Parses the configuration file. 27 | """ 28 | with open(self.filename) as f: 29 | self.doc = yaml.safe_load(f.read()) 30 | if "current-context" in self.doc and self.doc["current-context"]: 31 | current_context = filter(lambda x: 32 | x['name'] == self.doc['current-context'], 33 | self.doc['contexts'])[0]['context'] 34 | username = current_context['user'] 35 | clustername = current_context['cluster'] 36 | self.user = filter(lambda x: x['name'] == username, 37 | self.doc['users'])[0]['user'] 38 | self.cluster = filter(lambda x: x['name'] == clustername, 39 | self.doc['clusters'])[0]['cluster'] 40 | 41 | 42 | class K8sClient(object): 43 | """ 44 | Client for interfacing with the Kubernetes API. 45 | """ 46 | 47 | def __init__(self, cfg_file, url=None, token=None, ca=None, 48 | insecure_skip_tls_verify=False, version="/v1"): 49 | """ 50 | Creates a new instance of the HTTPClient. 51 | 52 | :Parameters: 53 | - `cfg_file`: Kubectl config file (useful for testing) 54 | - `token`: Useful for service account 55 | - `version`: The version of the API to use 56 | """ 57 | self.url = url 58 | self.token = token 59 | self.insecure_skip_tls_verify = insecure_skip_tls_verify 60 | self.ca = ca 61 | if cfg_file: 62 | self.config = K8sConfig(cfg_file) 63 | self.config.parse() 64 | self.url = self.config.cluster["server"] 65 | self.token = self.config.user["token"] 66 | if "certificate-authority" in self.config.cluster: 67 | self.ca = self.config.cluster["certificate-authority"] 68 | elif self.config.cluster.get('insecure-skip-tls-verify'): 69 | self.insecure_skip_tls_verify = True 70 | # TODO handle client cert 71 | self.version = version 72 | self.session = self.build_session() 73 | 74 | def build_session(self): 75 | """ 76 | Creates a new session for the client. 77 | """ 78 | s = requests.Session() 79 | if self.ca: 80 | s.verify = self.ca 81 | elif self.insecure_skip_tls_verify: 82 | s.verify = False 83 | s.headers["Authorization"] = "Bearer {}".format(self.token) 84 | # TODO: handle client cert 85 | return s 86 | 87 | def get_kwargs(self, **kwargs): 88 | """ 89 | Creates a full URL to request based on arguments. 90 | 91 | :Parametes: 92 | - `kwargs`: All keyword arguments to build a kubernetes API endpoint 93 | """ 94 | bits = [ 95 | self.version, 96 | ] 97 | if "namespace" in kwargs: 98 | bits.extend([ 99 | "namespaces", 100 | kwargs.pop("namespace"), 101 | ]) 102 | url = kwargs.get("url", "") 103 | if url.startswith("/"): 104 | url = url[1:] 105 | bits.append(url) 106 | kwargs["url"] = self.url + posixpath.join(*bits) 107 | return kwargs 108 | 109 | def request(self, *args, **kwargs): 110 | """ 111 | Makes an API request based on arguments. 112 | 113 | :Parameters: 114 | - `args`: Non-keyword arguments 115 | - `kwargs`: Keyword arguments 116 | """ 117 | return self.session.request(*args, **self.get_kwargs(**kwargs)) 118 | 119 | def get(self, *args, **kwargs): 120 | """ 121 | Executes an HTTP GET. 122 | 123 | :Parameters: 124 | - `args`: Non-keyword arguments 125 | - `kwargs`: Keyword arguments 126 | """ 127 | return self.session.get(*args, **self.get_kwargs(**kwargs)) 128 | -------------------------------------------------------------------------------- /swarm/README.md: -------------------------------------------------------------------------------- 1 | # Nitrox for Docker Swarm 2 | You can run Nitrox as a container or as a regular python script. 3 | 4 | # Usage 5 | ## Pre-requisites 6 | 1. Docker Swarm Cluster. Instructions assume you are running on the Swarm Manager/Master 7 | 2. NetScaler pre-requisites are [here](../README.md) 8 | 3. An `appkey` that will be used to label the containers that comprise the apps/microservices. For example, `com.citrix.lb.appname` 9 | 10 | ## As a container 11 | ### Launch the 'nitrox' container 12 | The code has been containerized into `chiradeeptest/nitrox` . Use this container from the swarm master: 13 | 14 | ```` 15 | [root@swarm-master ~]# eval "$(docker-machine env --swarm swarm-master)" 16 | [root@swarm-master ~]# docker run \ 17 | -e NS_IP=$NS_IP \ 18 | -e NS_USER=$NS_USER \ 19 | -e NS_PASSWORD=$NS_PASSWORD \ 20 | -e APP_INFO='{"appkey": "com.citrix.lb.appname", "apps": [{"name": "AccountService"}, {"name": "ProductCatalog"}, {"name":"ShoppingCart"}, {"name":"OrderServer"}]}' \ 21 | -d \ 22 | -v /etc/docker:/etc/docker \ 23 | --name nitrox \ 24 | chiradeeptest/nitrox \ 25 | --swarm-url=$DOCKER_HOST \ 26 | --swarm-tls-ca-cert=/etc/docker/ca.pem \ 27 | --swarm-tls-cert=/etc/docker/server.pem \ 28 | --swarm-tls-key=/etc/docker/server-key.pem 29 | ```` 30 | Monitor the logs of the containers with `docker logs nitrox` 31 | 32 | ### Test 33 | Run some containers and see your NetScaler get reconfigured 34 | 35 | ```` 36 | for i in 0 1 2 3 4 37 | do 38 | docker run -d -l com.citrix.lb.appname=AccountService --name AccountService$i -p 800$i:80 nginx 39 | done 40 | ```` 41 | Kill a few container instances: 42 | 43 | ```` 44 | docker stop AccountService0 45 | docker start AccountService0 46 | ```` 47 | Logs: 48 | 49 | ```` 50 | 2015-12-01 00:55:37,045 - DEBUG - [docker_swarm.py:watch_app ] (Thread-1) Event status: die, id: 97df5d1fa1d0 2015-12-01 00:55:37,048 - INFO - [docker_swarm.py:watch_app ] (Thread-1) Configuring NS for app AccountService,container id=97df5d1fa1d0 51 | 2015-12-01 00:55:37,048 - INFO - [docker_swarm.py:get_backends_for_app] (Thread-1) Getting backends for app label com.citrix.lb.appname=AccountService 52 | 2015-12-01 00:55:37,051 - DEBUG - [docker_swarm.py:configure_ns_for_app] (Thread-1) Backends are [(u'10.71.137.30', 8004), (u'10.71.137.7', 8003), (u'10.71.137.38', 8002), (u'10.71.137.30', 8001)] 53 | 2015-12-01 00:55:37,129 - INFO - [netscaler.py:_create_service_group] (Thread-1) Service group AccountService already configured 54 | 2015-12-01 00:55:37,182 - INFO - [netscaler.py:_bind_service_group_lb] (Thread-1) LB AccountService is already bound to service group AccountService 55 | 2015-12-01 00:55:37,240 - INFO - [netscaler.py:_configure_services] (Thread-1) Unbinding 10.71.137.38:8000 from service group AccountService 56 | 2015-12-01 00:55:37,279 - INFO - [netscaler.py:_configure_services] (Thread-1) 10.71.137.30:8002 is already bound to service group AccountService 57 | 2015-12-01 00:55:37,279 - INFO - [netscaler.py:_configure_services] (Thread-1) 10.71.137.38:8001 is already bound to service group AccountService 58 | 2015-12-01 00:55:37,279 - INFO - [netscaler.py:_configure_services] (Thread-1) 10.71.137.7:8004 is already bound to service group AccountService 59 | ```` 60 | 61 | 62 | ## For developers / hackers 63 | 64 | Download and install the Citrix NetScaler SDK for Python: 65 | ``` 66 | wget http://downloadns.citrix.com.edgesuite.net/11872/ns-11.0-65.31-sdk.tar.gz 67 | tar xzf ns-11.0-65.31-sdk.tar.gz 68 | tar xzvf ns-11.0-65.31-nitro-python.tgz 69 | cd nitro-python-1.0/ 70 | sudo python setup.py install 71 | ``` 72 | Install the docker python client 73 | ```` 74 | sudo pip install docker-py 75 | ```` 76 | 77 | Get the code: 78 | ``` 79 | git clone https://github.com/chiradeep/nitrox.git 80 | cd nitrox 81 | ``` 82 | 83 | Run the code while pointing it to the Docker Swarm environment. (This assumes you are running on the Docker Swarm manager) 84 | 85 | ``` 86 | eval "$(docker-machine env --swarm swarm-master)" 87 | python main.py --swarm-url=$DOCKER_HOST --swarm-tls-ca-cert=$DOCKER_CERT_PATH/ca.pem --swarm-tls-cert=$DOCKER_CERT_PATH/cert.pem --swarm-tls-key=$DOCKER_CERT_PATH/key.pem 88 | ``` 89 | 90 | Containers instances for each app backend have to be started with a label of the form label=app_key=app_name. For instance 91 | 92 | ```` 93 | for i in 0 1 2 3 4 5 94 | do 95 | docker run -d -l com.citrix.lb.appname=foo --name www$i -p 80$i:80 nginx 96 | done 97 | ```` 98 | 99 | Try changing the state of a few containers: 100 | 101 | ```` 102 | docker stop www0 103 | docker start www0 104 | ```` 105 | The Netscaler will get reconfigured by removing the container from the load balancer service group (docker stop) or with the new location/port of the container (docker run). 106 | -------------------------------------------------------------------------------- /swarm/docker_swarm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import threading 5 | from docker import Client 6 | from docker import tls 7 | 8 | import logging 9 | logger = logging.getLogger('docker_netscaler') 10 | 11 | 12 | class DockerSwarmInterface: 13 | 14 | def __init__(self, swarm_url, swarm_tls_ca_cert, swarm_tls_cert, 15 | swarm_tls_key, swarm_allow_insecure, app_info, netscaler): 16 | tls_config = False 17 | if not swarm_allow_insecure: 18 | if swarm_url.startswith("tcp"): 19 | swarm_url = swarm_url.replace("tcp", "https") 20 | logger.info("Using swarm url %s" % swarm_url) 21 | tls_config = tls.TLSConfig(client_cert=(swarm_tls_cert, 22 | swarm_tls_key), 23 | verify=swarm_tls_ca_cert, 24 | assert_hostname=False) 25 | self.client = Client(base_url=swarm_url, tls=tls_config) 26 | self.app_info = app_info 27 | self.netskaler = netscaler 28 | self.lock = threading.Lock() 29 | 30 | def get_backends_for_app(self, app_label): 31 | logger.info("Getting backends for app label %s" % app_label) 32 | containers = self.client.containers(filters={'status': 'running', 33 | 'label': [app_label]}) 34 | portConfigs = [n['Ports'] for n in containers] 35 | """ 36 | [[{u'Type': u'tcp', u'PrivatePort': 443}, 37 | {u'IP': u'0.0.0.0', u'Type': u'tcp', u'PublicPort': 807, u'PrivatePort': 80}], 38 | [{u'IP': u'0.0.0.0', u'Type': u'tcp', u'PublicPort': 806, u'PrivatePort': 80}, 39 | {u'Type': u'tcp', u'PrivatePort': 443}]] 40 | """ 41 | result = [] 42 | for ports in portConfigs: 43 | for port in ports: 44 | if port.get('PublicPort'): 45 | # TODO: handle the case where more than one port is exposed 46 | result.append((port['IP'], port['PublicPort'])) 47 | 48 | return result 49 | 50 | def configure_ns_for_app(self, app_key, appname): 51 | self.lock.acquire() 52 | try: 53 | app_label = app_key + "=" + appname 54 | backends = self.get_backends_for_app(app_label) 55 | # backends = map(lambda y: ("192.168.99.100", y[1]), backends) 56 | # TODO: remove above for actual swarm. With plain docker machine, 57 | # host IP is "0.0.0.0" -- that cannot be load balanced. Docker 58 | # swarm supplies correct host IP. 59 | logger.debug("Backends are %s" % str(backends)) 60 | self.netskaler.configure_app(appname, backends) 61 | finally: 62 | self.lock.release() 63 | 64 | def configure_all(self): 65 | app_key = self.app_info['appkey'] 66 | appnames = map(lambda x: x['name'], self.app_info['apps']) 67 | logger.info("Configuring for app names: %s" % str(appnames)) 68 | for appname in appnames: 69 | self.configure_ns_for_app(app_key, appname) 70 | self.watch_all_apps() 71 | self.wait_for_all() 72 | 73 | def watch_app(self, app_key, appname): 74 | app_label = app_key + "=" + appname 75 | events = self.client.events( 76 | filters={"event": ["start", "kill", "die"], 77 | "label": [app_label]}) 78 | for e in events: 79 | evj = json.loads(e) 80 | status = evj['status'] 81 | c_id = evj['id'] 82 | if status in ['start', 'die', 'kill']: 83 | # TODO: BUG in docker swarm events does not actually apply 84 | # filters. Use 'docker ps' to verify the app that is changing 85 | # belongs to this thread 86 | logger.debug("Event status: %s, id: %.12s" % 87 | (evj['status'], evj['id'])) 88 | containers = self.client.containers( 89 | all=True, 90 | filters={"label": [app_label]}) 91 | container_ids = [c.get('Id') for c in containers 92 | if c.get('Id') == c_id] 93 | if container_ids: 94 | logger.info("Configuring NS for app %s, " 95 | "container id=%.12s" % (appname, c_id)) 96 | self.configure_ns_for_app(app_key, appname) 97 | 98 | def watch_all_apps(self): 99 | app_key = self.app_info['appkey'] 100 | appnames = map(lambda x: x['name'], self.app_info['apps']) 101 | for appname in appnames: 102 | logger.debug("Watching for events for app: %s" % str(appname)) 103 | t = threading.Thread(target=self.watch_app, 104 | args=(app_key, appname,)) 105 | t.start() 106 | 107 | def wait_for_all(self): 108 | main_thread = threading.currentThread() 109 | for t in threading.enumerate(): 110 | if t is main_thread: 111 | continue 112 | logging.debug('joining %s', t.getName()) 113 | t.join() 114 | -------------------------------------------------------------------------------- /marathon/mesos_marathon.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import json 4 | import logging 5 | import requests 6 | import requests.exceptions 7 | 8 | 9 | logger = logging.getLogger('docker_netscaler') 10 | 11 | 12 | class MarathonInterface(object): 13 | """Interface for the Marathon REST API.""" 14 | 15 | def __init__(self, server, netskaler, app_info, 16 | username=None, password=None, timeout=10000): 17 | """Constructor 18 | 19 | :param server: Marathon URL (e.g., 'http://host:8080' ) 20 | :param str username: Basic auth username 21 | :param str password: Basic auth password 22 | :param int timeout: Timeout (in seconds) for requests to Marathon 23 | """ 24 | self.server = server 25 | self.netskaler = netskaler 26 | self.app_info = app_info 27 | self.auth = (username, password) if username and password else None 28 | self.timeout = timeout 29 | 30 | def get_backends_for_app(self, appid): 31 | """Get host endpoints for apps 32 | 33 | :returns: endpoints dict 34 | :rtype: dict 35 | """ 36 | response = None 37 | headers = {'Content-Type': 'application/json', 38 | 'Accept': 'application/json'} 39 | url = self.server + 'v2/apps' + appid + "/tasks" 40 | try: 41 | response = requests.request('GET', url, 42 | headers=headers, 43 | auth=self.auth) 44 | except requests.exceptions.RequestException as e: 45 | logger.error('Error while calling %s: %s', url, e.message) 46 | return [] 47 | if response.status_code >= 300: 48 | logger.error('Got HTTP {code}: {body}'. 49 | format(code=response.status_code, body=response.text)) 50 | return [] 51 | return [(t['host'], t['ports'][0]) # TODO: what if there are > 1 ports 52 | for t in response.json()['tasks']] 53 | 54 | def events(self): 55 | """Get event stream 56 | Requires Marathon v0.9. See: 57 | https://mesosphere.github.io/marathon/docs/rest-api.html#event-stream 58 | """ 59 | path = 'v2/events' 60 | url = self.server + path 61 | headers = {'Content-Type': 'application/json', 62 | 'Accept': 'text/event-stream'} 63 | r = requests.get(url, 64 | auth=self.auth, 65 | headers=headers, 66 | stream=True) 67 | for line in r.iter_lines(): 68 | if line and line.find("data:") > -1: 69 | event = json.loads(line[line.find("data:") + 70 | len('data: '):].rstrip()) 71 | if event['eventType'] == 'status_update_event': 72 | yield {k: event[k] 73 | for k in ['appId', 'host', 'taskStatus', 'taskId']} 74 | yield None 75 | 76 | def watch_all_apps(self): 77 | appnames = map(lambda x: '/' + x['name'], self.app_info['apps']) 78 | for ev in self.events(): 79 | if ev is None: 80 | continue 81 | app = ev['appId'] 82 | host = ev['host'] 83 | status = ev['taskStatus'] 84 | relevant = status in ['TASK_RUNNING', 85 | 'TASK_FINISHED', 86 | 'TASK_FAILED', 87 | 'TASK_KILLED', 88 | 'TASK_LOST'] 89 | if app is not None\ 90 | and app in appnames and relevant: 91 | logger.info("Configuring NS for app %s, " 92 | "host=%.12s status=%s" % (app, host, status)) 93 | self.configure_ns_for_app(app.lstrip("/")) 94 | 95 | def configure_ns_for_app(self, appname): 96 | backends = self.get_backends_for_app("/" + appname) 97 | logger.debug("Backends for %s are %s" % (appname, str(backends))) 98 | self.netskaler.configure_app(appname, backends) 99 | 100 | def configure_ns_for_all_apps(self): 101 | appnames = map(lambda x: x['name'], self.app_info['apps']) 102 | for app in appnames: 103 | self.configure_ns_for_app(app) 104 | 105 | 106 | if __name__ == "__main__": 107 | parser = argparse.ArgumentParser(description='Process Marathon args') 108 | parser.add_argument("--marathon-url", required=True, dest='marathon_url') 109 | 110 | result = parser.parse_args() 111 | 112 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 113 | # {"name": "bar"}]}' 114 | app_info = json.loads(os.environ['APP_INFO']) 115 | appnames = map(lambda x: x['name'], app_info['apps']) 116 | 117 | marathon = MarathonInterface(result.marathon_url) 118 | for app in appnames: 119 | endpoints = marathon.get_app_endpoints("/" + app) 120 | logger.info("Endpoints for app " + app + ": " + str(endpoints)) 121 | 122 | for e in marathon.events(): 123 | if e is not None and e in appnames: 124 | endpoints = marathon.get_app_endpoints(e) 125 | logger.info("Endpoints for app " + e + ": " + str(endpoints)) 126 | -------------------------------------------------------------------------------- /kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Nitrox for Kubernetes 2 | You can run Nitrox as a container or as a regular python script. 3 | 4 | # Theory of Operation 5 | Exposing a replicated service to [external access](https://github.com/kubernetes/kubernetes/blob/master/docs/user-guide/accessing-the-cluster.md) in Kubernetes can be done with a supported `LoadBalancer` or `NodePort`. In the case of `NodePort` a (random) port is chosen and this port is exposed on every host (node) in the cluster. `nitrox` listens for changes in the replication controller for an app and figures out the hosts(nodes) the pods belonging to the replication controller run on. The list of [(nodeIP:nodePort)] for each pod is configured on the NetScaler. 6 | 7 | Note that this is rather inefficient: traffic sent to each NodePort is itself load balanced by `KubeProxy` to the destination Pods. So, NetScaler configuration such as `lbMethod` and `persistence` may be redundant / incompatible. 8 | 9 | # Usage 10 | ## Pre-requisites 11 | 1. Kubernetes cluster, at least v1.1 12 | 2. Kubernetes [service accounts](https://github.com/kubernetes/kubernetes/blob/master/docs/admin/service-accounts-admin.md) feature, enabled as described in [k8s.md](k8s.md) 13 | 3. NetScaler pre-requisites are [here](../README.md) 14 | 15 | ## As a container 16 | ### Launch the 'nitrox' container 17 | The code has been containerized into `chiradeeptest/nitrox` . You can run it as a Kubernetes service, or simply on any docker engine 18 | 19 | #### Plain old docker-engine (e.g., your laptop) 20 | Assuming your certificate authority cert in $HOME/.kube/config : 21 | 22 | ```` 23 | [root@localhost ~]# 24 | [root@localhost ~]# docker run \ 25 | -e NS_IP=$NS_IP \ 26 | -e NS_USER=$NS_USER \ 27 | -e NS_PASSWORD=$NS_PASSWORD \ 28 | -e APP_INFO='{"apps": [{"name": "frontend"}]}' \ 29 | -d \ 30 | --name nitrox \ 31 | chiradeeptest/nitrox \ 32 | -v $HOME/.kube:/kube \ 33 | --kube-token='ABCDEFGHIJKLMNOPQRSTUVWXYZ'\ 34 | --kube-certificate-file=/kube/ca.crt 35 | ```` 36 | Monitor the logs of the containers with `docker logs nitrox`. Note that you can use the `--insecure-skip-tls-verify` flag instead of `--kube-certifcate-file` if you do not have access to the file. 37 | 38 | #### As a Kubernetes Pod 39 | The nitrox container can be scheduled using the Kubernetes APIs: 40 | 41 | ```` 42 | kubectl create -f nitrox-rc.yaml 43 | kubectl get rc nitrox 44 | ```` 45 | You can find nitrox-rc.yaml [here](https://github.com/chiradeep/nitrox/blob/master/kubernetes/nitrox-rc.yaml) 46 | Edit the environment variables in the file before launching. 47 | 48 | ### Test 49 | Run some services using Kubernetes. For an example, see the [Guestbook](https://github.com/kubernetes/kubernetes/tree/master/examples/guestbook) . The example is ideal since it has a service ('frontend') that needs to be enabled for external access/load balancing. Edit the 'all-in-one' spec so that the spec for `Service` frontend has `type` `NodePort` 50 | 51 | ```` 52 | @@ -129,6 +129,7 @@ spec: 53 | ports: 54 | # the port that this service should serve on 55 | - port: 80 56 | + type: NodePort 57 | selector: 58 | app: guestbook 59 | tier: frontend 60 | ```` 61 | 62 | Create the [guestbook service](https://github.com/kubernetes/kubernetes/tree/master/examples/guestbook): 63 | 64 | 65 | ```` 66 | kubectl create -f examples/guestbook/all-in-one/guestbook-all-in-one.yaml 67 | ```` 68 | 69 | Now, scale the service up and down to see the NetScaler being reconfigured: 70 | 71 | ```` 72 | kubectl scale --replicas=2 rc/frontend 73 | ```` 74 | Logs: 75 | 76 | ```` 77 | 2015-12-11 13:50:11,975 - INFO - [netscaler.py:_configure_services] (MainThread) Unbinding 10.220.135.41:30734 from service group frontend 78 | 2015-12-11 13:50:12,035 - INFO - [netscaler.py:_configure_services] (MainThread) 10.220.135.39:30734 is already bound to service group frontend 79 | 2015-12-11 13:50:12,035 - INFO - [netscaler.py:_configure_services] (MainThread) 10.220.135.43:30734 is already bound to service group frontend 80 | ```` 81 | 82 | #### Addenda 83 | If you have the [DNS add-on](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns) then you won't need to edit the the guestbook spec. Also you can change `nitrox-rc.yaml` to use DNS and use the certificate file in `/var/run/secrets/kubernetes.io/ca.crt` instead of `insecure-skip-tls-verify`. In this case, the API server URL would be `https://kubernetes/api` 84 | 85 | 86 | ## For developers / hackers 87 | 88 | Download and install the Citrix NetScaler SDK for Python: 89 | 90 | ``` 91 | wget http://downloadns.citrix.com.edgesuite.net/10902/ns-11.0-65.31-sdk.tar.gz 92 | tar xzf ns-11.0-65.31-sdk.tar.gz 93 | tar xzvf ns-11.0-65.31-nitro-python.tgz 94 | cd nitro-python-1.0/ 95 | sudo python setup.py install 96 | ``` 97 | Install the docker python client & YAML support 98 | 99 | ```` 100 | sudo pip install docker-py 101 | sudo pip install pyyaml 102 | ```` 103 | 104 | Get the code: 105 | 106 | ``` 107 | git clone https://github.com/chiradeep/nitrox.git 108 | cd nitrox 109 | ``` 110 | 111 | Run the code while pointing it to the Kubernetes environment. 112 | 113 | ``` 114 | python main.py --kubernetes-apiserver=https://kubernetes-master:6443/ --kube-token='ABCDEFGHIJKLMNOPQRSTUVWXYZ' --kube-certificate-file=~/.kube/ca.crt 115 | 116 | or 117 | 118 | python main.py --kube-config=~/.kube/config 119 | ``` 120 | 121 | Create the [guestbook service](https://github.com/kubernetes/kubernetes/tree/master/examples/guestbook): 122 | 123 | ```` 124 | kubectl create -f examples/guestbook/all-in-one/guestbook-all-in-one.yaml 125 | ```` 126 | 127 | Now, scale the service up and down to see the NetScaler being reconfigured: 128 | 129 | ```` 130 | kubectl scale --replicas=2 rc/frontend 131 | ```` 132 | 133 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import os 6 | import sys 7 | import json 8 | sys.path.append(os.getcwd()) 9 | from swarm.docker_swarm import DockerSwarmInterface 10 | from marathon.mesos_marathon import MarathonInterface 11 | from kubernetes.kubernetes import KubernetesInterface 12 | from netscaler import NetscalerInterface 13 | from consul.cfg_file import ConfigFileDriver 14 | 15 | logging.basicConfig(level=logging.CRITICAL, 16 | format='%(asctime)s - %(levelname)s - [%(filename)s:%(funcName)-10s] (%(threadName)s) %(message)s') 17 | logger = logging.getLogger('docker_netscaler') 18 | logger.addFilter(logging.Filter('docker_netscaler')) 19 | logger.setLevel(logging.DEBUG) 20 | 21 | 22 | def docker_swarm(app_info, netskaler): 23 | parser = argparse.ArgumentParser(description='Process Docker client args') 24 | group = parser.add_mutually_exclusive_group(required=True) 25 | group.add_argument("--swarm-allow-insecure") 26 | group.add_argument("--swarm-tls-ca-cert") 27 | parser.add_argument("--swarm-url", required=True, dest='swarm_url') 28 | parser.add_argument("--swarm-tls-cert", required=False, 29 | dest='swarm_tls_cert') 30 | parser.add_argument("--swarm-tls-key", required=False, 31 | dest='swarm_tls_key') 32 | 33 | result = parser.parse_args() 34 | 35 | dokker = DockerSwarmInterface(result.swarm_url, result.swarm_tls_ca_cert, 36 | result.swarm_tls_cert, result.swarm_tls_key, 37 | result.swarm_allow_insecure, 38 | app_info, netskaler) 39 | dokker.configure_all() 40 | 41 | 42 | def mesos_marathon(app_info, netskaler): 43 | parser = argparse.ArgumentParser(description='Process Marathon args') 44 | parser.add_argument("--marathon-url", required=True, dest='marathon_url') 45 | parser.add_argument("--marathon-user", dest='marathon_user') 46 | parser.add_argument("--marathon-password", dest='marathon_password') 47 | result = parser.parse_args() 48 | marathon = MarathonInterface(server=result.marathon_url, 49 | netskaler=netskaler, 50 | app_info=app_info, 51 | username=result.marathon_user, 52 | password=result.marathon_password) 53 | 54 | marathon.configure_ns_for_all_apps() 55 | marathon.watch_all_apps() 56 | 57 | 58 | def kubernetes(appinfo, netskaler): 59 | parser = argparse.ArgumentParser(description='Process Kubernetes args') 60 | parser.add_argument("--kube-config", required=False, 61 | dest='cfg', default=None) 62 | parser.add_argument("--kube-token", required=False, 63 | dest='token', default=None) 64 | parser.add_argument("--kube-token-file", required=False, 65 | dest='token_file', default=None) 66 | parser.add_argument("--kube-certificate-authority", required=False, 67 | dest='ca', default=None) 68 | parser.add_argument("--kube-apiserver", required=False, 69 | dest='server', default=None) 70 | parser.add_argument("--insecure-skip-tls-verify", 71 | required=False, dest='insecure', default=None) 72 | 73 | result = parser.parse_args() 74 | 75 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 76 | # {"name": "bar"}]}' 77 | app_info = json.loads(os.environ['APP_INFO']) 78 | appnames = map(lambda x: x['name'], app_info['apps']) 79 | 80 | if result.token_file: 81 | with open(result.token_file) as tf: 82 | result.token = tf.read().strip() 83 | 84 | kube = KubernetesInterface(cfg_file=result.cfg, 85 | token=result.token, 86 | server=result.server, 87 | insecure=result.insecure, 88 | ca=result.ca, 89 | netskaler=netskaler, 90 | app_info=appinfo) 91 | for app in appnames: 92 | endpoints = kube.get_backends_for_app(app) 93 | logger.info("Endpoints for app " + app + ": " + str(endpoints)) 94 | kube.watch_all_apps() 95 | 96 | 97 | def cfg_file_driver(netskaler, cfg_file): 98 | 99 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 100 | # {"name": "bar"}]}' 101 | app_info = json.loads(os.environ['APP_INFO']) 102 | appnames = map(lambda x: x['name'], app_info['apps']) 103 | 104 | cfg_file_driver = ConfigFileDriver(netskaler=netskaler, 105 | filename=cfg_file) 106 | for app in appnames: 107 | cfg_file_driver.configure_ns_for_app(app) 108 | 109 | if __name__ == "__main__": 110 | 111 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 112 | # {"name": "bar"}]}' 113 | app_info = json.loads(os.environ['APP_INFO']) 114 | netskaler = NetscalerInterface(os.environ.get("NS_IP"), 115 | os.environ.get("NS_USER"), 116 | os.environ.get("NS_PASSWORD"), 117 | app_info, 118 | os.environ.get("NS_CONFIG_FRONT_END")) 119 | 120 | parser = argparse.ArgumentParser() 121 | group = parser.add_mutually_exclusive_group(required=True) 122 | group.add_argument("--swarm-url", dest='swarm_url') 123 | group.add_argument("--marathon-url", dest='marathon_url') 124 | group.add_argument("--kube-config", dest='kube_config') 125 | group.add_argument("--kube-apiserver", dest='kube_server') 126 | group.add_argument("--cfg-file", dest='cfg_file') 127 | result = parser.parse_known_args() 128 | 129 | if result[0].swarm_url: 130 | docker_swarm(app_info, netskaler) 131 | elif result[0].marathon_url: 132 | mesos_marathon(app_info, netskaler) 133 | elif result[0].kube_config or result[0].kube_server: 134 | kubernetes(app_info, netskaler) 135 | elif result[0].cfg_file: 136 | cfg_file_driver(netskaler, result[0].cfg_file) 137 | -------------------------------------------------------------------------------- /kubernetes/kubernetes.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import json 4 | import logging 5 | import requests 6 | import requests.exceptions 7 | from client import K8sClient 8 | # from pykube.config import KubeConfig 9 | # from pykube.http import HTTPClient 10 | 11 | 12 | logger = logging.getLogger('docker_netscaler') 13 | 14 | 15 | class KubernetesInterface(object): 16 | """Interface for the Kubernetes REST API.""" 17 | 18 | def __init__(self, netskaler, app_info, 19 | cfg_file=None, token=None, ca=None, 20 | server=None, insecure=False): 21 | """Constructor 22 | 23 | :param str cfg_file: location of kubectl config (e.g., ~/.kube/config) 24 | :param NetscalerInterface netskaler: Netscaler object 25 | :param app_info : dictionary of app names 26 | :param token: Auth (bearer) token 27 | :param server: Kubernetes URL (e.g., 'http://api-server:8080' ) 28 | :param ca: certificate authority of kube api server 29 | :param insecure: whether to ignore certificate host mismatch 30 | """ 31 | self.cfg_file = cfg_file 32 | self.netskaler = netskaler 33 | self.app_info = app_info 34 | self.insecure_tls_skip_verify = insecure 35 | self.client = K8sClient(cfg_file=cfg_file, 36 | url=server, 37 | token=token, 38 | ca=ca, 39 | insecure_skip_tls_verify=insecure) 40 | 41 | def _get(self, api, namespace='default'): 42 | response = None 43 | success = True 44 | try: 45 | # TODO:support other namespace 46 | response = self.client.get(url=api, 47 | namespace=namespace) 48 | except requests.exceptions.RequestException as e: 49 | logger.error('Error while calling %s:%s', api, e.message) 50 | success = False # TODO: throw exception 51 | if success and response.status_code >= 300: 52 | logger.error('Got HTTP {code}: {body}'. 53 | format(code=response.status_code, body=response.text)) 54 | success = False 55 | return success, response 56 | 57 | def get_node_ports(self): 58 | success, response = self._get('/services') 59 | if not success: 60 | return [] 61 | svc_list = response.json() 62 | nodePorts = [{item['metadata']['name']: 63 | [port['nodePort'] for port in item['spec']['ports']]} 64 | for item in svc_list['items'] 65 | if item['spec']['type'] == 'NodePort'] 66 | # nodePorts.keys() has names of services that have NodePort 67 | return nodePorts 68 | 69 | def get_backends_for_app(self, appid): 70 | """Get host endpoints for apps (services) 71 | 72 | :returns: list of endpoint (hostIp, port) tuples 73 | :rtype: list 74 | """ 75 | backends = [] 76 | api = '/services/' + appid 77 | success, response = self._get(api) 78 | if not success and response and response.status_code >= 300: 79 | status = response.json() 80 | if status['reason'] == 'NotFound': 81 | logger.info("Service %s not found" % appid) 82 | if not success: 83 | return backends 84 | svc = response.json() 85 | # node port is the backend port we need. Handle only 1 port for now 86 | nodePort = svc['spec']['ports'][0]['nodePort'] # TODO 87 | if nodePort == 0: 88 | logger.warn("Service %s does not have a node port" % appid) 89 | return backends 90 | # find the endpoint for the service so that we can find its pods 91 | api = '/endpoints' 92 | success, endpoints = self._get(api) 93 | if not success: 94 | return backends 95 | podnames = [] 96 | if endpoints.status_code == 200: 97 | endpoints = endpoints.json() 98 | for ep in endpoints['items']: 99 | if ep['metadata']['name'] == appid: 100 | if ep['subsets']: 101 | podnames = [addr['targetRef']['name'] 102 | for addr in ep['subsets'][0]['addresses']] 103 | break 104 | for p in podnames: 105 | api = '/pods/' + p 106 | success, response = self._get(api) 107 | if not success: 108 | continue 109 | pod = response.json() 110 | status = pod['status']['phase'] 111 | host = pod['status']['hostIP'] 112 | if status == 'Running': 113 | backends.append((host, nodePort)) 114 | return list(set(backends)) 115 | 116 | def events(self, resource_version): 117 | """Get event stream for k8s endpoints 118 | """ 119 | url = self.client.url +\ 120 | "/v1/watch/namespaces/default/endpoints?" +\ 121 | "resourceVersion=%s&watch=true" % resource_version 122 | evts = self.client.session.request('GET', url, 123 | stream=True) 124 | # TODO re-start the loop when disconnected from api server 125 | for e in evts.iter_lines(): 126 | event_json = json.loads(e) 127 | yield event_json 128 | 129 | def watch_all_apps(self): 130 | appnames = map(lambda x: x['name'], self.app_info['apps']) 131 | api = '/endpoints' 132 | success, response = self._get(api) 133 | if not success: 134 | logger.error("Failed to watch for endpoint changes, exiting") 135 | return 136 | endpoints = response.json() 137 | resource_version = endpoints['metadata']['resourceVersion'] 138 | for e in self.events(resource_version): 139 | service_name = e['object']['metadata']['name'] 140 | if service_name in appnames: 141 | self.configure_ns_for_app(service_name) 142 | 143 | def configure_ns_for_app(self, appname): 144 | backends = self.get_backends_for_app(appname) 145 | logger.info("Backends for %s are %s" % (appname, str(backends))) 146 | self.netskaler.configure_app(appname, backends) 147 | 148 | def configure_ns_for_all_apps(self): 149 | appnames = map(lambda x: x['name'], self.app_info['apps']) 150 | for app in appnames: 151 | self.configure_ns_for_app(app) 152 | 153 | 154 | if __name__ == "__main__": 155 | logging.basicConfig(level=logging.INFO) 156 | parser = argparse.ArgumentParser(description='Process Kubernetes args') 157 | parser.add_argument("--kubeconfig", required=False, dest='cfg') 158 | parser.add_argument("--token", required=False, dest='token') 159 | parser.add_argument("--server", required=False, dest='server') 160 | parser.add_argument("--insecure-tls-verify", required=False, 161 | dest='insecure') 162 | 163 | result = parser.parse_args() 164 | 165 | # '{"appkey": "com.citrix.lb.appname", "apps": [{"name": "foo"}, 166 | # {"name": "bar"}]}' 167 | app_info = json.loads(os.environ['APP_INFO']) 168 | appnames = map(lambda x: x['name'], app_info['apps']) 169 | 170 | kube = KubernetesInterface(netskaler=None, app_info=app_info, 171 | cfg_file=result.cfg, insecure=True) 172 | for app in appnames: 173 | endpoints = kube.get_backends_for_app(app) 174 | logger.info("Endpoints for app " + app + ": " + str(endpoints)) 175 | kube.watch_all_apps() 176 | -------------------------------------------------------------------------------- /netscaler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from functools import wraps 4 | import logging 5 | 6 | from nssrc.com.citrix.netscaler.nitro.exception.nitro_exception \ 7 | import nitro_exception 8 | from nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbvserver \ 9 | import lbvserver 10 | from nssrc.com.citrix.netscaler.nitro.service.nitro_service\ 11 | import nitro_service 12 | from nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup\ 13 | import servicegroup 14 | from nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbvserver_servicegroup_binding\ 15 | import lbvserver_servicegroup_binding 16 | from nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_servicegroupmember_binding\ 17 | import servicegroup_servicegroupmember_binding 18 | 19 | 20 | logger = logging.getLogger('docker_netscaler') 21 | 22 | 23 | def ns_session_scope(func): 24 | @wraps(func) 25 | def login_logout(self, *args, **kwargs): 26 | self.ns_session = nitro_service(self.nsip, 'HTTP') 27 | self.ns_session.set_credential(self.nslogin, self.nspasswd) 28 | self.ns_session.timeout = 600 29 | self.ns_session.login() 30 | result = func(self, *args, **kwargs) 31 | self.ns_session.logout() 32 | self.ns_session = None 33 | return result 34 | return login_logout 35 | 36 | 37 | class NetscalerInterface: 38 | 39 | def __init__(self, nsip, nslogin, nspasswd, app_info, 40 | configure_frontends=False): 41 | self.nsip = nsip 42 | self.nslogin = nslogin 43 | self.nspasswd = nspasswd 44 | self.ns_session = None 45 | self.app_info = app_info 46 | """ 47 | app_info expected structure: 48 | '{"appkey": "com.citrix.lb.appname", 49 | "apps": [{"name": "foo0", "lb_ip":"10.220.73.122", "lb_port":"443"}, 50 | {"name": "foo1", "lb_ip":"10.220.73.123", "lb_port":"80"}, 51 | {"name":"foo2"}, {"name":"foo3"}]}' 52 | """ 53 | if configure_frontends: 54 | frontends = [(l['name'], l['lb_ip'], l['lb_port']) 55 | for l in self.app_info['apps'] 56 | if l.get('lb_ip') and l.get('lb_port')] 57 | for f in frontends: 58 | self.configure_lb_frontend(f[0], f[1], f[2]) 59 | 60 | def _create_service_group(self, grpname): 61 | try: 62 | svc_grp = servicegroup.get(self.ns_session, grpname) 63 | if (svc_grp.servicegroupname == grpname): 64 | logger.info("Service group %s already configured " % grpname) 65 | return 66 | except nitro_exception as e: 67 | pass 68 | svc_grp = servicegroup() 69 | svc_grp.servicegroupname = grpname 70 | svc_grp.servicetype = "HTTP" 71 | servicegroup.add(self.ns_session, svc_grp) 72 | 73 | def _create_lb(self, lbname, lbmethod, vip, port): 74 | try: 75 | lb = lbvserver.get(self.ns_session, lbname) 76 | if (lb.name == lbname) and \ 77 | (lb.ipv46 == vip) and \ 78 | (str(lb.port) == port): 79 | logger.info("LB %s is already configured " % lbname) 80 | return 81 | else: 82 | logger.info("LB %s is already configured with a different \ 83 | VIP/port : %s:%s\n" % (lb.name, lb.ipv46, lb.port)) 84 | raise Exception("LB %s already configured with different VIP/\ 85 | port : %s:%s\n" % (lbname, lb.ipv46, lb.port)) 86 | except nitro_exception as e: 87 | pass 88 | 89 | lb = lbvserver() 90 | lb.name = lbname 91 | lb.ipv46 = vip 92 | lb.servicetype = "HTTP" 93 | lb.port = port 94 | lb.lbmethod = lbmethod 95 | lbvserver.add(self.ns_session, lb) 96 | 97 | def _add_service(self, grpname, srvr_ip, srvr_port): 98 | try: 99 | bindings = servicegroup_servicegroupmember_binding.get( 100 | self.ns_session, grpname) 101 | for binding in bindings: 102 | if binding.ip == srvr_ip and str(binding.port) == srvr_port: 103 | logger.info("Service %s:%s is already bound to service \ 104 | group %s " % (srvr_ip, srvr_port, grpname)) 105 | return 106 | 107 | except nitro_exception as e: 108 | pass 109 | binding = servicegroup_servicegroupmember_binding() 110 | binding.servicegroupname = grpname 111 | binding.ip = srvr_ip 112 | binding.port = srvr_port 113 | servicegroup_servicegroupmember_binding.add(self.ns_session, binding) 114 | 115 | def _bind_service_group_lb(self, lbname, grpname): 116 | try: 117 | bindings = lbvserver_servicegroup_binding.get(self.ns_session, 118 | lbname) 119 | for b in bindings: 120 | if b.name == lbname and b.servicegroupname == grpname: 121 | logger.info("LB %s is already bound to service group %s" 122 | % (lbname, grpname)) 123 | return 124 | except nitro_exception as e: 125 | pass 126 | 127 | binding = lbvserver_servicegroup_binding() 128 | binding.name = lbname 129 | binding.servicegroupname = grpname 130 | lbvserver_servicegroup_binding.add(self.ns_session, binding) 131 | 132 | def _configure_services(self, grpname, srvrs): 133 | to_add = srvrs 134 | to_remove = [] 135 | try: 136 | bindings = servicegroup_servicegroupmember_binding.get( 137 | self.ns_session, grpname) 138 | existing = [(b.ip, b.port) for b in bindings if b.port != 0] 139 | to_remove = list(set(existing) - set(srvrs)) 140 | to_add = list(set(srvrs) - set(existing)) 141 | to_leave = list(set(srvrs) & set(existing)) 142 | except nitro_exception as e: 143 | pass # no bindings 144 | for s in to_remove: 145 | binding = servicegroup_servicegroupmember_binding() 146 | binding.servicegroupname = grpname 147 | binding.ip = s[0] 148 | binding.port = s[1] 149 | logger.info("Unbinding %s:%s from service group %s " % (s[0], s[1], 150 | grpname)) 151 | servicegroup_servicegroupmember_binding.delete(self.ns_session, 152 | binding) 153 | for s in to_add: 154 | binding = servicegroup_servicegroupmember_binding() 155 | binding.servicegroupname = grpname 156 | binding.ip = s[0] 157 | binding.port = s[1] 158 | logger.info("Binding %s:%s from service group %s " % 159 | (s[0], s[1], grpname)) 160 | servicegroup_servicegroupmember_binding.add(self.ns_session, 161 | binding) 162 | for s in to_leave: 163 | logger.info("%s:%s is already bound to service group %s" 164 | % (s[0], s[1], grpname)) 165 | 166 | @ns_session_scope 167 | def configure_lb_frontend(self, lbname, lb_vip, lb_port): 168 | try: 169 | self._create_lb(lbname, "ROUNDROBIN", lb_vip, lb_port) 170 | except nitro_exception as ne: 171 | logger.warn("Nitro Exception: %s" % ne.message) 172 | except Exception as e: 173 | logger.warn("Exception: %s" % e.message) 174 | 175 | @ns_session_scope 176 | def configure_lb(self, lbname, lb_vip, lb_ports, srvrs): 177 | try: 178 | self._create_lb(lbname, "ROUNDROBIN", lb_vip, lb_ports) 179 | self._create_service_group(lbname) # Reuse lbname 180 | self._bind_service_group_lb(lbname, lbname) 181 | self._configure_services(lbname, srvrs) 182 | except nitro_exception as ne: 183 | logger.warn("Nitro Exception: %s" % ne.message) 184 | except Exception as e: 185 | logger.warn("Exception: %s" % e.message) 186 | 187 | @ns_session_scope 188 | def configure_app(self, lbname, srvrs): 189 | try: 190 | self._create_service_group(lbname) # Reuse lbname 191 | self._bind_service_group_lb(lbname, lbname) 192 | self._configure_services(lbname, srvrs) 193 | except nitro_exception as ne: 194 | logger.warn("Nitro Exception: %s" % ne.message) 195 | except Exception as e: 196 | logger.warn("Exception: %s" % e.message) 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Citrix Systems, Inc. 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 | --------------------------------------------------------------------------------